Spaces:
Running
Running
| 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 = `<!-- AUTOGEN:${key} START - generated by scripts/skills/generate.ts, do not edit by hand -->`; | |
| const close = `<!-- AUTOGEN:${key} END -->`; | |
| 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<string>(); | |
| 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<string>(); | |
| 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 <agent>\` 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<string, unknown> }; | |
| 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"); | |
| } | |