@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 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
- if (p.includes(" -> ")) p = p.split(" -> ").pop(); // rename: keep the new path
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
- for (const name of readdirSync(dir)) {
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
- for (const name of readdirSync(dir)) {
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 null; // only relative, in-project specifiers
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) addEdge(rel(t.getFilePath()), exp.getNamedExports().filter((n) => !n.isTypeOnly()).map((n) => n.getName()));
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", "--dry-run", "--setup-mcp", "--mcp",
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
- let partial = capped;
1161
- while (partial.length > 1) {
1162
- partial = partial.slice(0, partial.length - 1);
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) { used += pt; shownFiles.push({ file, symbols: partial.map((s) => ({ name: s.name, kind: s.kind })) }); break; }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raymondchins/agentmap",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -14,6 +14,7 @@
14
14
  "agentmap.mjs",
15
15
  "mcp.mjs",
16
16
  "hooks",
17
+ "skills",
17
18
  "NOTICE"
18
19
  ],
19
20
  "scripts": {
@@ -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
+ }