chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, "..", "..");
|
||||
const SKILLS = join(ROOT, "plugin", "skills");
|
||||
|
||||
const errors: string[] = [];
|
||||
const MAX_LINES = 100;
|
||||
const DUP_MARKER = "/plugin list";
|
||||
|
||||
function parseFrontmatter(text: string): Record<string, string> | null {
|
||||
if (!text.startsWith("---")) return null;
|
||||
const end = text.indexOf("\n---", 3);
|
||||
if (end === -1) return null;
|
||||
const body = text.slice(3, end);
|
||||
const out: Record<string, string> = {};
|
||||
for (const line of body.split("\n")) {
|
||||
const m = /^([a-zA-Z_-]+):\s*(.*)$/.exec(line.trim());
|
||||
if (m) out[m[1]] = m[2];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const dirs = readdirSync(SKILLS, { withFileTypes: true })
|
||||
.filter((e) => e.isDirectory() && !e.name.startsWith("_"))
|
||||
.map((e) => e.name);
|
||||
|
||||
for (const name of dirs) {
|
||||
const skillFile = join(SKILLS, name, "SKILL.md");
|
||||
if (!existsSync(skillFile)) {
|
||||
errors.push(`${name}: missing SKILL.md`);
|
||||
continue;
|
||||
}
|
||||
const text = readFileSync(skillFile, "utf8");
|
||||
const rel = `plugin/skills/${name}/SKILL.md`;
|
||||
|
||||
const fm = parseFrontmatter(text);
|
||||
if (!fm) {
|
||||
errors.push(`${rel}: missing or malformed frontmatter`);
|
||||
} else {
|
||||
if (!fm.name) errors.push(`${rel}: frontmatter missing 'name'`);
|
||||
if (fm.name && fm.name !== name) errors.push(`${rel}: frontmatter name '${fm.name}' != dir '${name}'`);
|
||||
if (!fm.description) errors.push(`${rel}: frontmatter missing 'description'`);
|
||||
else {
|
||||
if (!/use when/i.test(fm.description)) errors.push(`${rel}: description must contain a "Use when ..." trigger sentence`);
|
||||
if (fm.description.length > 1024) errors.push(`${rel}: description exceeds 1024 chars`);
|
||||
}
|
||||
}
|
||||
|
||||
const lines = text.split("\n").length;
|
||||
if (lines > MAX_LINES) errors.push(`${rel}: ${lines} lines (max ${MAX_LINES}); move detail into REFERENCE.md or EXAMPLES.md`);
|
||||
|
||||
const body = fm ? text.slice(text.indexOf("\n---", 3) + 4) : text;
|
||||
if (body.includes(DUP_MARKER)) {
|
||||
errors.push(`${rel}: inlines the shared troubleshooting block; reference ../_shared/TROUBLESHOOTING.md instead`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!existsSync(join(SKILLS, "_shared", "TROUBLESHOOTING.md"))) {
|
||||
errors.push(`_shared/TROUBLESHOOTING.md is missing`);
|
||||
}
|
||||
|
||||
const pluginJson = join(ROOT, "plugin", "plugin.json");
|
||||
if (existsSync(pluginJson)) {
|
||||
const desc = (JSON.parse(readFileSync(pluginJson, "utf8")).description as string) ?? "";
|
||||
const m = /(\d+)\s+skills/.exec(desc);
|
||||
if (!m) errors.push(`plugin/plugin.json: description should state the skill count as "N skills"`);
|
||||
else if (Number(m[1]) !== dirs.length) {
|
||||
errors.push(`plugin/plugin.json: description says ${m[1]} skills but ${dirs.length} skill dirs exist`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
console.error("Skill lint failed:");
|
||||
for (const e of errors) console.error(` - ${e}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Skill lint passed: ${dirs.length} skills checked.`);
|
||||
@@ -0,0 +1,170 @@
|
||||
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");
|
||||
}
|
||||
Reference in New Issue
Block a user