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.
- package/README.md +158 -0
- package/bin/agentmd.js +186 -0
- package/package.json +55 -0
- package/src/commands/agents.js +44 -0
- package/src/commands/analytics.js +128 -0
- package/src/commands/auth.js +144 -0
- package/src/commands/ci.js +296 -0
- package/src/commands/extract.js +254 -0
- package/src/commands/info.js +105 -0
- package/src/commands/init.js +166 -0
- package/src/commands/install.js +216 -0
- package/src/commands/link.js +206 -0
- package/src/commands/list.js +99 -0
- package/src/commands/outdated.js +92 -0
- package/src/commands/remove.js +72 -0
- package/src/commands/review.js +293 -0
- package/src/commands/search.js +110 -0
- package/src/commands/sync.js +245 -0
- package/src/commands/telemetry.js +82 -0
- package/src/commands/test.js +226 -0
- package/src/commands/update.js +115 -0
- package/src/commands/validate.js +125 -0
- package/src/config/license-public-keys.json +17 -0
- package/src/detect.json +452 -0
- package/src/lib/agents.js +301 -0
- package/src/lib/analytics.js +137 -0
- package/src/lib/coordinates.js +103 -0
- package/src/lib/credentials.js +115 -0
- package/src/lib/detect.js +352 -0
- package/src/lib/diff.js +142 -0
- package/src/lib/enterprise.js +140 -0
- package/src/lib/fetcher.js +174 -0
- package/src/lib/invocation.js +47 -0
- package/src/lib/license.js +234 -0
- package/src/lib/manifest.js +191 -0
- package/src/lib/patterns.js +135 -0
- package/src/lib/pro.js +149 -0
- package/src/lib/ranking.js +287 -0
- package/src/lib/registry.js +207 -0
- package/src/lib/star.js +105 -0
- package/src/lib/status.js +75 -0
- package/src/lib/telemetry.js +169 -0
- package/src/lib/versions.js +190 -0
- package/src/registry.json +33907 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract.js
|
|
3
|
+
*
|
|
4
|
+
* `agentmd extract` — reverse-engineer the conventions this repository
|
|
5
|
+
* already follows (but nobody wrote down) into a local standard that `link`
|
|
6
|
+
* ships to the agent like any registry package.
|
|
7
|
+
*
|
|
8
|
+
* agentmd extract write .agentmd/presets/local-Team-conventions.md
|
|
9
|
+
* agentmd extract --dry print the standard, write nothing
|
|
10
|
+
* agentmd extract --force overwrite an existing extracted standard
|
|
11
|
+
* agentmd extract --name style call the package Team/style instead
|
|
12
|
+
*
|
|
13
|
+
* Needs ANTHROPIC_API_KEY (or an `ant auth login` profile). Only the evidence
|
|
14
|
+
* sample described below leaves the machine.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
const { execFileSync } = require("child_process");
|
|
22
|
+
const pc = require("picocolors");
|
|
23
|
+
const {
|
|
24
|
+
readManifest,
|
|
25
|
+
writeManifest,
|
|
26
|
+
upsertPreset,
|
|
27
|
+
findPreset,
|
|
28
|
+
ensurePresetsDir,
|
|
29
|
+
resolvePresetPath,
|
|
30
|
+
presetFilename,
|
|
31
|
+
checksum,
|
|
32
|
+
PRESETS_DIR,
|
|
33
|
+
} = require("../lib/manifest");
|
|
34
|
+
const { cmd } = require("../lib/invocation");
|
|
35
|
+
|
|
36
|
+
const A = cmd();
|
|
37
|
+
const MODEL = process.env.AGENTMD_REVIEW_MODEL || "claude-opus-5";
|
|
38
|
+
|
|
39
|
+
// Evidence budget. ~120k chars ≈ 30k tokens: enough to see real patterns,
|
|
40
|
+
// small enough that one call stays cheap and never hits max_tokens.
|
|
41
|
+
const BUDGET = 120_000;
|
|
42
|
+
const FILE_CAP = 6_000;
|
|
43
|
+
const CONFIG_CAP = 4_000;
|
|
44
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".venv", "venv", "target", "coverage", "__pycache__", ".turbo", ".cache", "out", "vendor"]);
|
|
45
|
+
const SOURCE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift|vue|svelte|sql|prisma)$/;
|
|
46
|
+
const TEST_HINT = /(\.test\.|\.spec\.|_test\.|\/tests?\/|\/__tests__\/|conftest\.py)/;
|
|
47
|
+
const CONFIG_GLOBS = [
|
|
48
|
+
"package.json", "tsconfig.json", "biome.json", "pyproject.toml", "ruff.toml", "go.mod",
|
|
49
|
+
".editorconfig", "Makefile", "Dockerfile", ".prettierrc", ".prettierrc.json", ".prettierrc.js",
|
|
50
|
+
".eslintrc", ".eslintrc.json", ".eslintrc.js", ".eslintrc.cjs", "eslint.config.js", "eslint.config.mjs", "eslint.config.ts",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function flagValue(args, name) {
|
|
54
|
+
const withEq = args.find((a) => a.startsWith(`--${name}=`));
|
|
55
|
+
if (withEq) return withEq.slice(name.length + 3);
|
|
56
|
+
const i = args.indexOf(`--${name}`);
|
|
57
|
+
return i !== -1 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function git(cwd, ...argv) {
|
|
61
|
+
try {
|
|
62
|
+
return execFileSync("git", argv, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
|
|
63
|
+
} catch {
|
|
64
|
+
return null; // not a git repo, or git missing — evidence is just thinner
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Directory tree, dirs only, two levels deep. */
|
|
69
|
+
function tree(cwd) {
|
|
70
|
+
const lines = [];
|
|
71
|
+
const walk = (dir, depth) => {
|
|
72
|
+
let entries;
|
|
73
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
74
|
+
for (const e of entries.filter((e) => e.isDirectory() && !SKIP_DIRS.has(e.name) && !e.name.startsWith(".")).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
75
|
+
lines.push(" ".repeat(depth) + e.name + "/");
|
|
76
|
+
if (depth < 1) walk(path.join(dir, e.name), depth + 1);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
walk(cwd, 0);
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Every source file under cwd (bounded walk), with size. */
|
|
84
|
+
function sourceFiles(cwd) {
|
|
85
|
+
const out = [];
|
|
86
|
+
const walk = (dir, depth) => {
|
|
87
|
+
if (depth > 6 || out.length > 5000) return;
|
|
88
|
+
let entries;
|
|
89
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
90
|
+
for (const e of entries) {
|
|
91
|
+
if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name) && !e.name.startsWith(".")) walk(path.join(dir, e.name), depth + 1); continue; }
|
|
92
|
+
if (!SOURCE_EXT.test(e.name)) continue;
|
|
93
|
+
const full = path.join(dir, e.name);
|
|
94
|
+
let size = 0;
|
|
95
|
+
try { size = fs.statSync(full).size; } catch { continue; }
|
|
96
|
+
out.push({ rel: path.relative(cwd, full).split(path.sep).join("/"), size });
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
walk(cwd, 0);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readCapped(cwd, rel, cap) {
|
|
104
|
+
try { return fs.readFileSync(path.join(cwd, rel), "utf-8").slice(0, cap); } catch { return null; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Collect the evidence sample. Returns { text, omitted } . */
|
|
108
|
+
function gather(cwd) {
|
|
109
|
+
const parts = [];
|
|
110
|
+
const omitted = [];
|
|
111
|
+
let used = 0;
|
|
112
|
+
const add = (label, body) => {
|
|
113
|
+
if (!body) return false;
|
|
114
|
+
const chunk = `<${label}>\n${body}\n</${label}>\n`;
|
|
115
|
+
if (used + chunk.length > BUDGET) { omitted.push(label); return false; }
|
|
116
|
+
parts.push(chunk); used += chunk.length; return true;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
add("tree", tree(cwd));
|
|
120
|
+
|
|
121
|
+
for (const name of CONFIG_GLOBS) {
|
|
122
|
+
const body = readCapped(cwd, name, CONFIG_CAP);
|
|
123
|
+
if (!body) continue;
|
|
124
|
+
if (name === "package.json") {
|
|
125
|
+
// Scripts and dependency names carry the conventions; the rest is noise.
|
|
126
|
+
try { const p = JSON.parse(fs.readFileSync(path.join(cwd, name), "utf-8")); add(`config file="${name}"`, JSON.stringify({ scripts: p.scripts, dependencies: Object.keys(p.dependencies || {}), devDependencies: Object.keys(p.devDependencies || {}) }, null, 1)); continue; } catch {}
|
|
127
|
+
}
|
|
128
|
+
add(`config file="${name}"`, body);
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const wf = fs.readdirSync(path.join(cwd, ".github", "workflows")).filter((f) => /\.ya?ml$/.test(f));
|
|
132
|
+
if (wf.length) add("workflows", wf.join("\n"));
|
|
133
|
+
} catch {}
|
|
134
|
+
|
|
135
|
+
const subjects = git(cwd, "log", "--format=%s", "-n", "50");
|
|
136
|
+
if (subjects) add("recent-commits", subjects.trim());
|
|
137
|
+
|
|
138
|
+
const all = sourceFiles(cwd);
|
|
139
|
+
const tests = all.filter((f) => TEST_HINT.test("/" + f.rel));
|
|
140
|
+
const src = all.filter((f) => !TEST_HINT.test("/" + f.rel));
|
|
141
|
+
const largest = src.slice().sort((a, b) => b.size - a.size).slice(0, 6).map((f) => f.rel);
|
|
142
|
+
const recentRaw = git(cwd, "log", "--name-only", "--format=", "-n", "30") || "";
|
|
143
|
+
const srcSet = new Set(src.map((f) => f.rel));
|
|
144
|
+
const recent = [...new Set(recentRaw.split("\n").map((s) => s.trim()).filter((s) => srcSet.has(s)))].slice(0, 6);
|
|
145
|
+
const picked = [...new Set([...largest, ...recent])].slice(0, 12);
|
|
146
|
+
for (const rel of picked) add(`file path="${rel}"`, readCapped(cwd, rel, FILE_CAP));
|
|
147
|
+
for (const f of tests.slice().sort((a, b) => b.size - a.size).slice(0, 4)) add(`test path="${f.rel}"`, readCapped(cwd, f.rel, FILE_CAP));
|
|
148
|
+
|
|
149
|
+
return { text: parts.join("\n"), omitted, files: picked.length + Math.min(4, tests.length), chars: used };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function repoName(cwd) {
|
|
153
|
+
const remote = git(cwd, "remote", "get-url", "origin");
|
|
154
|
+
const m = remote && /([^/:]+?)(?:\.git)?\s*$/.exec(remote);
|
|
155
|
+
return (m && m[1]) || path.basename(cwd);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function extract(args) {
|
|
159
|
+
args = args || [];
|
|
160
|
+
const cwd = process.cwd();
|
|
161
|
+
const dry = args.includes("--dry");
|
|
162
|
+
const force = args.includes("--force");
|
|
163
|
+
const name = (flagValue(args, "name") || "conventions").toLowerCase().replace(/[^a-z0-9.-]+/g, "-").replace(/^-+|-+$/g, "") || "conventions";
|
|
164
|
+
const id = `local/Team/${name}`;
|
|
165
|
+
const filename = presetFilename("local", "Team", name);
|
|
166
|
+
|
|
167
|
+
const manifest = readManifest(cwd);
|
|
168
|
+
if (!dry && !force && findPreset(manifest, id)) {
|
|
169
|
+
console.error(pc.yellow(`\n${id} already exists (${PRESETS_DIR}/${filename}).`));
|
|
170
|
+
console.error(pc.dim(` Re-extract over it with '${A} extract --force', or preview with '${A} extract --dry'.\n`));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const evidence = gather(cwd);
|
|
175
|
+
if (evidence.files === 0) {
|
|
176
|
+
console.error(pc.red("\nNo source files found here — nothing to extract conventions from.\n"));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
console.log(pc.bold(`\nGathered evidence from ${pc.cyan(evidence.files)} file${evidence.files === 1 ? "" : "s"}`) + pc.dim(` (${Math.round(evidence.chars / 1000)}k chars, ${MODEL})`));
|
|
180
|
+
if (evidence.omitted.length) console.log(pc.yellow(` Left out to fit the budget: ${evidence.omitted.length} item${evidence.omitted.length === 1 ? "" : "s"}`));
|
|
181
|
+
|
|
182
|
+
let Anthropic;
|
|
183
|
+
try { Anthropic = require("@anthropic-ai/sdk"); } catch {
|
|
184
|
+
console.error(pc.red("@anthropic-ai/sdk is not installed — reinstall activate-agentmd."));
|
|
185
|
+
process.exit(2);
|
|
186
|
+
}
|
|
187
|
+
const client = new Anthropic();
|
|
188
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
189
|
+
|
|
190
|
+
const system = [
|
|
191
|
+
"You derive the unwritten, project-specific conventions a codebase actually follows, from a sample of its files, configs and commit history.",
|
|
192
|
+
"Cover, where the evidence supports it: export style, naming, folder structure, error handling, validation at boundaries, data access patterns, testing conventions, commit conventions, tooling.",
|
|
193
|
+
"Only state conventions with evidence in the sample. Each rule carries a one-line `evidence:` citation naming the file or config it comes from. Do not restate generic best practice. Do not invent.",
|
|
194
|
+
"Output one complete standard in this exact format and nothing else — no preamble, no fences around the whole document:",
|
|
195
|
+
`---\nname: ${name}\ncategory: Team\ndescription: <one sentence>\nlicense: MIT\nauthor: ${repoName(cwd)} (extracted by agentmd)\nlast-verified: ${today}\nreviewed-by: unreviewed\nversion: 1.0.0\n---`,
|
|
196
|
+
"Then `# Purpose` (what this codebase is and what these rules protect), then one `# <Topic>` section per convention group with short right/wrong code blocks written in the project's own style, then `# Anti-patterns` (a table: pattern | why it is wrong here | do this instead), then `# Checklist` (one `- [ ]` line per rule).",
|
|
197
|
+
].join("\n");
|
|
198
|
+
|
|
199
|
+
let response;
|
|
200
|
+
try {
|
|
201
|
+
response = await client.beta.messages.create({
|
|
202
|
+
model: MODEL,
|
|
203
|
+
max_tokens: 16000,
|
|
204
|
+
betas: ["server-side-fallback-2026-07-01"],
|
|
205
|
+
fallbacks: "default",
|
|
206
|
+
system: [{ type: "text", text: system, cache_control: { type: "ephemeral" } }],
|
|
207
|
+
messages: [{ role: "user", content: evidence.text }],
|
|
208
|
+
});
|
|
209
|
+
} catch (e) {
|
|
210
|
+
if (e instanceof Anthropic.AuthenticationError || /authentication method|api ?key/i.test(e.message)) {
|
|
211
|
+
console.error(pc.red("No API credentials. Set ANTHROPIC_API_KEY (or run `ant auth login`) and try again."));
|
|
212
|
+
} else if (e instanceof Anthropic.RateLimitError) console.error(pc.red("Rate limited — retry in a moment."));
|
|
213
|
+
else console.error(pc.red(`Extract failed: ${e.message}`));
|
|
214
|
+
process.exit(2);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (response.stop_reason === "refusal") {
|
|
218
|
+
console.error(pc.red("The model declined to process this repository" + (response.stop_details?.explanation ? `: ${response.stop_details.explanation}` : ".")));
|
|
219
|
+
process.exit(2);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let content = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
|
|
223
|
+
// Tolerate a fenced reply; refuse anything that is not a standard.
|
|
224
|
+
content = content.replace(/^```(?:markdown|md)?\s*\n/, "").replace(/\n```\s*$/, "").trim() + "\n";
|
|
225
|
+
if (!content.startsWith("---") || !/^# Purpose/m.test(content)) {
|
|
226
|
+
console.error(pc.red("\nThe reply was not a standard in the expected format; nothing written. Raw output:\n"));
|
|
227
|
+
console.log(content);
|
|
228
|
+
process.exit(2);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const sections = (content.match(/^# (?!Purpose|Anti-patterns|Checklist)/gm) || []).length;
|
|
232
|
+
|
|
233
|
+
if (dry) {
|
|
234
|
+
console.log("");
|
|
235
|
+
console.log(content);
|
|
236
|
+
console.log(pc.dim(`--dry: ${sections} convention group${sections === 1 ? "" : "s"} found; nothing written. Run '${A} extract' to install it.\n`));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
ensurePresetsDir(cwd);
|
|
241
|
+
fs.writeFileSync(resolvePresetPath(cwd, filename), content, "utf-8");
|
|
242
|
+
upsertPreset(manifest, {
|
|
243
|
+
id, model: "local", category: "Team", preset: name, version: "1.0.0",
|
|
244
|
+
checksum: checksum(content), url: "local", file: `${PRESETS_DIR}/${filename}`,
|
|
245
|
+
});
|
|
246
|
+
writeManifest(cwd, manifest);
|
|
247
|
+
|
|
248
|
+
console.log("");
|
|
249
|
+
console.log(pc.green(`✓ ${sections} convention group${sections === 1 ? "" : "s"} extracted`) + pc.dim(` → ${PRESETS_DIR}/${filename}`));
|
|
250
|
+
console.log(pc.dim(` Recorded as ${id}. Review it like any other standard — it is your team's, edit freely.`));
|
|
251
|
+
console.log(pc.dim(` Run '${A} link' to ship it to your agent.\n`));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = { extract };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* info.js
|
|
3
|
+
*
|
|
4
|
+
* Handles: agentmd info <model/category/preset>
|
|
5
|
+
*
|
|
6
|
+
* The package detail view: what it is, how big it is, whether this project
|
|
7
|
+
* has it, and the command to install it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
const pc = require("picocolors");
|
|
13
|
+
const { getPreset, listModels } = require("../lib/registry");
|
|
14
|
+
const { parseCoordinates, suggest } = require("../lib/coordinates");
|
|
15
|
+
const { readManifest, findPreset } = require("../lib/manifest");
|
|
16
|
+
const { TARGETS } = require("../lib/agents");
|
|
17
|
+
const { cmd } = require("../lib/invocation");
|
|
18
|
+
|
|
19
|
+
/** How the user invoked us — "npx activate-agentmd" or "agentmd". */
|
|
20
|
+
const A = cmd();
|
|
21
|
+
|
|
22
|
+
function humanBytes(n) {
|
|
23
|
+
return n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function row(label, value) {
|
|
27
|
+
console.log(` ${pc.dim(label.padEnd(14))} ${value}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function info(args) {
|
|
31
|
+
const positional = (args || []).filter((a) => !a.startsWith("--"));
|
|
32
|
+
if (positional.length === 0) {
|
|
33
|
+
console.error(pc.red(`Usage: ${A} info <model/category/preset>`));
|
|
34
|
+
console.error(pc.dim(` Example: ${A} info claude/Security/owasp`));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const coords = parseCoordinates(positional[0]);
|
|
39
|
+
if (!coords.model || !listModels().includes(coords.model)) {
|
|
40
|
+
console.error(pc.red(`Unknown model: "${coords.raw.split("/")[0] || coords.raw}"`));
|
|
41
|
+
console.error(pc.dim(` Available: ${listModels().join(", ")}`));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (!coords.category || !coords.preset) {
|
|
45
|
+
console.error(pc.red("info needs a full package path: <model>/<category>/<preset>"));
|
|
46
|
+
console.error(pc.dim(` Example: ${A} info ${coords.model}/Security/owasp`));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const entry = getPreset(coords.model, coords.category, coords.preset);
|
|
51
|
+
if (!entry) {
|
|
52
|
+
console.error(pc.red(`Package not found: ${coords.model}/${coords.category}/${coords.preset}`));
|
|
53
|
+
const near = suggest(coords.model, coords.preset);
|
|
54
|
+
if (near.length) {
|
|
55
|
+
console.error(pc.dim("\n Did you mean:"));
|
|
56
|
+
for (const n of near) console.error(pc.dim(` ${A} info ${n.id}`));
|
|
57
|
+
}
|
|
58
|
+
console.error(pc.dim(`\n Search: ${A} search ${coords.preset}`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const installed = findPreset(readManifest(process.cwd()), entry.id);
|
|
63
|
+
|
|
64
|
+
console.log("");
|
|
65
|
+
console.log(pc.bold(pc.cyan(entry.id)) + pc.dim(` v${entry.version}`));
|
|
66
|
+
console.log("");
|
|
67
|
+
if (entry.description) {
|
|
68
|
+
console.log(` ${entry.description}`);
|
|
69
|
+
console.log("");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
row("Model", entry.model);
|
|
73
|
+
row("Category", entry.category);
|
|
74
|
+
row("Size", humanBytes(entry.bytes));
|
|
75
|
+
row("File", entry.file);
|
|
76
|
+
row("License", entry.license || "MIT");
|
|
77
|
+
|
|
78
|
+
// Provenance is printed even when empty. "not reviewed" is information; an
|
|
79
|
+
// omitted row would read as though the question had never been asked.
|
|
80
|
+
row("Author", entry.author || pc.yellow("unattributed"));
|
|
81
|
+
row(
|
|
82
|
+
"Reviewed",
|
|
83
|
+
entry.reviewedBy
|
|
84
|
+
? pc.green(entry.reviewedBy) + (entry.lastVerified ? pc.dim(` (${entry.lastVerified})`) : "")
|
|
85
|
+
: pc.yellow("not reviewed") + pc.dim(" — read it before trusting it")
|
|
86
|
+
);
|
|
87
|
+
row("Source", pc.dim(entry.url));
|
|
88
|
+
row("Agents", Object.values(TARGETS).map((t) => t.label.split(" (")[0]).join(", "));
|
|
89
|
+
|
|
90
|
+
console.log("");
|
|
91
|
+
if (installed) {
|
|
92
|
+
row("Installed", pc.green("yes") + pc.dim(` ${installed.file}`));
|
|
93
|
+
row("", pc.dim(`v${installed.version || "?"} on ${new Date(installed.installedAt).toISOString().slice(0, 10)}`));
|
|
94
|
+
console.log("");
|
|
95
|
+
console.log(pc.dim(` Check for changes: ${A} outdated`));
|
|
96
|
+
console.log(pc.dim(` Remove: ${A} remove ` + entry.id));
|
|
97
|
+
} else {
|
|
98
|
+
row("Installed", pc.dim("no"));
|
|
99
|
+
console.log("");
|
|
100
|
+
console.log(pc.bold(` ${A} install ` + entry.id));
|
|
101
|
+
}
|
|
102
|
+
console.log("");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { info };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init.js
|
|
3
|
+
*
|
|
4
|
+
* Handles: agentmd init [--model=claude] [--yes] [--dry]
|
|
5
|
+
*
|
|
6
|
+
* Scans the project, reports what it found, and offers to install the
|
|
7
|
+
* packages that match. This is the intended first command — it exists so a
|
|
8
|
+
* developer never has to read the registry to get a useful first install.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const readline = require("readline");
|
|
15
|
+
const pc = require("picocolors");
|
|
16
|
+
const { detect, RECOMMENDATIONS } = require("../lib/detect");
|
|
17
|
+
const { detectVersions } = require("../lib/versions");
|
|
18
|
+
const { getPreset, listModels } = require("../lib/registry");
|
|
19
|
+
const { installEntries } = require("./install");
|
|
20
|
+
const { readManifest, isInstalled } = require("../lib/manifest");
|
|
21
|
+
const { cmd } = require("../lib/invocation");
|
|
22
|
+
|
|
23
|
+
/** How the user invoked us — "npx activate-agentmd" or "agentmd". */
|
|
24
|
+
const A = cmd();
|
|
25
|
+
|
|
26
|
+
const DEFAULT_MODEL = "claude";
|
|
27
|
+
|
|
28
|
+
/** Yes/no prompt. Non-interactive stdin (CI, pipes) answers "no". */
|
|
29
|
+
function confirm(question) {
|
|
30
|
+
if (!process.stdin.isTTY) return Promise.resolve(false);
|
|
31
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
rl.question(question, (answer) => {
|
|
34
|
+
rl.close();
|
|
35
|
+
resolve(!/^n/i.test(answer.trim()));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function flagValue(args, name) {
|
|
41
|
+
const hit = (args || []).find((a) => a.startsWith(`--${name}=`));
|
|
42
|
+
return hit ? hit.slice(name.length + 3) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function init(args) {
|
|
46
|
+
args = args || [];
|
|
47
|
+
const model = flagValue(args, "model") || DEFAULT_MODEL;
|
|
48
|
+
const assumeYes = args.includes("--yes") || args.includes("-y");
|
|
49
|
+
const dry = args.includes("--dry");
|
|
50
|
+
|
|
51
|
+
if (!listModels().includes(model)) {
|
|
52
|
+
console.error(pc.red(`Unknown model: "${model}"`));
|
|
53
|
+
console.error(pc.dim(` Available: ${listModels().join(", ")}`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cwd = process.cwd();
|
|
58
|
+
console.log(pc.bold("\nagentmd") + pc.dim(" scanning project…\n"));
|
|
59
|
+
|
|
60
|
+
const { signals, packages, workspaces } = detect(cwd);
|
|
61
|
+
|
|
62
|
+
if (signals.length === 0) {
|
|
63
|
+
console.log(pc.yellow(" No known stack detected in this directory."));
|
|
64
|
+
console.log(pc.dim(`\n ${A} looks for package.json, Dockerfile, tsconfig.json, prisma/,`));
|
|
65
|
+
console.log(pc.dim(" .github/workflows/ and similar markers."));
|
|
66
|
+
console.log(pc.dim(`\n Browse the registry instead: ${A} list ${model}`));
|
|
67
|
+
console.log("");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A monorepo gets a per-workspace breakdown. Merging four stacks into one
|
|
72
|
+
// flat list would read as though the repo were one application, which is
|
|
73
|
+
// exactly the thing that makes the recommendations untrustworthy.
|
|
74
|
+
const nested = workspaces.filter((w) => w.path !== ".");
|
|
75
|
+
if (nested.length > 0) {
|
|
76
|
+
console.log(
|
|
77
|
+
pc.dim(` ${workspaces.length} workspace${workspaces.length === 1 ? "" : "s"} detected\n`)
|
|
78
|
+
);
|
|
79
|
+
for (const workspace of workspaces) {
|
|
80
|
+
const name = workspace.path === "." ? "." + path.sep + " (root)" : workspace.path + path.sep;
|
|
81
|
+
console.log(` ${pc.cyan(name)}`);
|
|
82
|
+
for (const s of workspace.signals) {
|
|
83
|
+
const suffix = (RECOMMENDATIONS[s.signal] || []).length === 0
|
|
84
|
+
? pc.dim(" (no packages for this yet)")
|
|
85
|
+
: "";
|
|
86
|
+
console.log(` ${pc.green("✓")} ${pc.white(s.label.padEnd(22))} ${pc.dim(s.via)}${suffix}`);
|
|
87
|
+
}
|
|
88
|
+
console.log("");
|
|
89
|
+
}
|
|
90
|
+
console.log(pc.dim(" Recommendations below are the union across all workspaces.\n"));
|
|
91
|
+
} else {
|
|
92
|
+
for (const s of signals) {
|
|
93
|
+
const suffix = s.packages.length === 0 ? pc.dim(" (no packages for this yet)") : "";
|
|
94
|
+
console.log(` ${pc.green("✓")} ${pc.white(s.label.padEnd(24))} ${pc.dim(s.via)}${suffix}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Version pins: the majors this project runs, which `link` turns into
|
|
99
|
+
// explicit "v2 not v1" rules — the antidote to a model's training cutoff.
|
|
100
|
+
const pins = detectVersions(cwd);
|
|
101
|
+
if (pins.length) {
|
|
102
|
+
console.log("");
|
|
103
|
+
for (const p of pins) console.log(` ${pc.cyan("⇅")} ${pc.white(`${p.label} ${p.version}`.padEnd(24))} ${pc.dim(p.via)}${pc.dim(" (version pin)")}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Only recommend what actually resolves. A mapping typo becomes a missing
|
|
107
|
+
// recommendation here and a test failure in CI — never a broken install.
|
|
108
|
+
const manifest = readManifest(cwd);
|
|
109
|
+
const resolved = [];
|
|
110
|
+
const alreadyInstalled = [];
|
|
111
|
+
|
|
112
|
+
for (const id of packages) {
|
|
113
|
+
const [category, preset] = id.split("/");
|
|
114
|
+
const entry = getPreset(model, category, preset);
|
|
115
|
+
if (!entry) continue;
|
|
116
|
+
if (isInstalled(manifest, entry.model, entry.category, entry.preset)) alreadyInstalled.push(entry);
|
|
117
|
+
else resolved.push(entry);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (resolved.length === 0) {
|
|
121
|
+
console.log("");
|
|
122
|
+
console.log(
|
|
123
|
+
alreadyInstalled.length > 0
|
|
124
|
+
? pc.green(`✓ All ${alreadyInstalled.length} recommended packages are already installed.`)
|
|
125
|
+
: pc.yellow("No registry packages match this stack yet.")
|
|
126
|
+
);
|
|
127
|
+
console.log("");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const byCategory = new Map();
|
|
132
|
+
for (const entry of resolved) {
|
|
133
|
+
if (!byCategory.has(entry.category)) byCategory.set(entry.category, []);
|
|
134
|
+
byCategory.get(entry.category).push(entry.preset);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log("");
|
|
138
|
+
console.log(pc.bold(`Recommended packages for ${pc.cyan(model)}:`));
|
|
139
|
+
console.log("");
|
|
140
|
+
for (const [category, presets] of [...byCategory].sort()) {
|
|
141
|
+
console.log(` ${pc.white(category.padEnd(14))} ${pc.dim(presets.join(", "))}`);
|
|
142
|
+
}
|
|
143
|
+
console.log("");
|
|
144
|
+
console.log(pc.dim(` ${resolved.length} to install`) + (alreadyInstalled.length ? pc.dim(`, ${alreadyInstalled.length} already present`) : ""));
|
|
145
|
+
console.log("");
|
|
146
|
+
|
|
147
|
+
if (dry) {
|
|
148
|
+
console.log(pc.dim(" --dry: nothing installed. Commands that would run:\n"));
|
|
149
|
+
for (const entry of resolved) console.log(pc.dim(` ${A} install ${entry.id}`));
|
|
150
|
+
console.log("");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const go = assumeYes || (await confirm(pc.bold("Install these? ") + pc.dim("[Y/n] ")));
|
|
155
|
+
if (!go) {
|
|
156
|
+
console.log(pc.dim("\n Nothing installed. Install individually with:"));
|
|
157
|
+
console.log(pc.dim(` ${A} install ${resolved[0].id}`));
|
|
158
|
+
console.log("");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log("");
|
|
163
|
+
await installEntries(resolved, {});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { init };
|