@raymondchins/agentmap 0.6.0 → 0.7.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 +21 -0
- package/agentmap.mjs +182 -22
- package/package.json +2 -1
- package/skills/SKILL.md +71 -0
- package/skills/cursor-rule.mdc +27 -0
- package/skills/install.mjs +97 -0
package/README.md
CHANGED
|
@@ -185,6 +185,25 @@ internally on `tool_name`. For reference, or to wire it by hand:
|
|
|
185
185
|
That's the "forced to use it" in the tagline: the map stays current on its own, and the
|
|
186
186
|
agent is steered to it the moment it reaches for a dependency-shaped grep or Bash search.
|
|
187
187
|
|
|
188
|
+
### 3. Agent skills (Cursor, Claude Code, Codex)
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npx @raymondchins/agentmap --install-skill
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Copies a packaged **SKILL.md** (Claude Code / Codex / OpenCode) and a **Cursor rule**
|
|
195
|
+
(`.cursor/rules/agentmap.mdc`, `alwaysApply: true`) into the current repo or global
|
|
196
|
+
agent directories. Options:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
agentmap --install-skill --platform cursor # Cursor rule only
|
|
200
|
+
agentmap --install-skill --platform claude # .claude/skills/agentmap/SKILL.md
|
|
201
|
+
agentmap --install-skill --global --platform claude # ~/.claude/skills/...
|
|
202
|
+
agentmap --install-skill --dry-run # preview paths, no writes
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Pair with `--install-hooks` (Claude Code) or `--mcp` (Cursor MCP).
|
|
206
|
+
|
|
188
207
|
---
|
|
189
208
|
|
|
190
209
|
## Quickstart
|
|
@@ -481,6 +500,8 @@ $ node agentmap.mjs --print | jq '.hubs[0]'
|
|
|
481
500
|
| `--version` / `-v` | Print the version from `package.json` and exit 0. |
|
|
482
501
|
| `--json` | **Global modifier.** When present, every command prints exactly one JSON object to stdout (no prose). Shapes vary per command: `--json --hubs` → `{command,fileCount,sha,hubs:[string]}`, `--json --find X` → `{command,query,matches:[{file,name,kind}]}`, `--json --relates X` → `{command,file,pagerank,exports,imports,dependents,related}`, `--json --any X` → `{command,query,kind,…payload}`, etc. Bare `--json` (no query flag) → `{command:"build",fileCount,features,topHub}`. |
|
|
483
502
|
| `--install-hooks` | Copy `hooks/post-commit` into `.git/hooks/` (chmod 0755), ensure `.claude/agentmap.json` is in `.gitignore`, and auto-wire the Claude Code `PreToolUse(Grep)` nudge into `.claude/settings.json` (merge-safe + idempotent). Exit 0 on success, stderr + exit 1 on failure. |
|
|
503
|
+
| `--hook-status` | Report whether the post-commit hook, PreToolUse nudge, and `.gitignore` entry are installed (no writes). |
|
|
504
|
+
| `--install-skill` | Install packaged agent skill + Cursor rule (`--platform claude\|cursor\|agents\|all`, default `all`; `--project` default, or `--global`; `--dry-run` preview). |
|
|
484
505
|
| `--mcp` | Start agentmap as a **stdio MCP server** so non-Claude-Code agents (Cursor, Cline, any MCP client) can call every flag as a first-class tool. |
|
|
485
506
|
|
|
486
507
|
**Exit-code contract:** `0` = success / match / help / version; `1` = query returned zero results (`--any`, `--find`, `--relates`, `--feature` with no match); `2` = usage error (missing required arg, unknown flag). Any token starting with `-` that matches no known flag prints an error to stderr and exits 2.
|
package/agentmap.mjs
CHANGED
|
@@ -64,6 +64,10 @@ const sh = (c) => { try { return execSync(c, { stdio: ["ignore", "pipe", "ignore
|
|
|
64
64
|
// Live content search for the --any fallback. `git grep` over tracked +
|
|
65
65
|
// untracked files (skips gitignored paths like node_modules). Reads DISK, so
|
|
66
66
|
// never stale. -F = fixed-string so literals like "bg-[#faf8f2]" aren't regex.
|
|
67
|
+
// -i = case-insensitive BY DESIGN (discovery ergonomics, matches --find which
|
|
68
|
+
// lowercases its query): a "content" hit may differ in case from the query as
|
|
69
|
+
// typed, but every match is printed verbatim with file:line so the true casing
|
|
70
|
+
// is always visible — results are a superset, never a falsified exact-case hit.
|
|
67
71
|
// stderr ignored so "fatal: not a git repository" stays quiet in non-git repos.
|
|
68
72
|
// Exclude sensitive files from the --untracked sweep so a local .env / key /
|
|
69
73
|
// secrets file never gets scanned and surfaced (and via MCP fed to an LLM).
|
|
@@ -88,8 +92,11 @@ const dirtyCount = () =>
|
|
|
88
92
|
// listed individually (default "all" folds it to "?? newdir/" and the
|
|
89
93
|
// extension regex misses it → a STALE cache would be served).
|
|
90
94
|
sh("git status --porcelain --untracked-files=all").split("\n").filter(Boolean).filter((l) => {
|
|
95
|
+
const xy = l.slice(0, 2); // porcelain status code (XY)
|
|
91
96
|
let p = l.slice(3); // strip "XY " status prefix
|
|
92
|
-
|
|
97
|
+
// only rename/copy entries use the ` old -> new ` form — gating on the status
|
|
98
|
+
// code avoids falsely splitting a plain file whose NAME contains " -> ".
|
|
99
|
+
if (/[RC]/.test(xy) && p.includes(" -> ")) p = p.split(" -> ").pop(); // rename/copy: keep new path
|
|
93
100
|
p = p.replace(/^"|"$/g, ""); // unquote space/special paths
|
|
94
101
|
return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue)$/.test(p);
|
|
95
102
|
}).length;
|
|
@@ -107,8 +114,13 @@ const SRC_EXT = /\.(ts|tsx|mts|cts|jsx|js|mjs|cjs|vue)$/;
|
|
|
107
114
|
function sourceFingerprint() {
|
|
108
115
|
try {
|
|
109
116
|
const entries = [];
|
|
110
|
-
const walk = (dir) => {
|
|
111
|
-
|
|
117
|
+
const walk = (dir, depth) => {
|
|
118
|
+
if (depth > 40) return; // depth cap — don't fully walk a pathologically deep tree
|
|
119
|
+
// per-directory try/catch: a single permission-denied subdir must NOT abort
|
|
120
|
+
// the WHOLE walk (that would return "" and silently disable caching) — skip
|
|
121
|
+
// the unreadable dir and keep going so the fingerprint stays usable.
|
|
122
|
+
let names; try { names = readdirSync(dir); } catch { return; }
|
|
123
|
+
for (const name of names) {
|
|
112
124
|
if (name === "node_modules" || name === ".git" || name === ".next") continue;
|
|
113
125
|
const full = dir + "/" + name;
|
|
114
126
|
let st;
|
|
@@ -118,11 +130,11 @@ function sourceFingerprint() {
|
|
|
118
130
|
// recursion / stack overflow.
|
|
119
131
|
try { st = lstatSync(full); } catch { continue; }
|
|
120
132
|
if (st.isSymbolicLink()) continue;
|
|
121
|
-
if (st.isDirectory()) walk(full);
|
|
133
|
+
if (st.isDirectory()) walk(full, depth + 1);
|
|
122
134
|
else if (SRC_EXT.test(name)) entries.push(`${full}:${st.mtimeMs}:${st.size}`);
|
|
123
135
|
}
|
|
124
136
|
};
|
|
125
|
-
walk(".");
|
|
137
|
+
walk(".", 0);
|
|
126
138
|
entries.sort();
|
|
127
139
|
return createHash("sha1").update(entries.join("\n")).digest("hex");
|
|
128
140
|
} catch { return ""; }
|
|
@@ -347,19 +359,21 @@ function makeProject() {
|
|
|
347
359
|
]);
|
|
348
360
|
// Non-git `.vue` fallback: walk the tree like sourceFingerprint() does.
|
|
349
361
|
try {
|
|
350
|
-
const walk = (dir) => {
|
|
351
|
-
|
|
362
|
+
const walk = (dir, depth) => {
|
|
363
|
+
if (depth > 40) return; // depth cap, matching sourceFingerprint()
|
|
364
|
+
let names; try { names = readdirSync(dir); } catch { return; } // skip unreadable dir, don't abort the whole walk
|
|
365
|
+
for (const name of names) {
|
|
352
366
|
if (name === "node_modules" || name === ".git" || name === ".next") continue;
|
|
353
367
|
const full = dir + "/" + name;
|
|
354
368
|
// lstatSync (NOT statSync) + skip symlinks, matching sourceFingerprint():
|
|
355
369
|
// a circular symlink would otherwise recurse until the stack overflows.
|
|
356
370
|
let st; try { st = lstatSync(full); } catch { continue; }
|
|
357
371
|
if (st.isSymbolicLink()) continue;
|
|
358
|
-
if (st.isDirectory()) walk(full);
|
|
372
|
+
if (st.isDirectory()) walk(full, depth + 1);
|
|
359
373
|
else if (name.endsWith(".vue")) vueFiles.push(full.replace(/^\.\//, ""));
|
|
360
374
|
}
|
|
361
375
|
};
|
|
362
|
-
walk(".");
|
|
376
|
+
walk(".", 0);
|
|
363
377
|
} catch { /* ignore — proceed without Vue */ }
|
|
364
378
|
}
|
|
365
379
|
// Build the virtual→real map and register each `<script>` block as a virtual
|
|
@@ -376,7 +390,7 @@ function makeProject() {
|
|
|
376
390
|
vueMap[`${cwdp}/${vpath}`] = `${cwdp}/${f}`;
|
|
377
391
|
vueReal[`${cwdp}/${f}`] = true;
|
|
378
392
|
}
|
|
379
|
-
return { project, vueMap, vueReal };
|
|
393
|
+
return { project, vueMap, vueReal, aliasOpts };
|
|
380
394
|
}
|
|
381
395
|
|
|
382
396
|
// ---------------------------------------------------------------------------
|
|
@@ -386,7 +400,7 @@ function makeProject() {
|
|
|
386
400
|
// ---------------------------------------------------------------------------
|
|
387
401
|
function build() {
|
|
388
402
|
const t0 = Date.now();
|
|
389
|
-
const { project, vueMap, vueReal } = makeProject();
|
|
403
|
+
const { project, vueMap, vueReal, aliasOpts } = makeProject();
|
|
390
404
|
const { SyntaxKind } = tsMorph();
|
|
391
405
|
const CallExpression = SyntaxKind.CallExpression;
|
|
392
406
|
const cwd = process.cwd().replace(/\\/g, "/");
|
|
@@ -408,8 +422,51 @@ function build() {
|
|
|
408
422
|
// /index.*. Returns the rel key or null. Powers side-effect (6b) + dynamic
|
|
409
423
|
// import()/require() (6c) edges that ts-morph's specifier resolution skips.
|
|
410
424
|
const RES_EXT = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
|
|
425
|
+
// posix-join that collapses "" / "." / ".." segments (no fs access).
|
|
426
|
+
const joinPosix = (a, b) => {
|
|
427
|
+
const parts = (a + "/" + b).split("/"); const st = [];
|
|
428
|
+
for (const seg of parts) { if (seg === "" || seg === ".") continue; if (seg === "..") st.pop(); else st.push(seg); }
|
|
429
|
+
return (a.startsWith("/") ? "/" : "") + st.join("/");
|
|
430
|
+
};
|
|
431
|
+
// resolve an absolute-ish path to an in-project source file, honoring
|
|
432
|
+
// extensionless + /index.* + .vue resolution (shared by relative + alias paths).
|
|
433
|
+
const tryResolveAt = (abs) => {
|
|
434
|
+
if (vueReal[abs]) return rel(abs);
|
|
435
|
+
let sf = project.getSourceFile(abs);
|
|
436
|
+
if (!sf) for (const e of RES_EXT) { sf = project.getSourceFile(`${abs}.${e}`); if (sf) break; }
|
|
437
|
+
if (!sf) for (const e of RES_EXT) { sf = project.getSourceFile(`${abs}/index.${e}`); if (sf) break; }
|
|
438
|
+
if (sf) return rel(sf.getFilePath());
|
|
439
|
+
if (vueReal[`${abs}.vue`]) return rel(`${abs}.vue`);
|
|
440
|
+
return null;
|
|
441
|
+
};
|
|
442
|
+
// #3 fix: tsconfig/jsconfig baseUrl+paths alias resolution ("@/x", "~/x") for
|
|
443
|
+
// the side-effect/dynamic/require edges too. Static imports already resolve via
|
|
444
|
+
// ts-morph's getModuleSpecifierSourceFile(); without this, `import "@/lib/x"`,
|
|
445
|
+
// `import("@/lib/x")` and `require("@/lib/x")` silently formed NO edge.
|
|
446
|
+
const ROOTABS = process.cwd().replace(/\\/g, "/");
|
|
447
|
+
const aliasBase = aliasOpts.baseUrl ? joinPosix(ROOTABS, aliasOpts.baseUrl) : ROOTABS;
|
|
448
|
+
const aliasEntries = Object.entries(aliasOpts.paths || {});
|
|
449
|
+
const resolveAlias = (spec) => {
|
|
450
|
+
for (const [pat, targets] of aliasEntries) {
|
|
451
|
+
const star = pat.indexOf("*");
|
|
452
|
+
let sub = null;
|
|
453
|
+
if (star === -1) { if (spec === pat) sub = ""; else continue; }
|
|
454
|
+
else {
|
|
455
|
+
const pre = pat.slice(0, star), suf = pat.slice(star + 1);
|
|
456
|
+
if (spec.length < pre.length + suf.length || !spec.startsWith(pre) || !spec.endsWith(suf)) continue;
|
|
457
|
+
sub = spec.slice(pre.length, spec.length - suf.length);
|
|
458
|
+
}
|
|
459
|
+
for (const tRaw of (Array.isArray(targets) ? targets : [targets])) {
|
|
460
|
+
const tStar = tRaw.indexOf("*");
|
|
461
|
+
const candidate = tStar === -1 ? tRaw : tRaw.slice(0, tStar) + sub + tRaw.slice(tStar + 1);
|
|
462
|
+
const hit = tryResolveAt(joinPosix(aliasBase, candidate));
|
|
463
|
+
if (hit) return hit;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
};
|
|
411
468
|
const resolveSpec = (fromAbsDir, spec) => {
|
|
412
|
-
if (!spec.startsWith(".")) return
|
|
469
|
+
if (!spec.startsWith(".")) return resolveAlias(spec); // alias (baseUrl/paths) or null for non-relative
|
|
413
470
|
// normalize fromAbsDir + spec into an absolute-ish posix path
|
|
414
471
|
const join = (a, b) => {
|
|
415
472
|
const parts = (a + "/" + b).split("/"); const st = [];
|
|
@@ -438,7 +495,9 @@ function build() {
|
|
|
438
495
|
for (const sf of sourceFiles) {
|
|
439
496
|
const path = rel(sf.getFilePath());
|
|
440
497
|
if (excluded(path)) continue;
|
|
498
|
+
try {
|
|
441
499
|
const fromDir = sf.getDirectoryPath().replace(/\\/g, "/");
|
|
500
|
+
const reExports = new Set(); // #2: names that are pass-through re-exports, not real uses
|
|
442
501
|
// exports, remembering which exported name was the file's DEFAULT export so
|
|
443
502
|
// default-import edges can later resolve "default" → the real symbol name.
|
|
444
503
|
let defaultExportName = null;
|
|
@@ -475,7 +534,11 @@ function build() {
|
|
|
475
534
|
for (const exp of sf.getExportDeclarations()) {
|
|
476
535
|
if (exp.isTypeOnly()) continue; // type-only re-exports excluded from edges
|
|
477
536
|
const t = exp.getModuleSpecifierSourceFile();
|
|
478
|
-
if (t)
|
|
537
|
+
if (t) {
|
|
538
|
+
const names = exp.getNamedExports().filter((n) => !n.isTypeOnly()).map((n) => n.getName());
|
|
539
|
+
addEdge(rel(t.getFilePath()), names); // keep the FILE-level edge (barrel depends on origin)
|
|
540
|
+
for (const n of names) reExports.add(n); // #2: mark as re-export so rankSymbols won't count it as a reference
|
|
541
|
+
}
|
|
479
542
|
}
|
|
480
543
|
// 6c: dynamic import("./x") and require("./x") with relative, in-project
|
|
481
544
|
// string-literal specifiers → edge with names ["*"]. Prefilter on raw text
|
|
@@ -494,9 +557,15 @@ function build() {
|
|
|
494
557
|
}
|
|
495
558
|
const imports = Object.keys(importedSymbols);
|
|
496
559
|
for (const tp of imports) (dependents[tp] ??= []).push(path);
|
|
497
|
-
files[path] = { exports, imports, importedSymbols, defaultExportName };
|
|
560
|
+
files[path] = { exports, imports, importedSymbols, defaultExportName, reExports: [...reExports] };
|
|
498
561
|
const feat = featureOf(path);
|
|
499
562
|
if (feat) (features[feat] ??= []).push(path);
|
|
563
|
+
} catch (e) {
|
|
564
|
+
// #1 fix: a single pathological file (malformed import specifier, ts-morph
|
|
565
|
+
// edge case) must NOT abort the whole map — skip it + warn, preserving the
|
|
566
|
+
// graceful-degradation contract agentmap advertises.
|
|
567
|
+
process.stderr.write(`# agentmap: skipped ${path} (parse error: ${e?.message ?? e})\n`);
|
|
568
|
+
}
|
|
500
569
|
}
|
|
501
570
|
// 7: resolve default-import edges. A default import was recorded literally as
|
|
502
571
|
// "default"; rankSymbols skips "default", so default-exported symbols (the
|
|
@@ -564,10 +633,12 @@ function rankSymbols(files, focus) {
|
|
|
564
633
|
definition.set(`${p}|${e.name}`, { file: p, name: e.name, kind: e.kind });
|
|
565
634
|
}
|
|
566
635
|
}
|
|
567
|
-
for (const [p, f] of Object.entries(files))
|
|
636
|
+
for (const [p, f] of Object.entries(files)) {
|
|
637
|
+
const reExp = new Set(f.reExports || []); // #2: pass-through re-exports aren't real references
|
|
568
638
|
for (const tp of f.imports)
|
|
569
639
|
for (const name of f.importedSymbols[tp] || [])
|
|
570
|
-
if (name !== "*" && name !== "default") getOrSet(references, name, () => []).push(p);
|
|
640
|
+
if (name !== "*" && name !== "default" && !reExp.has(name)) getOrSet(references, name, () => []).push(p);
|
|
641
|
+
}
|
|
571
642
|
|
|
572
643
|
// mentioned idents from focus files' exports + their basenames
|
|
573
644
|
let mentioned = null;
|
|
@@ -846,6 +917,60 @@ function installHooks({ dryRun = false } = {}) {
|
|
|
846
917
|
console.log("\nDone — the map auto-refreshes on commit, and greps are nudged to agentmap first.");
|
|
847
918
|
}
|
|
848
919
|
|
|
920
|
+
// Marker string baked into hooks/post-commit — used by --hook-status to detect our
|
|
921
|
+
// hook even when chained with other tools in the same file.
|
|
922
|
+
const POST_COMMIT_MARKER = "agentmap — git post-commit hook";
|
|
923
|
+
const NUDGE_REL = ".claude/hooks/agentmap-nudge.mjs";
|
|
924
|
+
const MAP_IGNORE_LINE = ".claude/agentmap/";
|
|
925
|
+
|
|
926
|
+
function hookStatus() {
|
|
927
|
+
const gitDir = sh("git rev-parse --git-dir");
|
|
928
|
+
if (!gitDir) {
|
|
929
|
+
console.log("not a git repository — run inside the repo you want to check");
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const hooksDir = `${gitDir}/hooks`;
|
|
933
|
+
const postCommitPath = `${hooksDir}/post-commit`;
|
|
934
|
+
|
|
935
|
+
let postCommit = "not installed";
|
|
936
|
+
if (existsSync(postCommitPath)) {
|
|
937
|
+
const body = readFileSync(postCommitPath, "utf8");
|
|
938
|
+
postCommit = body.includes(POST_COMMIT_MARKER) ? "installed" : "not installed (hook exists but agentmap not found)";
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
let nudge = existsSync(NUDGE_REL) ? "installed" : "not installed";
|
|
942
|
+
|
|
943
|
+
let grepWire = "not wired";
|
|
944
|
+
let bashWire = "not wired";
|
|
945
|
+
const settingsPath = ".claude/settings.json";
|
|
946
|
+
if (existsSync(settingsPath)) {
|
|
947
|
+
try {
|
|
948
|
+
const settings = parseSettings(readFileSync(settingsPath, "utf8"), settingsPath);
|
|
949
|
+
const entries = settings.hooks?.PreToolUse || [];
|
|
950
|
+
const has = (matcher) => entries.some(
|
|
951
|
+
(e) => e?.matcher === matcher && Array.isArray(e?.hooks) && e.hooks.some((h) => typeof h?.command === "string" && h.command.includes("agentmap-nudge")),
|
|
952
|
+
);
|
|
953
|
+
grepWire = has("Grep") ? "wired" : "not wired";
|
|
954
|
+
bashWire = has("Bash") ? "wired" : "not wired";
|
|
955
|
+
} catch {
|
|
956
|
+
grepWire = "not wired (invalid settings.json)";
|
|
957
|
+
bashWire = "not wired (invalid settings.json)";
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
let gitignore = "missing entry";
|
|
962
|
+
if (existsSync(".gitignore")) {
|
|
963
|
+
const ok = readFileSync(".gitignore", "utf8").split(/\r?\n/).some((l) => l.trim() === MAP_IGNORE_LINE);
|
|
964
|
+
gitignore = ok ? "ok" : "missing entry";
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
console.log(`post-commit: ${postCommit}`);
|
|
968
|
+
console.log(`nudge (${NUDGE_REL}): ${nudge}`);
|
|
969
|
+
console.log(`PreToolUse(Grep): ${grepWire}`);
|
|
970
|
+
console.log(`PreToolUse(Bash): ${bashWire}`);
|
|
971
|
+
console.log(`.gitignore (${MAP_IGNORE_LINE}): ${gitignore}`);
|
|
972
|
+
}
|
|
973
|
+
|
|
849
974
|
// ---------------------------------------------------------------------------
|
|
850
975
|
// --setup-mcp: register the agentmap MCP server in the global configs of
|
|
851
976
|
// MCP-capable IDEs that aren't Claude Code (which uses --install-hooks instead).
|
|
@@ -927,7 +1052,8 @@ const out = (obj, prose) => { if (wantJson) console.log(JSON.stringify(obj)); el
|
|
|
927
1052
|
// NOT in this set is an unknown flag → usage error (exit 2), not a silent build.
|
|
928
1053
|
const KNOWN = new Set([
|
|
929
1054
|
"--json", "--print",
|
|
930
|
-
"--help", "-h", "--version", "-v", "--install-hooks", "--
|
|
1055
|
+
"--help", "-h", "--version", "-v", "--install-hooks", "--hook-status", "--install-skill", "--platform", "--project", "--global",
|
|
1056
|
+
"--dry-run", "--setup-mcp", "--mcp",
|
|
931
1057
|
"--any", "--find", "--relates", "--map", "--focus", "--tokens",
|
|
932
1058
|
"--symbols", "--feature", "--features", "--hubs",
|
|
933
1059
|
]);
|
|
@@ -936,7 +1062,7 @@ const KNOWN = new Set([
|
|
|
936
1062
|
// so a dash-leading query like `--any "-O/bin/sh"` is bound as the query, not
|
|
937
1063
|
// mistaken for an unknown flag. (arg() already rejects a "--"-leading value, so
|
|
938
1064
|
// `--any --foo` still falls through to the missing-arg guard instead.)
|
|
939
|
-
const VALUE_FLAGS = new Set(["--any", "--find", "--relates", "--feature", "--focus", "--tokens", "--symbols"]);
|
|
1065
|
+
const VALUE_FLAGS = new Set(["--any", "--find", "--relates", "--feature", "--focus", "--tokens", "--symbols", "--platform"]);
|
|
940
1066
|
const valueIdx = new Set();
|
|
941
1067
|
for (let i = 0; i < args.length - 1; i++) if (VALUE_FLAGS.has(args[i])) valueIdx.add(i + 1);
|
|
942
1068
|
|
|
@@ -964,6 +1090,9 @@ Maintenance:
|
|
|
964
1090
|
--install-hooks [--dry-run]
|
|
965
1091
|
install git post-commit + copy the PreToolUse nudge +
|
|
966
1092
|
wire .claude/settings.json (--dry-run = preview, no writes)
|
|
1093
|
+
--install-skill [--platform claude|cursor|agents|all] [--project|--global] [--dry-run]
|
|
1094
|
+
install SKILL.md / Cursor rule for coding agents
|
|
1095
|
+
--hook-status report whether agentmap git/nudge wiring is installed
|
|
967
1096
|
--setup-mcp [--dry-run]
|
|
968
1097
|
configure MCP server for OpenCode & Antigravity IDE
|
|
969
1098
|
(--dry-run = preview, no writes)
|
|
@@ -1002,6 +1131,29 @@ else if (has("--install-hooks")) {
|
|
|
1002
1131
|
try { installHooks({ dryRun: has("--dry-run") }); process.exit(0); }
|
|
1003
1132
|
catch (e) { console.error(`agentmap --install-hooks failed: ${e?.message || e}`); process.exit(1); }
|
|
1004
1133
|
}
|
|
1134
|
+
// --install-skill: copy packaged SKILL.md / Cursor rule (see skills/install.mjs).
|
|
1135
|
+
else if (has("--install-skill")) {
|
|
1136
|
+
try {
|
|
1137
|
+
// lazy import keeps skills/install.mjs (and its package.json read) OFF the
|
|
1138
|
+
// hot path — warm --any/--find queries must not load it.
|
|
1139
|
+
const { installSkill } = await import("./skills/install.mjs");
|
|
1140
|
+
installSkill({
|
|
1141
|
+
platforms: arg("--platform") || "all",
|
|
1142
|
+
project: !has("--global"),
|
|
1143
|
+
global: has("--global"),
|
|
1144
|
+
dryRun: has("--dry-run"),
|
|
1145
|
+
});
|
|
1146
|
+
process.exit(0);
|
|
1147
|
+
} catch (e) {
|
|
1148
|
+
console.error(`agentmap --install-skill failed: ${e?.message || e}`);
|
|
1149
|
+
process.exit(1);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
// --hook-status: report post-commit / nudge / settings wiring (no writes).
|
|
1153
|
+
else if (has("--hook-status")) {
|
|
1154
|
+
try { hookStatus(); process.exit(0); }
|
|
1155
|
+
catch (e) { console.error(`agentmap --hook-status failed: ${e?.message || e}`); process.exit(1); }
|
|
1156
|
+
}
|
|
1005
1157
|
// --setup-mcp: configure MCP server for OpenCode & Antigravity IDE.
|
|
1006
1158
|
else if (has("--setup-mcp")) {
|
|
1007
1159
|
try { setupMcp({ dryRun: has("--dry-run") }); process.exit(0); }
|
|
@@ -1157,11 +1309,19 @@ else if (has("--any")) {
|
|
|
1157
1309
|
// omitted. Otherwise `continue` so smaller lower-ranked files can still
|
|
1158
1310
|
// fill the remaining budget (don't `break` on the first overflow).
|
|
1159
1311
|
if (first && budget > 0) {
|
|
1160
|
-
|
|
1161
|
-
while (partial.length > 1)
|
|
1162
|
-
|
|
1312
|
+
// #6 fix: try progressively fewer symbols DOWN TO ONE. The old
|
|
1313
|
+
// `while (partial.length > 1)` sliced before testing and never tried
|
|
1314
|
+
// the single-symbol block, so a tiny --tokens could emit NOTHING for
|
|
1315
|
+
// the top file despite the "never wholly omitted" intent. If even one
|
|
1316
|
+
// symbol overflows the budget, nothing fits (correct) — but we now try.
|
|
1317
|
+
for (let k = capped.length - 1; k >= 1; k--) {
|
|
1318
|
+
const partial = capped.slice(0, k);
|
|
1163
1319
|
const pt = tokEst(lineOf(partial));
|
|
1164
|
-
if (used + pt <= budget) {
|
|
1320
|
+
if (used + pt <= budget) {
|
|
1321
|
+
used += pt;
|
|
1322
|
+
shownFiles.push({ file, symbols: partial.map((s) => ({ name: s.name, kind: s.kind })) });
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1165
1325
|
}
|
|
1166
1326
|
first = false;
|
|
1167
1327
|
}
|
package/package.json
CHANGED
package/skills/SKILL.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentmap
|
|
3
|
+
description: >-
|
|
4
|
+
Use for TypeScript/JavaScript codebase navigation — symbol lookup, blast radius,
|
|
5
|
+
reuse checks, and repo orientation. Prefer agentmap before serial grep when
|
|
6
|
+
exploring imports, exports, or where a symbol lives. Package is
|
|
7
|
+
@raymondchins/agentmap (not the unrelated npm package agentmap).
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# agentmap
|
|
11
|
+
|
|
12
|
+
Queryable, ranked **import graph** for TS/JS repos (PageRank hubs, symbol ranking, token-budgeted digest). Faster and more accurate than grep for structural questions.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- **Where is X defined?** / **who imports this file?** / **what breaks if I edit this?**
|
|
17
|
+
- **Reuse check** before adding a util, component, or type
|
|
18
|
+
- **Session start** — orient to a large monorepo cheaply
|
|
19
|
+
|
|
20
|
+
## When not to use
|
|
21
|
+
|
|
22
|
+
- Raw string / config value search (try `agentmap --any` first — layer 4 is live `git grep`)
|
|
23
|
+
- Non-TS/JS files, runtime call graphs, or full semantic "how does it work?" (use codebase search)
|
|
24
|
+
- Next-style `--feature` on TanStack Router / non-`app/` layouts (often empty)
|
|
25
|
+
|
|
26
|
+
## Commands (run in repo root)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Smart router — default first move
|
|
30
|
+
agentmap --any <query>
|
|
31
|
+
|
|
32
|
+
# Reuse / symbol definition
|
|
33
|
+
agentmap --find <SymbolName>
|
|
34
|
+
|
|
35
|
+
# Blast radius (exports, imports, dependents, related files)
|
|
36
|
+
agentmap --relates <path/to/file.ts>
|
|
37
|
+
|
|
38
|
+
# Token-budgeted ranked digest
|
|
39
|
+
agentmap --map --tokens 400
|
|
40
|
+
agentmap --map --focus <path> --tokens 400
|
|
41
|
+
|
|
42
|
+
# Hub files (PageRank)
|
|
43
|
+
agentmap --hubs
|
|
44
|
+
|
|
45
|
+
# JSON for tools
|
|
46
|
+
agentmap --json --any <query>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Install: `npm i -g @raymondchins/agentmap` or `npx @raymondchins/agentmap`. Map cache: `.claude/agentmap/` (gitignored).
|
|
50
|
+
|
|
51
|
+
## Agent platforms
|
|
52
|
+
|
|
53
|
+
| Platform | Setup |
|
|
54
|
+
|----------|--------|
|
|
55
|
+
| **Claude Code** | `agentmap --install-hooks` (post-commit refresh + PreToolUse grep nudge) |
|
|
56
|
+
| **Cursor / MCP clients** | `agentmap --mcp` in MCP config, or Shell + commands above |
|
|
57
|
+
| **This skill** | `agentmap --install-skill` |
|
|
58
|
+
|
|
59
|
+
## Workflow
|
|
60
|
+
|
|
61
|
+
1. If the map may be stale after edits, run `agentmap` (no flags) or rely on post-commit hook.
|
|
62
|
+
2. Start with `agentmap --any <symbol or topic>`.
|
|
63
|
+
3. Before editing a hub file, run `agentmap --relates <that-file>`.
|
|
64
|
+
4. Fall back to grep only when agentmap returns no useful structure hit.
|
|
65
|
+
|
|
66
|
+
## Package name
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx @raymondchins/agentmap --any Procedure
|
|
70
|
+
# NOT: npm install agentmap (different package)
|
|
71
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: agentmap ranked TS/JS repo map
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
This project can use **agentmap** (`@raymondchins/agentmap`) — a queryable import/symbol map at `.claude/agentmap/`.
|
|
7
|
+
|
|
8
|
+
**Before Grep/Glob for structural questions** (where is a symbol, who imports a file, reuse check, blast radius), prefer:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
agentmap --any <query> # file → symbol → feature → git-grep fallback
|
|
12
|
+
agentmap --find <Symbol> # exported symbols matching name
|
|
13
|
+
agentmap --relates <file.ts> # imports, dependents, related files
|
|
14
|
+
agentmap --map --tokens 400 # cheap repo orientation
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Use Read/Grep directly when:
|
|
18
|
+
|
|
19
|
+
1. agentmap already oriented you and you need exact lines to edit
|
|
20
|
+
2. The map is missing — run `agentmap` once to build `.claude/agentmap/`
|
|
21
|
+
3. Searching raw strings, logs, or non-TS files
|
|
22
|
+
|
|
23
|
+
**Cursor MCP (optional):** `agentmap --mcp` exposes the same queries as tools.
|
|
24
|
+
|
|
25
|
+
**Note:** `--features` only detects Next.js `app/` routes; TanStack `src/routes/` repos often show `features (0)`.
|
|
26
|
+
|
|
27
|
+
Package: `@raymondchins/agentmap` — not the unrelated PyPI/npm `agentmap` package.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// --install-skill: copy packaged SKILL.md / Cursor rule into project or global
|
|
3
|
+
// agent directories (project or global scope).
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const SKILLS_DIR = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
/** @type {Record<string, { label: string; src: string; dest: (root: string) => string; projectOnly?: boolean }>} */
|
|
13
|
+
const PLATFORMS = {
|
|
14
|
+
claude: {
|
|
15
|
+
label: "Claude Code",
|
|
16
|
+
src: join(SKILLS_DIR, "SKILL.md"),
|
|
17
|
+
dest: (root) => join(root, ".claude", "skills", "agentmap", "SKILL.md"),
|
|
18
|
+
},
|
|
19
|
+
agents: {
|
|
20
|
+
label: "Codex / OpenCode (.agents)",
|
|
21
|
+
src: join(SKILLS_DIR, "SKILL.md"),
|
|
22
|
+
dest: (root) => join(root, ".agents", "skills", "agentmap", "SKILL.md"),
|
|
23
|
+
},
|
|
24
|
+
cursor: {
|
|
25
|
+
label: "Cursor",
|
|
26
|
+
src: join(SKILLS_DIR, "cursor-rule.mdc"),
|
|
27
|
+
dest: (root) => join(root, ".cursor", "rules", "agentmap.mdc"),
|
|
28
|
+
projectOnly: true,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function atomicWrite(dest, body) {
|
|
33
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
34
|
+
const tmp = `${dest}.tmp`;
|
|
35
|
+
writeFileSync(tmp, body, "utf8");
|
|
36
|
+
renameSync(tmp, dest);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parsePlatforms(raw) {
|
|
40
|
+
if (!raw || raw === "all") return ["claude", "cursor", "agents"];
|
|
41
|
+
const names = raw.split(/[,+]/).map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
42
|
+
for (const n of names) {
|
|
43
|
+
if (!PLATFORMS[n]) {
|
|
44
|
+
throw new Error(`unknown platform '${n}' — choose: ${Object.keys(PLATFORMS).join(", ")}, all`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return names;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function gitAddHint(paths) {
|
|
51
|
+
const unique = [...new Set(paths.map((p) => p.replace(/\/[^/]+$/, "") || p))];
|
|
52
|
+
if (unique.length) console.log(`\nOptional: git add ${unique.map((p) => `"${p}"`).join(" ")}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {{ platforms?: string; project?: boolean; global?: boolean; dryRun?: boolean }} opts
|
|
57
|
+
*/
|
|
58
|
+
export function installSkill({ platforms: platformsArg = "all", project = true, global: globalScope = false, dryRun = false } = {}) {
|
|
59
|
+
if (project && globalScope) throw new Error("use either --project or --global, not both");
|
|
60
|
+
// read version lazily (inside the function, not at module load) so importing
|
|
61
|
+
// this module is side-effect-free / off the CLI hot path.
|
|
62
|
+
const VERSION = JSON.parse(readFileSync(join(SKILLS_DIR, "..", "package.json"), "utf8")).version;
|
|
63
|
+
const scope = globalScope ? "global" : "project";
|
|
64
|
+
const root = globalScope ? homedir() : process.cwd();
|
|
65
|
+
const names = parsePlatforms(platformsArg);
|
|
66
|
+
const targets = [];
|
|
67
|
+
|
|
68
|
+
if (dryRun) console.log(`--dry-run: would install agentmap skill (${scope} scope):`);
|
|
69
|
+
|
|
70
|
+
for (const name of names) {
|
|
71
|
+
const cfg = PLATFORMS[name];
|
|
72
|
+
if (cfg.projectOnly && globalScope) {
|
|
73
|
+
console.log(` skip ${cfg.label}: Cursor rule is project-scoped only (re-run with --project)`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!existsSync(cfg.src)) throw new Error(`packaged skill missing: ${cfg.src}`);
|
|
77
|
+
const dest = cfg.dest(root);
|
|
78
|
+
const body = readFileSync(cfg.src, "utf8");
|
|
79
|
+
const versionFile = join(dirname(dest), ".agentmap_version");
|
|
80
|
+
|
|
81
|
+
if (dryRun) {
|
|
82
|
+
console.log(` ${cfg.label}: ${dest}`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
atomicWrite(dest, body);
|
|
87
|
+
writeFileSync(versionFile, VERSION + "\n", "utf8");
|
|
88
|
+
console.log(` ${cfg.label} → ${dest}`);
|
|
89
|
+
targets.push(dest);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!dryRun && targets.length) {
|
|
93
|
+
console.log(`\nagentmap --install-skill: installed ${targets.length} file(s) (${scope}).`);
|
|
94
|
+
if (!globalScope) gitAddHint(targets);
|
|
95
|
+
console.log("Pair with: agentmap --install-hooks (Claude Code) or agentmap --mcp (Cursor MCP).");
|
|
96
|
+
}
|
|
97
|
+
}
|