import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { getAllTools, ESSENTIAL_TOOLS } from "../../src/mcp/tools-registry.js"; import { ADAPTERS } from "../../src/cli/connect/index.js"; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, "..", ".."); const SKILLS = join(ROOT, "plugin", "skills"); const SRC = join(ROOT, "src"); const check = process.argv.includes("--check"); function clean(s: string): string { return s.replace(/\s*[—–]\s*/g, ", "); } function block(key: string, body: string): { open: string; close: string; full: string } { const open = ``; const close = ``; return { open, close, full: `${open}\n${body.trim()}\n${close}` }; } function applyBlock(file: string, key: string, body: string): void { const { open, close, full } = block(key, body); const existing = existsSync(file) ? readFileSync(file, "utf8") : ""; let next: string; if (existing.includes(open) && existing.includes(close)) { const start = existing.indexOf(open); const end = existing.indexOf(close) + close.length; next = existing.slice(0, start) + full + existing.slice(end); } else if (existing.trim()) { next = `${existing.trimEnd()}\n\n${full}\n`; } else { next = `${full}\n`; } if (check) { if (existing !== next) { console.error(`DRIFT: ${file.replace(ROOT + "/", "")} (AUTOGEN:${key} out of date — run \`npm run skills:gen\`)`); process.exitCode = 1; } return; } if (existing !== next) { writeFileSync(file, next); console.log(`wrote AUTOGEN:${key} -> ${file.replace(ROOT + "/", "")}`); } } function walk(dir: string, out: string[] = []): string[] { for (const e of readdirSync(dir, { withFileTypes: true })) { const full = join(dir, e.name); if (e.isDirectory()) { if (e.name === "node_modules" || e.name === "dist") continue; walk(full, out); } else if (e.name.endsWith(".ts")) { out.push(full); } } return out; } function mdEscape(s: string): string { return clean(s.replace(/\|/g, "\\|").replace(/\n/g, " ")).trim(); } function tools(): string { const all = getAllTools(); const lines = [ `agentmemory exposes ${all.length} MCP tools. ${ESSENTIAL_TOOLS.size} are in the lean core set (\`--tools core\` or \`AGENTMEMORY_TOOLS=core\`); the rest load with \`--tools all\` (default).`, "", "| Tool | Core | Parameters | Purpose |", "| --- | --- | --- | --- |", ]; for (const t of all.sort((a, b) => a.name.localeCompare(b.name))) { const required = new Set(t.inputSchema.required ?? []); const params = Object.entries(t.inputSchema.properties ?? {}) .map(([n, s]) => `\`${n}\`${required.has(n) ? "*" : ""}: ${s.type}`) .join(", ") || "none"; const core = ESSENTIAL_TOOLS.has(t.name) ? "yes" : ""; lines.push(`| \`${t.name}\` | ${core} | ${mdEscape(params)} | ${mdEscape(t.description)} |`); } lines.push("", "`*` marks required parameters."); return lines.join("\n"); } function rest(): string { const text = readFileSync(join(SRC, "triggers", "api.ts"), "utf8"); const found: { path: string; method: string }[] = []; const re = /api_path:\s*"([^"]+)"/g; let m: RegExpExecArray | null; while ((m = re.exec(text)) !== null) { const path = m[1]; const win = text.slice(Math.max(0, m.index - 140), m.index + 140); const mm = /http_method:\s*"([A-Z]+)"/.exec(win); found.push({ path, method: mm ? mm[1] : "POST" }); } const seen = new Set(); const rows = found .filter((e) => (seen.has(e.path) ? false : (seen.add(e.path), true))) .sort((a, b) => a.path.localeCompare(b.path)); const lines = [ `The REST API is the primary surface. All paths are under \`http://localhost:3111\` (override with \`--port\`). When \`AGENTMEMORY_SECRET\` is set, send \`Authorization: Bearer $AGENTMEMORY_SECRET\`; localhost is otherwise open.`, "", `${rows.length} registered endpoints:`, "", "| Method | Path |", "| --- | --- |", ...rows.map((e) => `| ${e.method} | \`${e.path}\` |`), ]; return lines.join("\n"); } function env(): string { const files = walk(SRC); const vars = new Set(); for (const f of files) { const text = readFileSync(f, "utf8"); const re = /AGENTMEMORY_[A-Z0-9_]+/g; let m: RegExpExecArray | null; while ((m = re.exec(text)) !== null) { if (!m[0].endsWith("__")) vars.add(m[0]); } } const sorted = [...vars].sort(); const lines = [ `Configuration is read from the environment and from \`~/.agentmemory/.env\` (no \`export\` prefix). ${sorted.length} recognized variables:`, "", ...sorted.map((v) => `- \`${v}\``), ]; return lines.join("\n"); } function agents(): string { const lines = [ `\`agentmemory connect \` wires the memory server into a host agent. ${ADAPTERS.length} adapters:`, "", "| Agent | Name | Protocol |", "| --- | --- | --- |", ]; for (const a of [...ADAPTERS].sort((x, y) => x.name.localeCompare(y.name))) { const note = (a.protocolNote ?? "").replace(/^[→\s]+/, ""); lines.push(`| ${mdEscape(a.displayName)} | \`${a.name}\` | ${mdEscape(note) || "MCP"} |`); } return lines.join("\n"); } function hooks(): string { const file = join(ROOT, "plugin", "hooks", "hooks.json"); const json = JSON.parse(readFileSync(file, "utf8")) as { hooks?: Record }; const events = Object.keys(json.hooks ?? {}).sort(); const lines = [ `The Claude Code plugin registers hooks on ${events.length} lifecycle events to capture observations automatically:`, "", ...events.map((e) => `- \`${e}\``), ]; return lines.join("\n"); } applyBlock(join(SKILLS, "agentmemory-mcp-tools", "REFERENCE.md"), "tools", tools()); applyBlock(join(SKILLS, "agentmemory-rest-api", "REFERENCE.md"), "rest", rest()); applyBlock(join(SKILLS, "agentmemory-config", "REFERENCE.md"), "env", env()); applyBlock(join(SKILLS, "agentmemory-agents", "REFERENCE.md"), "agents", agents()); applyBlock(join(SKILLS, "agentmemory-hooks", "REFERENCE.md"), "hooks", hooks()); if (check && process.exitCode) { console.error("\nSkill reference docs are stale. Run: npm run skills:gen"); } else if (!check) { console.log("skills reference generation complete"); }