Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

pi/scripts/check_agent_content.mjs

Raw
#!/usr/bin/env node
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join, relative } from "node:path";

const ROOT = process.cwd();
const NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;

function walk(dir, predicate, out = []) {
  if (!existsSync(dir)) return out;
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const path = join(dir, entry.name);
    if (entry.isDirectory()) walk(path, predicate, out);
    else if (predicate(path, entry.name)) out.push(path);
  }
  return out;
}

function skillFiles() {
  return walk(join(ROOT, "agent", "skills"), (_, name) => name === "SKILL.md").sort();
}

function promptFiles() {
  return walk(join(ROOT, "agent", "prompts"), (_, name) => name.endsWith(".md")).sort();
}

function frontmatter(path) {
  const lines = readFileSync(path, "utf8").split(/\r?\n/);
  if (lines[0] !== "---") throw new Error("missing opening frontmatter fence");
  const end = lines.indexOf("---", 1);
  if (end === -1) throw new Error("missing closing frontmatter fence");
  return lines.slice(1, end);
}

function unquote(value) {
  const trimmed = value.trim();
  if (
    (trimmed.startsWith("\"") && trimmed.endsWith("\""))
    || (trimmed.startsWith("'") && trimmed.endsWith("'"))
  ) return trimmed.slice(1, -1);
  return trimmed;
}

function readIndented(lines, index) {
  const block = [];
  while (index + 1 < lines.length && /^\s+/.test(lines[index + 1])) block.push(lines[++index].trim());
  return [block, index];
}

function parseFrontmatter(lines) {
  const data = {};
  for (let i = 0; i < lines.length; i++) {
    const match = /^(\w[\w-]*):(?:\s*(.*))?$/.exec(lines[i]);
    if (!match) continue;
    const [, key, raw = ""] = match;
    const value = raw.trim();
    if (value === ">" || value === "|") {
      const [block, next] = readIndented(lines, i);
      i = next;
      data[key] = block.join(value === ">" ? " " : "\n");
    } else if (!value && i + 1 < lines.length && /^\s+/.test(lines[i + 1])) {
      const [block, next] = readIndented(lines, i);
      i = next;
      data[key] = unquote(block.join(" "));
    } else {
      data[key] = unquote(raw);
    }
  }
  return data;
}

function validateFrontmatter(path) {
  try {
    return parseFrontmatter(frontmatter(path));
  } catch (error) {
    return { __error: error.message };
  }
}

function validateSkill(path) {
  const data = validateFrontmatter(path);
  if (data.__error) return [data.__error];
  const errors = [];
  const dir = path.split(/[\\/]/).at(-2);
  if (!data.name) errors.push("missing string field: name");
  else if (!NAME_RE.test(data.name)) errors.push(`invalid skill name: ${JSON.stringify(data.name)}`);
  else if (dir !== data.name) {
    errors.push(`directory/name mismatch: ${JSON.stringify(dir)} != ${JSON.stringify(data.name)}`);
  }
  if (!data.description?.trim()) errors.push("missing string field: description");
  else if (data.description.length > 1024) errors.push(`description too long: ${data.description.length} > 1024`);
  return errors;
}

function validatePrompt(path) {
  const data = validateFrontmatter(path);
  if (data.__error) return [data.__error];
  if (!data.description?.trim()) return ["missing string field: description"];
  return [];
}

let failed = false;
for (
  const [kind, files, validate] of [
    ["skill", skillFiles(), validateSkill],
    ["prompt", promptFiles(), validatePrompt],
  ]
) {
  if (files.length === 0) {
    failed = true;
    console.log(`no ${kind} files found`);
  }
  for (const path of files) {
    for (const error of validate(path)) {
      failed = true;
      console.log(`${relative(ROOT, path)}: ${error}`);
    }
  }
}

process.exit(failed ? 1 : 0);