activate-agentmd 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +158 -0
  2. package/bin/agentmd.js +186 -0
  3. package/package.json +55 -0
  4. package/src/commands/agents.js +44 -0
  5. package/src/commands/analytics.js +128 -0
  6. package/src/commands/auth.js +144 -0
  7. package/src/commands/ci.js +296 -0
  8. package/src/commands/extract.js +254 -0
  9. package/src/commands/info.js +105 -0
  10. package/src/commands/init.js +166 -0
  11. package/src/commands/install.js +216 -0
  12. package/src/commands/link.js +206 -0
  13. package/src/commands/list.js +99 -0
  14. package/src/commands/outdated.js +92 -0
  15. package/src/commands/remove.js +72 -0
  16. package/src/commands/review.js +293 -0
  17. package/src/commands/search.js +110 -0
  18. package/src/commands/sync.js +245 -0
  19. package/src/commands/telemetry.js +82 -0
  20. package/src/commands/test.js +226 -0
  21. package/src/commands/update.js +115 -0
  22. package/src/commands/validate.js +125 -0
  23. package/src/config/license-public-keys.json +17 -0
  24. package/src/detect.json +452 -0
  25. package/src/lib/agents.js +301 -0
  26. package/src/lib/analytics.js +137 -0
  27. package/src/lib/coordinates.js +103 -0
  28. package/src/lib/credentials.js +115 -0
  29. package/src/lib/detect.js +352 -0
  30. package/src/lib/diff.js +142 -0
  31. package/src/lib/enterprise.js +140 -0
  32. package/src/lib/fetcher.js +174 -0
  33. package/src/lib/invocation.js +47 -0
  34. package/src/lib/license.js +234 -0
  35. package/src/lib/manifest.js +191 -0
  36. package/src/lib/patterns.js +135 -0
  37. package/src/lib/pro.js +149 -0
  38. package/src/lib/ranking.js +287 -0
  39. package/src/lib/registry.js +207 -0
  40. package/src/lib/star.js +105 -0
  41. package/src/lib/status.js +75 -0
  42. package/src/lib/telemetry.js +169 -0
  43. package/src/lib/versions.js +190 -0
  44. package/src/registry.json +33907 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * telemetry.js (command)
3
+ *
4
+ * Handles: agentmd telemetry [--enable|--disable|--status]
5
+ *
6
+ * Shows exactly what would be sent, as a rendered example rather than a
7
+ * promise. "Anonymous usage data" means nothing to a developer deciding
8
+ * whether to turn it on; the actual JSON means everything.
9
+ */
10
+
11
+ "use strict";
12
+
13
+ const pc = require("picocolors");
14
+ const t = require("../lib/telemetry");
15
+ const { cmd } = require("../lib/invocation");
16
+
17
+ const A = cmd();
18
+
19
+ /** The real payload for a real event, built by the real code path. */
20
+ function samplePayload() {
21
+ return t.sanitise({
22
+ event: t.EVENTS.COMMAND_RUN,
23
+ command: "link",
24
+ ok: true,
25
+ durationBucket: "<1s",
26
+ cliVersion: require("../../package.json").version,
27
+ nodeMajor: process.versions.node.split(".")[0],
28
+ platform: process.platform,
29
+ ci: !!process.env.CI,
30
+ anonymousId: t.readSettings().anonymousId || "3f2a…-generated-on-opt-in",
31
+ sentAt: new Date().toISOString(),
32
+ // Deliberately passed and deliberately dropped by the allowlist, to prove
33
+ // the filter is real rather than a claim in a readme.
34
+ cwd: process.cwd(),
35
+ repo: "acme/private-thing",
36
+ });
37
+ }
38
+
39
+ function telemetry(args) {
40
+ args = args || [];
41
+ const has = (name) => args.includes(`--${name}`);
42
+
43
+ if (has("enable")) {
44
+ const settings = t.enable();
45
+ console.log(`\n ${pc.green("✓")} Telemetry on. ${pc.dim(`Anonymous id ${settings.anonymousId.slice(0, 8)}… · ${t.settingsPath()}`)}`);
46
+ console.log(pc.dim(` Thank you — it decides which platforms and commands keep getting work.`));
47
+ console.log(pc.dim(` Off again any time with '${A} telemetry --disable'.\n`));
48
+ return;
49
+ }
50
+
51
+ if (has("disable")) {
52
+ t.disable();
53
+ console.log(`\n ${pc.green("✓")} Telemetry off. ${pc.dim("The anonymous id has been deleted, not just disabled.")}\n`);
54
+ return;
55
+ }
56
+
57
+ const on = t.isEnabled();
58
+ const envOverride =
59
+ process.env.DO_NOT_TRACK || process.env.DONT_TRACK ? "DO_NOT_TRACK is set in your environment"
60
+ : process.env.AGENTMD_TELEMETRY_DISABLED ? "AGENTMD_TELEMETRY_DISABLED is set"
61
+ : process.env.AGENTMD_TELEMETRY ? `AGENTMD_TELEMETRY=${process.env.AGENTMD_TELEMETRY}` : null;
62
+
63
+ console.log(`\n Telemetry is ${on ? pc.green("on") : pc.white("off")}${envOverride ? pc.dim(` — ${envOverride}`) : ""}`);
64
+ console.log(pc.dim(` ${on ? "Opted in" : "Off by default. Nothing has been sent."} · ${t.settingsPath()}`));
65
+
66
+ console.log(pc.bold("\n What one event looks like\n"));
67
+ for (const line of JSON.stringify(samplePayload(), null, 2).split("\n")) console.log(" " + pc.dim(line));
68
+
69
+ console.log(pc.bold("\n What is never in it\n"));
70
+ for (const item of [
71
+ "file paths, repository names, branch names",
72
+ "package names, standards you installed, diffs, findings",
73
+ "your license key, email, username or hostname",
74
+ ]) console.log(` ${pc.dim("·")} ${pc.dim(item)}`);
75
+
76
+ console.log(pc.dim(`\n The payload is built from an allowlist (${t.ALLOWED_FIELDS.length} fields) in src/lib/telemetry.js —`));
77
+ console.log(pc.dim(" a field that is not named there cannot be sent, including the two the"));
78
+ console.log(pc.dim(" sample above tries to pass in.\n"));
79
+ console.log(pc.dim(` ${A} telemetry --enable · ${A} telemetry --disable\n`));
80
+ }
81
+
82
+ module.exports = { telemetry, samplePayload };
@@ -0,0 +1,226 @@
1
+ /**
2
+ * test.js
3
+ *
4
+ * `agentmd test` — measures whether a model actually obeys the installed
5
+ * standards, instead of hoping it does. For each standard: generate a few
6
+ * tasks a developer would plausibly ask, have the model attempt them with
7
+ * the standard in its system prompt, then judge each attempt strictly
8
+ * against the standard's rules.
9
+ *
10
+ * agentmd test 3 most recently installed standards
11
+ * agentmd test Security/jwt API/rest specific standards
12
+ * agentmd test --all --tasks 5 every installed standard, 5 tasks each
13
+ * agentmd test --fail-under 80 exit 1 below 80% (CI)
14
+ * agentmd test --json machine-readable results
15
+ *
16
+ * Needs ANTHROPIC_API_KEY (or an `ant auth login` profile). Costs real API
17
+ * calls: roughly N+2 per standard.
18
+ */
19
+
20
+ "use strict";
21
+
22
+ const fs = require("fs");
23
+ const pc = require("picocolors");
24
+ const { readManifest, resolvePresetPath } = require("../lib/manifest");
25
+ const { cmd } = require("../lib/invocation");
26
+
27
+ const A = cmd();
28
+ const DEFAULT_MODEL = process.env.AGENTMD_REVIEW_MODEL || "claude-opus-5";
29
+ const CONCURRENCY = 4;
30
+ // ponytail: opus-5 list price; other models will estimate wrong, hence "estimate".
31
+ const PRICE = { input: 5 / 1e6, output: 25 / 1e6 };
32
+
33
+ function flagValue(args, name) {
34
+ const withEq = args.find((a) => a.startsWith(`--${name}=`));
35
+ if (withEq) return withEq.slice(name.length + 3);
36
+ const i = args.indexOf(`--${name}`);
37
+ return i !== -1 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : null;
38
+ }
39
+
40
+ /** Positional args: anything that isn't a flag or a flag's value. */
41
+ function positionals(args) {
42
+ const out = [];
43
+ for (let i = 0; i < args.length; i++) {
44
+ const a = args[i];
45
+ if (a.startsWith("--")) {
46
+ if (!a.includes("=") && ["tasks", "model", "fail-under"].includes(a.slice(2))) i++;
47
+ continue;
48
+ }
49
+ out.push(a);
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function parseJSON(text) {
55
+ const m = /\{[\s\S]*\}/.exec(text);
56
+ if (!m) return null;
57
+ try { return JSON.parse(m[0]); } catch { return null; }
58
+ }
59
+
60
+ /** Run `fn` over items with at most `limit` in flight. */
61
+ async function pool(items, limit, fn) {
62
+ const results = new Array(items.length);
63
+ let next = 0;
64
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
65
+ while (next < items.length) {
66
+ const i = next++;
67
+ results[i] = await fn(items[i], i);
68
+ }
69
+ }));
70
+ return results;
71
+ }
72
+
73
+ function pickStandards(cwd, manifest, ids, all) {
74
+ let presets = manifest.presets;
75
+ if (ids.length) {
76
+ const want = ids.map((s) => s.toLowerCase());
77
+ presets = presets.filter((p) => want.includes(`${p.category}/${p.preset}`.toLowerCase()));
78
+ const missing = ids.filter((id) => !presets.some((p) => `${p.category}/${p.preset}`.toLowerCase() === id.toLowerCase()));
79
+ if (missing.length) {
80
+ console.error(pc.red(`Not installed: ${missing.join(", ")}`));
81
+ console.error(pc.dim(` See what is installed with '${A} list --installed'.`));
82
+ process.exit(2);
83
+ }
84
+ } else if (!all) {
85
+ presets = presets.slice().sort((a, b) => String(b.installedAt || "").localeCompare(String(a.installedAt || ""))).slice(0, 3);
86
+ }
87
+ return presets.map((p) => {
88
+ let text = "";
89
+ try { text = fs.readFileSync(resolvePresetPath(cwd, p.file), "utf-8"); } catch {}
90
+ return { id: `${p.category}/${p.preset}`, text };
91
+ }).filter((s) => s.text);
92
+ }
93
+
94
+ async function test(args) {
95
+ args = args || [];
96
+ const cwd = process.cwd();
97
+ const all = args.includes("--all");
98
+ const asJson = args.includes("--json");
99
+ const model = flagValue(args, "model") || DEFAULT_MODEL;
100
+ const tasksPer = Math.min(6, Math.max(1, parseInt(flagValue(args, "tasks") || "3", 10) || 3));
101
+ const failUnder = flagValue(args, "fail-under");
102
+ if (failUnder !== null && !(Number(failUnder) >= 0 && Number(failUnder) <= 100)) {
103
+ console.error(pc.red("--fail-under must be a percentage between 0 and 100"));
104
+ process.exit(2);
105
+ }
106
+
107
+ const manifest = readManifest(cwd);
108
+ if (manifest.presets.length === 0) {
109
+ console.log(pc.yellow("\nNo packages installed — nothing to test."));
110
+ console.log(pc.dim(` Run '${A} init' first.\n`));
111
+ return;
112
+ }
113
+ const standards = pickStandards(cwd, manifest, positionals(args), all);
114
+ if (standards.length === 0) {
115
+ console.log(pc.yellow("\nInstalled package files are missing on disk."));
116
+ console.log(pc.dim(` Run '${A} update' to restore them.\n`));
117
+ process.exit(2);
118
+ }
119
+
120
+ let Anthropic;
121
+ try { Anthropic = require("@anthropic-ai/sdk"); } catch {
122
+ console.error(pc.red("@anthropic-ai/sdk is not installed — reinstall activate-agentmd."));
123
+ process.exit(2);
124
+ }
125
+ const client = new Anthropic();
126
+ const usage = { input: 0, output: 0 };
127
+
128
+ async function call(systemBlocks, user, maxTokens) {
129
+ let response;
130
+ try {
131
+ response = await client.beta.messages.create({
132
+ model,
133
+ max_tokens: maxTokens,
134
+ betas: ["server-side-fallback-2026-07-01"],
135
+ fallbacks: "default",
136
+ system: systemBlocks,
137
+ messages: [{ role: "user", content: user }],
138
+ });
139
+ } catch (e) {
140
+ if (e instanceof Anthropic.AuthenticationError || /authentication method|api ?key/i.test(e.message)) {
141
+ console.error(pc.red("No API credentials. Set ANTHROPIC_API_KEY (or run `ant auth login`) and try again."));
142
+ } else if (e instanceof Anthropic.RateLimitError) console.error(pc.red("Rate limited — retry in a moment."));
143
+ else console.error(pc.red(`Test failed: ${e.message}`));
144
+ process.exit(2);
145
+ }
146
+ if (response.stop_reason === "refusal") {
147
+ console.error(pc.red("The model declined a request" + (response.stop_details?.explanation ? `: ${response.stop_details.explanation}` : ".")));
148
+ process.exit(2);
149
+ }
150
+ usage.input += (response.usage?.input_tokens || 0) + (response.usage?.cache_read_input_tokens || 0) + (response.usage?.cache_creation_input_tokens || 0);
151
+ usage.output += response.usage?.output_tokens || 0;
152
+ return response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
153
+ }
154
+
155
+ if (!asJson) console.log(pc.bold(`\nRule efficacy — ${standards.length} standard${standards.length === 1 ? "" : "s"} × ${tasksPer} task${tasksPer === 1 ? "" : "s"}`) + pc.dim(` (${model})\n`));
156
+
157
+ const report = [];
158
+ for (const s of standards) {
159
+ // The standard is the stable prefix across all three phases; cache it.
160
+ const standardBlock = { type: "text", text: `<standard id="${s.id}">\n${s.text}\n</standard>`, cache_control: { type: "ephemeral" } };
161
+
162
+ // a. Tasks a model unaware of the standard would plausibly get wrong.
163
+ const gen = parseJSON(await call(
164
+ [{ type: "text", text: "You design tests for coding standards. Given a standard, write short, realistic coding tasks a developer might ask an AI coding agent. Each task must be phrased naturally (no hint that a rule exists) and chosen so that an agent unaware of the standard would plausibly violate one specific rule in it. Reply with JSON only: {\"tasks\":[{\"task\":\"...\",\"targets\":\"exact rule heading\"}]}." }, standardBlock],
165
+ `Write ${tasksPer} tasks.`, 4000));
166
+ const tasks = Array.isArray(gen?.tasks) ? gen.tasks.slice(0, tasksPer) : [];
167
+ if (tasks.length === 0) {
168
+ report.push({ id: s.id, tasks: [], results: [], error: "could not generate tasks" });
169
+ if (!asJson) console.log(` ${pc.yellow("?")} ${pc.white(s.id.padEnd(24))} ${pc.dim("could not generate tasks")}`);
170
+ continue;
171
+ }
172
+
173
+ // b. Attempts, with the standard exposed exactly as `link` would.
174
+ const attempts = await pool(tasks, CONCURRENCY, (t) => call(
175
+ [{ type: "text", text: "You are a coding agent working in this project. Follow the project's engineering standard below. Reply with code and the minimum prose needed." }, standardBlock],
176
+ t.task, 8000));
177
+
178
+ // c. Judge strictly against the standard.
179
+ const transcript = tasks.map((t, i) => `<task n="${i + 1}" targets="${String(t.targets).replace(/"/g, "'")}">\n${t.task}\n</task>\n<attempt n="${i + 1}">\n${attempts[i]}\n</attempt>`).join("\n\n");
180
+ const judged = parseJSON(await call(
181
+ [{ type: "text", text: "You are a strict reviewer. Judge each attempt only against the rules in the standard — not general code quality, not taste. An attempt passes if it does not violate the standard. Reply with JSON only: {\"results\":[{\"task\":1,\"pass\":true,\"rule\":\"exact rule heading\",\"reason\":\"one sentence\"}]}. Cite the rule broken (or, on a pass, the rule most relevant)." }, standardBlock],
182
+ transcript, 4000));
183
+ const results = tasks.map((t, i) => {
184
+ const r = Array.isArray(judged?.results) ? judged.results.find((x) => Number(x.task) === i + 1) : null;
185
+ return { task: i + 1, prompt: t.task, targets: t.targets, pass: r ? !!r.pass : false, rule: r?.rule || t.targets, reason: r?.reason || "no verdict from judge" };
186
+ });
187
+ report.push({ id: s.id, tasks, results });
188
+
189
+ if (!asJson) {
190
+ const passed = results.filter((r) => r.pass).length;
191
+ const ok = passed === results.length;
192
+ console.log(` ${ok ? pc.green("✓") : pc.red("✗")} ${pc.white(s.id.padEnd(24))} ${passed}/${results.length}`);
193
+ for (const r of results.filter((x) => !x.pass)) {
194
+ console.log(` ${" ".repeat(30)}${pc.dim(`task ${r.task}: ignored "${r.rule}" — ${r.reason}`)}`);
195
+ }
196
+ }
197
+ }
198
+
199
+ const total = report.reduce((n, r) => n + r.results.length, 0);
200
+ const passed = report.reduce((n, r) => n + r.results.filter((x) => x.pass).length, 0);
201
+ const pct = total ? Math.round((passed / total) * 100) : 0;
202
+ const cost = usage.input * PRICE.input + usage.output * PRICE.output;
203
+
204
+ if (asJson) {
205
+ console.log(JSON.stringify({ model, score: { passed, total, pct }, standards: report, usage, estimatedCostUsd: +cost.toFixed(4) }, null, 2));
206
+ } else {
207
+ console.log("");
208
+ console.log(pc.bold(`Score: ${passed}/${total} (${pct}%)`));
209
+ for (const r of report) {
210
+ const failed = r.results.filter((x) => !x.pass);
211
+ if (!failed.length) continue;
212
+ const counts = {};
213
+ for (const f of failed) counts[f.rule] = (counts[f.rule] || 0) + 1;
214
+ const worst = Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0];
215
+ console.log(pc.yellow(` ${r.id}: tighten "${worst}" with an explicit negative constraint (state what must never be done, not only what should).`));
216
+ }
217
+ console.log(pc.dim(`\n ~$${cost.toFixed(2)} estimated (${usage.input} in / ${usage.output} out tokens at ${DEFAULT_MODEL} list price)\n`));
218
+ }
219
+
220
+ if (failUnder !== null && pct < Number(failUnder)) {
221
+ console.error(pc.red(` Score ${pct}% is below --fail-under ${failUnder}% — failing.\n`));
222
+ process.exit(1);
223
+ }
224
+ }
225
+
226
+ module.exports = { test };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * update.js
3
+ *
4
+ * Handles: agentmd update [--dry] [--force]
5
+ *
6
+ * Pulls the current registry content for every installed package.
7
+ *
8
+ * Locally edited files are NOT overwritten unless --force. Presets are
9
+ * instructions a developer may well have tuned for their codebase; silently
10
+ * replacing that on a routine `update` would be a data-loss bug.
11
+ */
12
+
13
+ "use strict";
14
+
15
+ const fs = require("fs");
16
+ const pc = require("picocolors");
17
+ const { readManifest, writeManifest } = require("../lib/manifest");
18
+ const { checkAll } = require("../lib/status");
19
+ const { formatDiff, stat } = require("../lib/diff");
20
+ const { cmd } = require("../lib/invocation");
21
+
22
+ /** How the user invoked us — "npx activate-agentmd" or "agentmd". */
23
+ const A = cmd();
24
+
25
+ async function update(args) {
26
+ args = args || [];
27
+ const dry = args.includes("--dry");
28
+ const force = args.includes("--force");
29
+
30
+ const cwd = process.cwd();
31
+ const manifest = readManifest(cwd);
32
+
33
+ if (manifest.presets.length === 0) {
34
+ console.log(pc.yellow("\nNo packages installed — nothing to update."));
35
+ console.log(pc.dim(` Run '${A} init' to get recommendations for this project.\n`));
36
+ return;
37
+ }
38
+
39
+ // Extracted (local/) packages have no upstream to pull from.
40
+ const registryPresets = manifest.presets.filter((p) => p.source !== "local");
41
+ for (const p of manifest.presets.filter((p) => p.source === "local")) console.log(` ${pc.dim("·")} ${p.id} — ${pc.dim("local — managed by extract")}`);
42
+
43
+ console.log(pc.dim(`\nChecking ${registryPresets.length} package${registryPresets.length === 1 ? "" : "s"}…\n`));
44
+
45
+ const results = await checkAll(cwd, registryPresets);
46
+ const tally = { updated: 0, current: 0, skipped: 0, failed: 0 };
47
+
48
+ for (const result of results) {
49
+ const { entry, state } = result;
50
+
51
+ if (state === "current" || state === "modified") {
52
+ tally.current++;
53
+ continue;
54
+ }
55
+ if (state === "failed" || state === "removed") {
56
+ console.log(pc.red(` ✗ ${entry.id} — ${result.error || "no longer in the registry"}`));
57
+ tally.failed++;
58
+ continue;
59
+ }
60
+ if (state === "conflict" && !force) {
61
+ console.log(pc.yellow(` ! ${entry.id} — edited locally and updated upstream; left alone`));
62
+ // The conflict case is the one where seeing the diff matters most: it is
63
+ // the only decision `update` cannot make for you.
64
+ if (dry) {
65
+ const preview = formatDiff(result.localContent, result.remoteContent);
66
+ if (preview) console.log(preview + "\n");
67
+ }
68
+ tally.skipped++;
69
+ continue;
70
+ }
71
+
72
+ // outdated, missing, or a forced conflict
73
+ if (dry) {
74
+ const { added, removed } = stat(result.localContent || "", result.remoteContent);
75
+ console.log(
76
+ pc.yellow(` ↑ ${entry.id}`) +
77
+ pc.dim(` v${entry.version || "?"} → v${result.remote.version} `) +
78
+ pc.green(`+${added}`) +
79
+ " " +
80
+ pc.red(`-${removed}`) +
81
+ (state === "missing" ? pc.dim(" (file missing locally)") : "")
82
+ );
83
+ const preview = formatDiff(result.localContent || "", result.remoteContent);
84
+ if (preview) console.log(preview + "\n");
85
+ tally.updated++;
86
+ continue;
87
+ }
88
+
89
+ fs.writeFileSync(result.filePath, result.remoteContent, "utf-8");
90
+ entry.checksum = result.remoteSum;
91
+ entry.version = result.remote.version;
92
+ entry.updatedAt = new Date().toISOString();
93
+ console.log(pc.green(` ✓ ${entry.id} — updated`));
94
+ tally.updated++;
95
+ }
96
+
97
+ if (!dry && tally.updated > 0) writeManifest(cwd, manifest);
98
+
99
+ console.log("");
100
+ console.log(
101
+ pc.bold(`${tally.updated} ${dry ? "would update" : "updated"}`) +
102
+ pc.dim(`, ${tally.current} current`) +
103
+ (tally.skipped ? pc.yellow(`, ${tally.skipped} skipped`) : "") +
104
+ (tally.failed ? pc.red(`, ${tally.failed} failed`) : "")
105
+ );
106
+
107
+ if (dry && tally.updated > 0) console.log(pc.dim(` Run '${A} update' to apply.`));
108
+ if (tally.skipped > 0) console.log(pc.dim(` Overwrite local edits with '${A} update --force'.`));
109
+ if (!dry && tally.updated > 0) console.log(pc.dim(` Run '${A} link' to refresh your agent config files.`));
110
+ console.log("");
111
+
112
+ if (tally.failed > 0) process.exitCode = 1;
113
+ }
114
+
115
+ module.exports = { update };
@@ -0,0 +1,125 @@
1
+ /**
2
+ * validate.js
3
+ *
4
+ * Handles: agentmd validate
5
+ *
6
+ * Checks the registry index the CLI ships with for internal consistency, and
7
+ * checks this project's manifest against it. Offline — no network needed —
8
+ * so CI can run it on every push.
9
+ *
10
+ * Exits non-zero on any error, so it can gate a release.
11
+ */
12
+
13
+ "use strict";
14
+
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+ const pc = require("picocolors");
18
+ const { getAllPresets, listModels, listCategories, getCategoryPresets } = require("../lib/registry");
19
+ const { RECOMMENDATIONS, BASELINE } = require("../lib/detect");
20
+ const { getPreset } = require("../lib/registry");
21
+ const { readManifest } = require("../lib/manifest");
22
+
23
+ const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/;
24
+
25
+ function validate() {
26
+ const errors = [];
27
+ const warnings = [];
28
+ let checked = 0;
29
+
30
+ for (const model of listModels()) {
31
+ for (const entry of getAllPresets(model)) {
32
+ checked++;
33
+
34
+ if (!SAFE_NAME.test(entry.preset)) errors.push(`${entry.id}: unsafe package name`);
35
+ if (!SAFE_NAME.test(entry.category)) errors.push(`${entry.id}: unsafe category name`);
36
+ if (entry.preset !== entry.preset.trim() || entry.category !== entry.category.trim()) {
37
+ errors.push(`${entry.id}: name has leading or trailing whitespace`);
38
+ }
39
+ if (!entry.file || entry.file.includes("/") || entry.file.includes("\\") || entry.file.includes("..")) {
40
+ errors.push(`${entry.id}: invalid file "${entry.file}"`);
41
+ }
42
+ if (!entry.file || !entry.file.toLowerCase().endsWith(".md")) {
43
+ errors.push(`${entry.id}: file is not markdown`);
44
+ }
45
+ if (!entry.version) errors.push(`${entry.id}: missing version`);
46
+ if (!entry.url.startsWith("https://")) errors.push(`${entry.id}: URL is not HTTPS`);
47
+ if (!entry.bytes) errors.push(`${entry.id}: empty preset`);
48
+
49
+ if (!entry.description) warnings.push(`${entry.id}: no description (hurts search)`);
50
+ }
51
+ }
52
+
53
+ // Coordinates resolve by normalized form (lowercase, separators stripped) so
54
+ // the website's slugified URLs work as install commands. That only stays
55
+ // unambiguous while no two names normalize alike — check it, rather than
56
+ // assume it holds as packages are added.
57
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
58
+ for (const model of listModels()) {
59
+ const categories = listCategories(model);
60
+ const seenCategory = new Map();
61
+ for (const category of categories) {
62
+ const key = normalize(category);
63
+ if (seenCategory.has(key)) {
64
+ errors.push(`${model}: categories "${category}" and "${seenCategory.get(key)}" are ambiguous once slugified`);
65
+ }
66
+ seenCategory.set(key, category);
67
+
68
+ const seenPreset = new Map();
69
+ for (const entry of getCategoryPresets(model, category)) {
70
+ const presetKey = normalize(entry.preset);
71
+ if (seenPreset.has(presetKey)) {
72
+ errors.push(`${entry.id}: ambiguous with "${seenPreset.get(presetKey)}" once slugified`);
73
+ }
74
+ seenPreset.set(presetKey, entry.preset);
75
+ }
76
+ }
77
+ }
78
+
79
+ // Every recommendation the stack detector can emit must resolve, or `init`
80
+ // would promise a package that cannot be installed.
81
+ const referenced = [...new Set(Object.values(RECOMMENDATIONS).flat().concat(BASELINE))];
82
+ for (const id of referenced) {
83
+ const [category, preset] = id.split("/");
84
+ for (const model of listModels()) {
85
+ if (!getPreset(model, category, preset)) {
86
+ warnings.push(`detect.js recommends ${id}, which ${model} does not have`);
87
+ }
88
+ }
89
+ }
90
+
91
+ // The project manifest, if there is one.
92
+ const cwd = process.cwd();
93
+ const manifest = readManifest(cwd);
94
+ for (const entry of manifest.presets) {
95
+ if (!getPreset(entry.model, entry.category, entry.preset)) {
96
+ warnings.push(`installed ${entry.id} is no longer in the registry`);
97
+ }
98
+ if (entry.file && !fs.existsSync(path.join(cwd, entry.file))) {
99
+ warnings.push(`installed ${entry.id} is missing its file (${entry.file})`);
100
+ }
101
+ }
102
+
103
+ console.log("");
104
+ console.log(pc.dim(`Validated ${checked} packages across ${listModels().length} models, ${referenced.length} detector mappings.`));
105
+ console.log("");
106
+
107
+ for (const w of warnings) console.log(` ${pc.yellow("warn")} ${w}`);
108
+ for (const e of errors) console.log(` ${pc.red("error")} ${e}`);
109
+
110
+ if (errors.length === 0 && warnings.length === 0) {
111
+ console.log(pc.green("✓ Registry is valid.\n"));
112
+ return;
113
+ }
114
+
115
+ console.log("");
116
+ console.log(
117
+ (errors.length ? pc.red(`${errors.length} error${errors.length === 1 ? "" : "s"}`) : pc.green("0 errors")) +
118
+ pc.dim(`, ${warnings.length} warning${warnings.length === 1 ? "" : "s"}`)
119
+ );
120
+ console.log("");
121
+
122
+ if (errors.length > 0) process.exitCode = 1;
123
+ }
124
+
125
+ module.exports = { validate };
@@ -0,0 +1,17 @@
1
+ {
2
+ "$comment": "Trusted Ed25519 public keys for Agent.md Pro licenses. This file is the source of truth; scripts/build-keyset.js validates it and copies it into packages/cli/src/config/ so it ships inside the npm tarball, and CI fails if the two drift. PUBLIC HALVES ONLY — a private key must never appear in this repository. A key may only be listed here once its private half is in the vault named by vaultRef. See docs/license-key-management.md.",
3
+ "schema": 1,
4
+ "defaultKid": "agmd-2026-09",
5
+ "legacyKid": "agmd-2026-09",
6
+ "keys": [
7
+ {
8
+ "kid": "agmd-2026-09",
9
+ "alg": "ed25519",
10
+ "status": "active",
11
+ "publicKey": "MCowBQYDK2VwAyEAOCiLmEn/IwwBcNlMpvJi4twQURdqeEdAPZy+pZqlBh8=",
12
+ "issuedAt": "2026-09-13",
13
+ "vaultRef": "op://Engineering/agentmd-signing-2026-09/private",
14
+ "comment": "First and only signing key. Licenses issued before kid support carry no kid and verify against this one via legacyKid."
15
+ }
16
+ ]
17
+ }