@raymondchins/agentmap 0.2.1 → 0.2.3

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
@@ -6,12 +6,13 @@
6
6
 
7
7
  **The repo map your coding agent is _forced_ to use — ~98% fewer tokens to understand your TS/JS codebase.**
8
8
 
9
- A queryable, ranked code-relationship map for TypeScript/JavaScript repos that answers
10
- "understand the codebase" questions in **~98% fewer tokens on average** (up to **99.9%
11
- per task**) vs reading raw files. Personalized PageRank importance, Aider-style symbol
12
- ranking, a token-budgeted digest, and a single `--any` router (file symbol feature →
13
- live git-grep) — wired straight into the agent loop so it actually gets used, not just
14
- published.
9
+ Your AI coding agent re-learns your codebase every session opening files and grepping to find
10
+ what connects to what, burning tokens before it writes a line. agentmap gives it a **queryable,
11
+ ranked code-relationship map for TypeScript/JavaScript repos** instead a `ts-morph` import/symbol
12
+ graph ranked by personalized PageRank. Ask it to *"add a field"* or *"fix the login bug"* and it
13
+ finds the right files, their imports, and what already exists in
14
+ **~98% fewer tokens on average** (up to **99.9% per task**) — kept current by a post-commit
15
+ auto-refresh and actually used via a `PreToolUse(Grep)` hook.
15
16
 
16
17
  [![npm](https://img.shields.io/npm/v/@raymondchins/agentmap)](https://www.npmjs.com/package/@raymondchins/agentmap)
17
18
  [![CI](https://github.com/raymondchins/agentmap/actions/workflows/ci.yml/badge.svg)](https://github.com/raymondchins/agentmap/actions/workflows/ci.yml)
@@ -23,13 +24,61 @@ published.
23
24
 
24
25
  ---
25
26
 
27
+ ## Benchmark
28
+
29
+ Every task you hand a coding agent starts with the same hidden step — *find the relevant code*.
30
+ Here's the token cost of that step, **reading raw files vs querying agentmap**, on a real 154-file
31
+ Next.js app ([vercel/ai-chatbot](https://github.com/vercel/ai-chatbot)). Every figure is captured
32
+ tool output (`node benchmark/bench.mjs <repo>` at the pinned sha):
33
+
34
+ <table width="100%">
35
+ <thead>
36
+ <tr>
37
+ <th align="left">The question the agent has to answer first</th>
38
+ <th align="right">Reading files</th>
39
+ <th align="right">With agentmap</th>
40
+ <th align="right">Saved</th>
41
+ </tr>
42
+ </thead>
43
+ <tbody>
44
+ <tr><td align="left">Where is this symbol defined?</td><td align="right">1,950</td><td align="right">20</td><td align="right">99%</td></tr>
45
+ <tr><td align="left">Does a helper for this already exist? <i>(reuse)</i></td><td align="right">14,740</td><td align="right">19</td><td align="right">99.9%</td></tr>
46
+ <tr><td align="left">What breaks if I change this file? <i>(blast radius)</i></td><td align="right">81,038</td><td align="right">616</td><td align="right">99.2%</td></tr>
47
+ <tr><td align="left">What files make up this feature?</td><td align="right">6,121</td><td align="right">1,025</td><td align="right">83.3%</td></tr>
48
+ <tr><td align="left">Give me a repo overview</td><td align="right">3,065</td><td align="right">1,127</td><td align="right">63.2%</td></tr>
49
+ <tr><td align="left">Load the whole repo into context</td><td align="right">150,281</td><td align="right">1,127</td><td align="right">99.3%</td></tr>
50
+ <tr><td align="left">What does this one file import?</td><td align="right">583</td><td align="right">517</td><td align="right">11.3%</td></tr>
51
+ <tr><td align="left"><b>All 7 tasks combined</b></td><td align="right"><b>257,778</b></td><td align="right"><b>4,451</b></td><td align="right"><b>98.3%</b></td></tr>
52
+ </tbody>
53
+ </table>
54
+
55
+ <sub>Context tokens the agent burns to answer each question — token est = chars/4, applied to both sides.</sub>
56
+
57
+ That's the agent reaching the same answer on **58× fewer tokens** overall — and the pattern holds
58
+ across [zod](https://github.com/colinhacks/zod) (367 files, **99.2%**) and
59
+ [taxonomy](https://github.com/shadcn-ui/taxonomy) (125 files, **96.0%**), peaking at **646× fewer**
60
+ on a single whole-repo map. Reproducible at pinned shas; full per-scenario tables in
61
+ **[`./benchmark/RESULTS.md`](./benchmark/RESULTS.md)**.
62
+
63
+ **Speed:** a cold build (parse + PageRank + symbol graph) takes **~1.2s**; a warm cached query
64
+ returns in **~0.1s** (the lazy-loaded path added in 0.2.2) — the agent has a ranked answer back
65
+ before it would have finished opening the first handful of files.
66
+
67
+ Honest notes: the win scales with the work — the small rows above (63%, 11%) are the floor, and a
68
+ *trivial single-file* lookup can even cost **more** than `cat`+`grep` (taxonomy's file-import task
69
+ hit −313%; we leave it in). Numbers measure **context-token volume**, not answer quality or wall-clock.
70
+
71
+ ---
72
+
26
73
  ## Why it's different
27
74
 
28
- Most "repo context" tools are one-shot: they pack the repository (or a slice of it) into a
29
- prompt and stop there. agentmap is a **queryable, ranked, and self-refreshing** map that an
30
- agent can interrogate flag-by-flag — and, crucially, is **wired into the agent loop** via a
31
- post-commit auto-refresh and a `PreToolUse` hook that nudges the agent to use the map
32
- *before* it falls back to serial grep.
75
+ Most "repo context" tools are a photocopy: they dump your repository (or a slice of it) into
76
+ the prompt once and walk away. The copy goes stale the moment you edit a file, and nothing
77
+ makes the agent actually read it.
78
+
79
+ agentmap is the opposite a **queryable, ranked, self-refreshing** map the agent interrogates
80
+ flag-by-flag, that **rebuilds itself on every commit**, and that a `PreToolUse` hook steers the
81
+ agent toward *before* it falls back to serial grep.
33
82
 
34
83
  | | **agentmap** | Aider repo map | RepoMapper | Repomix | code2prompt |
35
84
  | --- | --- | --- | --- | --- | --- |
@@ -47,6 +96,73 @@ and it's a **file-level import graph**, not a full call-site/reference resolver
47
96
 
48
97
  ---
49
98
 
99
+ ## The agent loop (the actual point)
100
+
101
+ Here's the quiet failure of every other repo-map tool: it builds a beautiful map, and then the
102
+ agent forgets it exists and greps anyway. A map the agent doesn't open is just dead weight.
103
+
104
+ agentmap closes that loop. Two hooks (in [`./hooks/`](./hooks/)) do the work: the map
105
+ **refreshes itself after every commit**, and the agent gets **nudged to query it before it
106
+ serial-greps**. You wire it once — then it stays current on its own, and stays used.
107
+
108
+ ### 1. Auto-refresh on commit
109
+
110
+ [`hooks/post-commit`](./hooks/post-commit) rebuilds `.claude/agentmap.json` after each
111
+ commit, detached + silenced so it never slows the commit. It skips during
112
+ rebase/merge/cherry-pick and no-ops if Node is missing.
113
+
114
+ The hooks ship inside the npm package. The simplest setup:
115
+
116
+ ```bash
117
+ npx @raymondchins/agentmap --install-hooks
118
+ ```
119
+
120
+ This copies `hooks/post-commit` into `.git/hooks/`, sets it executable, ensures
121
+ `.claude/agentmap.json` is in `.gitignore`, and **auto-wires the `PreToolUse` nudge
122
+ hook into `.claude/settings.json`** (merge-safe + idempotent) so map enforcement is
123
+ on by default — no manual paste. Manual alternative for just the post-commit hook:
124
+
125
+ ```bash
126
+ # from your repo root
127
+ cp hooks/post-commit .git/hooks/post-commit
128
+ chmod +x .git/hooks/post-commit
129
+ ```
130
+
131
+ The hook auto-locates the builder: a local `agentmap.mjs`, then `scripts/agentmap.mjs`, then
132
+ the installed `agentmap` binary, then `npx --no-install @raymondchins/agentmap`.
133
+
134
+ ### 2. Force the agent to use it — `PreToolUse` hook
135
+
136
+ [`hooks/agentmap-nudge.mjs`](./hooks/agentmap-nudge.mjs) is a **non-blocking** `PreToolUse(Grep)`
137
+ hook for Claude Code. When a `Grep` looks like a dependency / who-imports / component-usage /
138
+ reuse search, it injects a reminder steering the agent to `agentmap --any` first. It never
139
+ denies the grep, and stays silent for raw-string / Tailwind-class / lowercase-HTML-tag
140
+ sweeps — so it's high-signal, not nagging.
141
+
142
+ `--install-hooks` writes this into `.claude/settings.json` for you (merge-safe — it
143
+ preserves existing settings and won't duplicate on re-run). For reference, or to wire
144
+ it by hand:
145
+
146
+ ```json
147
+ {
148
+ "hooks": {
149
+ "PreToolUse": [
150
+ {
151
+ "matcher": "Grep",
152
+ "hooks": [
153
+ { "type": "command", "command": "node ./hooks/agentmap-nudge.mjs" }
154
+ ]
155
+ }
156
+ ]
157
+ }
158
+ }
159
+ ```
160
+
161
+ That's the "forced to use it" in the tagline: the map stays current on its own, and the
162
+ agent is steered to it the moment it reaches for a dependency-shaped grep.
163
+
164
+ ---
165
+
50
166
  ## Quickstart
51
167
 
52
168
  No install needed:
@@ -77,8 +193,9 @@ agentmap: 154 files | 4 features | top hub: lib/utils.ts (deg 52, pr 0.105171)
77
193
 
78
194
  ## The `--any` router
79
195
 
80
- One flag, no flag-picking. `--any <query>` resolves your query through a cascade and
81
- returns the first layer that hits:
196
+ Don't want to learn eight flags? You don't have to. Throw anything at `--any` a filename, a
197
+ function, a feature, even a raw string — and it figures out what you meant, returning the first
198
+ layer that hits:
82
199
 
83
200
  ```
84
201
  --any <query>
@@ -339,74 +456,13 @@ $ node agentmap.mjs --print | jq '.hubs[0]'
339
456
  | `--help` / `-h` | Print a usage block listing every flag and exit 0. |
340
457
  | `--version` / `-v` | Print the version from `package.json` and exit 0. |
341
458
  | `--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}`. |
342
- | `--install-hooks` | Copy `hooks/post-commit` into `.git/hooks/` (chmod 0755), ensure `.claude/agentmap.json` is in `.gitignore`, and print the Claude Code `settings.json` `PreToolUse` snippet. Exit 0 on success, stderr + exit 1 on failure. |
459
+ | `--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. |
343
460
  | `--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. |
344
461
 
345
462
  **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.
346
463
 
347
464
  ---
348
465
 
349
- ## The agent loop (the actual point)
350
-
351
- A repo map only helps if the agent uses it. agentmap ships two hooks (in [`./hooks/`](./hooks/))
352
- that close the loop: the map refreshes itself after every commit, and the agent gets nudged
353
- to query the map before it serial-greps.
354
-
355
- ### 1. Auto-refresh on commit
356
-
357
- [`hooks/post-commit`](./hooks/post-commit) rebuilds `.claude/agentmap.json` after each
358
- commit, detached + silenced so it never slows the commit. It skips during
359
- rebase/merge/cherry-pick and no-ops if Node is missing.
360
-
361
- The hooks ship inside the npm package. The simplest setup:
362
-
363
- ```bash
364
- npx @raymondchins/agentmap --install-hooks
365
- ```
366
-
367
- This copies `hooks/post-commit` into `.git/hooks/`, sets it executable, ensures
368
- `.claude/agentmap.json` is in `.gitignore`, and prints the `.claude/settings.json`
369
- snippet for the `PreToolUse` nudge hook (below). Manual alternative:
370
-
371
- ```bash
372
- # from your repo root
373
- cp hooks/post-commit .git/hooks/post-commit
374
- chmod +x .git/hooks/post-commit
375
- ```
376
-
377
- The hook auto-locates the builder: a local `agentmap.mjs`, then `scripts/agentmap.mjs`, then
378
- the installed `agentmap` binary, then `npx --no-install @raymondchins/agentmap`.
379
-
380
- ### 2. Force the agent to use it — `PreToolUse` hook
381
-
382
- [`hooks/agentmap-nudge.mjs`](./hooks/agentmap-nudge.mjs) is a **non-blocking** `PreToolUse(Grep)`
383
- hook for Claude Code. When a `Grep` looks like a dependency / who-imports / component-usage /
384
- reuse search, it injects a reminder steering the agent to `agentmap --any` first. It never
385
- denies the grep, and stays silent for raw-string / Tailwind-class / lowercase-HTML-tag
386
- sweeps — so it's high-signal, not nagging.
387
-
388
- Wire it up in `.claude/settings.json`:
389
-
390
- ```json
391
- {
392
- "hooks": {
393
- "PreToolUse": [
394
- {
395
- "matcher": "Grep",
396
- "hooks": [
397
- { "type": "command", "command": "node ./hooks/agentmap-nudge.mjs" }
398
- ]
399
- }
400
- ]
401
- }
402
- }
403
- ```
404
-
405
- That's the "forced to use it" in the tagline: the map stays current on its own, and the
406
- agent is steered to it the moment it reaches for a dependency-shaped grep.
407
-
408
- ---
409
-
410
466
  ## Scope & limitations
411
467
 
412
468
  Honesty first — this is deliberately a small, sharp tool, not a universal code-graph.
@@ -433,24 +489,6 @@ Honesty first — this is deliberately a small, sharp tool, not a universal code
433
489
 
434
490
  ---
435
491
 
436
- ## Benchmark
437
-
438
- Measured across **7 agent tasks on 3 real public repos** — reproducible with `node benchmark/bench.mjs <repo>`:
439
-
440
- | Repo | Files | Tokens saved |
441
- |------|------:|-------------:|
442
- | [vercel/ai-chatbot](https://github.com/vercel/ai-chatbot) | 154 | **98.3%** |
443
- | [colinhacks/zod](https://github.com/colinhacks/zod) | 367 | **99.2%** |
444
- | [shadcn-ui/taxonomy](https://github.com/shadcn-ui/taxonomy) | 125 | **96.0%** |
445
-
446
- Per-task peaks (real, across the three repos): **whole-repo map 99.8%**, **reuse-before-rebuild lookup 99.9%**, **blast-radius 99.2%**, **find-symbol 99%**. Cold build (parse + PageRank + symbol graph) **~1.2s**; warm cached query **~0.2s**.
447
-
448
- Honest notes: the win scales with repo size — a *trivial single-file* `--any` lookup can actually cost **more** than `cat`+`grep` (taxonomy showed −313% on that one task; we leave it in). Numbers measure **context-token volume**, not end-to-end retrieval accuracy. Token est = `chars / 4`, applied to both sides.
449
-
450
- Full methodology, per-repo tables, and all caveats: **[`./benchmark/RESULTS.md`](./benchmark/RESULTS.md)**.
451
-
452
- ---
453
-
454
492
  ## Contributing
455
493
 
456
494
  Issues and PRs welcome. High-value directions:
package/agentmap.mjs CHANGED
@@ -13,11 +13,18 @@
13
13
  // Near-zero deps (ts-morph only). Runs in the target repo's cwd.
14
14
  // Algorithm credit: Aider's repo map (Apache-2.0) — github.com/Aider-AI/aider
15
15
  // ============================================================================
16
- import { Project, SyntaxKind } from "ts-morph";
17
- const CallExpression = SyntaxKind.CallExpression;
18
16
  import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync, readdirSync, statSync, chmodSync } from "node:fs";
19
17
  import { execSync, execFileSync } from "node:child_process";
20
18
  import { createHash } from "node:crypto";
19
+ import { createRequire } from "node:module";
20
+
21
+ // Lazy ts-morph: its ~105ms module init only fires on a COLD rebuild. Warm cache
22
+ // queries (the common case) never construct a Project, so they skip the load
23
+ // entirely (~2x faster warm). createRequire keeps it synchronous — no async to
24
+ // thread through build()/makeProject().
25
+ const _require = createRequire(import.meta.url);
26
+ let _tsm = null;
27
+ const tsMorph = () => (_tsm ??= _require("ts-morph"));
21
28
 
22
29
  const MAP = ".claude/agentmap.json";
23
30
  const SCHEMA_VERSION = 2;
@@ -177,6 +184,7 @@ function identMul(ident, defineCount, mentioned) {
177
184
  // else (missing / malformed / solution-style references that index 0 files) fall
178
185
  // back to broad source globs so the tool degrades gracefully instead of crashing.
179
186
  function makeProject() {
187
+ const { Project } = tsMorph();
180
188
  // skipFileDependencyResolution: ~40% faster build, verified identical edge
181
189
  // set (we resolve module specifiers explicitly below, never via the implicit
182
190
  // dependency graph). allowJs so .js/.jsx are parsed.
@@ -248,6 +256,8 @@ function makeProject() {
248
256
  function build() {
249
257
  const t0 = Date.now();
250
258
  const project = makeProject();
259
+ const { SyntaxKind } = tsMorph();
260
+ const CallExpression = SyntaxKind.CallExpression;
251
261
  const cwd = process.cwd().replace(/\\/g, "/");
252
262
  const rel = (p) => p.replace(cwd + "/", "");
253
263
  const files = {}, dependents = {}, features = {};
@@ -534,14 +544,13 @@ function fileBlock(key, f) {
534
544
 
535
545
  // ---------------------------------------------------------------------------
536
546
  // --install-hooks: copy the package post-commit hook into .git/hooks, ensure
537
- // .claude/agentmap.json is gitignored, and print the Claude Code PreToolUse
538
- // snippet (mirrors hooks/INSTALL.md). Throws on any failure so the caller can
539
- // stderr+exit 1. Resolves the package hooks/ dir relative to THIS script so it
540
- // works whether agentmap is run locally, via npx, or globally installed.
547
+ // .claude/agentmap.json is gitignored, and auto-wire the Claude Code
548
+ // PreToolUse(Grep) nudge into the project's .claude/settings.json so map
549
+ // enforcement is ON by default (no manual copy-paste). Merge-safe + idempotent.
550
+ // Throws on any failure so the caller can stderr+exit 1.
541
551
  // ---------------------------------------------------------------------------
542
552
  function installHooks() {
543
553
  const src = new URL("./hooks/post-commit", import.meta.url);
544
- const nudge = new URL("./hooks/agentmap-nudge.mjs", import.meta.url);
545
554
  // The package hooks/ dir must ship alongside agentmap.mjs.
546
555
  if (!existsSync(src)) throw new Error(`packaged hook not found at ${src.pathname} (is the hooks/ dir present?)`);
547
556
 
@@ -565,14 +574,36 @@ function installHooks() {
565
574
  writeFileSync(".gitignore", IGNORE_LINE + "\n");
566
575
  }
567
576
 
568
- // Success report + the ready-to-paste PreToolUse snippet (points node at the
569
- // installed nudge use its absolute path so it resolves from any cwd).
577
+ // Auto-wire the PreToolUse(Grep) enforcement nudge into the PROJECT settings
578
+ // (.claude/settings.json) so "the agent is forced to use the map" is ON by
579
+ // default — not a manual paste. Merge-safe + idempotent: preserves any
580
+ // existing settings/hooks, never duplicates our entry. Uses a project-relative
581
+ // command so a committed settings.json stays portable across machines.
582
+ const NUDGE_CMD = "node node_modules/@raymondchins/agentmap/hooks/agentmap-nudge.mjs";
583
+ const settingsPath = ".claude/settings.json";
584
+ let settings = {};
585
+ if (existsSync(settingsPath)) {
586
+ try { settings = JSON.parse(readFileSync(settingsPath, "utf8")) || {}; }
587
+ catch { throw new Error(`${settingsPath} is not valid JSON — fix or remove it, then re-run --install-hooks`); }
588
+ }
589
+ settings.hooks ??= {};
590
+ settings.hooks.PreToolUse ??= [];
591
+ const alreadyWired = settings.hooks.PreToolUse.some(
592
+ (e) => Array.isArray(e?.hooks) && e.hooks.some((h) => typeof h?.command === "string" && h.command.includes("agentmap-nudge")),
593
+ );
594
+ if (!alreadyWired) {
595
+ settings.hooks.PreToolUse.push({ matcher: "Grep", hooks: [{ type: "command", command: NUDGE_CMD }] });
596
+ mkdirSync(".claude", { recursive: true });
597
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
598
+ }
599
+
600
+ // Success report.
570
601
  console.log(`installed post-commit hook → ${dest}`);
571
602
  console.log(ignoredAlready ? `.gitignore already has ${IGNORE_LINE}` : `added ${IGNORE_LINE} to .gitignore`);
572
- console.log("\nAdd this to your .claude/settings.json to enable the PreToolUse(Grep) nudge:\n");
573
- console.log(JSON.stringify({
574
- hooks: { PreToolUse: [{ matcher: "Grep", hooks: [{ type: "command", command: `node ${nudge.pathname}` }] }] },
575
- }, null, 2));
603
+ console.log(alreadyWired
604
+ ? `${settingsPath} already wires the PreToolUse(Grep) → agentmap nudge — left as-is`
605
+ : `wired PreToolUse(Grep) agentmap nudge into ${settingsPath} (map enforcement on by default)`);
606
+ console.log("\nDone — the map auto-refreshes on commit, and greps are nudged to agentmap first.");
576
607
  }
577
608
 
578
609
  // ---------------------------------------------------------------------------
package/hooks/INSTALL.md CHANGED
@@ -121,8 +121,10 @@ agentmap --install-hooks
121
121
  ```
122
122
 
123
123
  This copies `hooks/post-commit` to `.git/hooks/post-commit`, chmods it, ensures
124
- `.claude/agentmap.json` is in `.gitignore`, and prints the Claude Code
125
- `settings.json` PreToolUse snippet all in one step.
124
+ `.claude/agentmap.json` is in `.gitignore`, and auto-wires the Claude Code
125
+ `PreToolUse(Grep)` nudge into `.claude/settings.json` (merge-safepreserves
126
+ existing settings, idempotent on re-run) — enforcement on by default, all in one
127
+ step. No manual paste needed.
126
128
 
127
129
  **Manual alternative:**
128
130
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raymondchins/agentmap",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },