@raymondchins/agentmap 0.2.2 → 0.3.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 +141 -95
- package/agentmap.mjs +23 -11
- package/hooks/agentmap-nudge.mjs +80 -38
- package/package.json +1 -1
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
[](https://www.npmjs.com/package/@raymondchins/agentmap)
|
|
17
18
|
[](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
|
|
29
|
-
prompt and
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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,84 @@ 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** hook for
|
|
137
|
+
Claude Code that covers **both** the `Grep` tool and raw Bash text-searchers
|
|
138
|
+
(`grep`/`rg`/`egrep`/`fgrep`/`ag`/`ack`). When either looks like a dependency /
|
|
139
|
+
who-imports / component-usage / reuse / where-is-symbol search, it injects a reminder
|
|
140
|
+
steering the agent to `agentmap --any` first. It never denies the call, and stays silent
|
|
141
|
+
for raw-string / Tailwind-class / lowercase-HTML-tag sweeps and for pipe-filtered commands
|
|
142
|
+
like `ps aux | grep node` — so it's high-signal, not nagging.
|
|
143
|
+
|
|
144
|
+
**Fires on:** `import`/`require`/`export`/`from '...'` patterns, JSX component tags
|
|
145
|
+
(`<Hero`, `<ProviderCard`), explicit intent words (`where is`, `who imports`, `reuse`,
|
|
146
|
+
`existing component`), and — in the Bash branch — bare multi-hump PascalCase identifiers
|
|
147
|
+
(`ProviderCard`, `TopProviders`) that almost always mean "where is this symbol / who uses
|
|
148
|
+
it". The Bash branch only fires when the searcher is the *primary* command (at the start,
|
|
149
|
+
or after `;`/`&&`); piped log-filters stay silent.
|
|
150
|
+
|
|
151
|
+
`--install-hooks` writes both matchers into `.claude/settings.json` for you (merge-safe —
|
|
152
|
+
preserves existing settings, won't duplicate on re-run). The single hook file dispatches
|
|
153
|
+
internally on `tool_name`. For reference, or to wire it by hand:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"hooks": {
|
|
158
|
+
"PreToolUse": [
|
|
159
|
+
{
|
|
160
|
+
"matcher": "Grep",
|
|
161
|
+
"hooks": [{ "type": "command", "command": "node ./hooks/agentmap-nudge.mjs" }]
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"matcher": "Bash",
|
|
165
|
+
"hooks": [{ "type": "command", "command": "node ./hooks/agentmap-nudge.mjs" }]
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
That's the "forced to use it" in the tagline: the map stays current on its own, and the
|
|
173
|
+
agent is steered to it the moment it reaches for a dependency-shaped grep or Bash search.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
50
177
|
## Quickstart
|
|
51
178
|
|
|
52
179
|
No install needed:
|
|
@@ -77,8 +204,9 @@ agentmap: 154 files | 4 features | top hub: lib/utils.ts (deg 52, pr 0.105171)
|
|
|
77
204
|
|
|
78
205
|
## The `--any` router
|
|
79
206
|
|
|
80
|
-
|
|
81
|
-
|
|
207
|
+
Don't want to learn eight flags? You don't have to. Throw anything at `--any` — a filename, a
|
|
208
|
+
function, a feature, even a raw string — and it figures out what you meant, returning the first
|
|
209
|
+
layer that hits:
|
|
82
210
|
|
|
83
211
|
```
|
|
84
212
|
--any <query>
|
|
@@ -346,70 +474,6 @@ $ node agentmap.mjs --print | jq '.hubs[0]'
|
|
|
346
474
|
|
|
347
475
|
---
|
|
348
476
|
|
|
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 **auto-wires the `PreToolUse` nudge
|
|
369
|
-
hook into `.claude/settings.json`** (merge-safe + idempotent) so map enforcement is
|
|
370
|
-
on by default — no manual paste. Manual alternative for just the post-commit hook:
|
|
371
|
-
|
|
372
|
-
```bash
|
|
373
|
-
# from your repo root
|
|
374
|
-
cp hooks/post-commit .git/hooks/post-commit
|
|
375
|
-
chmod +x .git/hooks/post-commit
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
The hook auto-locates the builder: a local `agentmap.mjs`, then `scripts/agentmap.mjs`, then
|
|
379
|
-
the installed `agentmap` binary, then `npx --no-install @raymondchins/agentmap`.
|
|
380
|
-
|
|
381
|
-
### 2. Force the agent to use it — `PreToolUse` hook
|
|
382
|
-
|
|
383
|
-
[`hooks/agentmap-nudge.mjs`](./hooks/agentmap-nudge.mjs) is a **non-blocking** `PreToolUse(Grep)`
|
|
384
|
-
hook for Claude Code. When a `Grep` looks like a dependency / who-imports / component-usage /
|
|
385
|
-
reuse search, it injects a reminder steering the agent to `agentmap --any` first. It never
|
|
386
|
-
denies the grep, and stays silent for raw-string / Tailwind-class / lowercase-HTML-tag
|
|
387
|
-
sweeps — so it's high-signal, not nagging.
|
|
388
|
-
|
|
389
|
-
`--install-hooks` writes this into `.claude/settings.json` for you (merge-safe — it
|
|
390
|
-
preserves existing settings and won't duplicate on re-run). For reference, or to wire
|
|
391
|
-
it by hand:
|
|
392
|
-
|
|
393
|
-
```json
|
|
394
|
-
{
|
|
395
|
-
"hooks": {
|
|
396
|
-
"PreToolUse": [
|
|
397
|
-
{
|
|
398
|
-
"matcher": "Grep",
|
|
399
|
-
"hooks": [
|
|
400
|
-
{ "type": "command", "command": "node ./hooks/agentmap-nudge.mjs" }
|
|
401
|
-
]
|
|
402
|
-
}
|
|
403
|
-
]
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
That's the "forced to use it" in the tagline: the map stays current on its own, and the
|
|
409
|
-
agent is steered to it the moment it reaches for a dependency-shaped grep.
|
|
410
|
-
|
|
411
|
-
---
|
|
412
|
-
|
|
413
477
|
## Scope & limitations
|
|
414
478
|
|
|
415
479
|
Honesty first — this is deliberately a small, sharp tool, not a universal code-graph.
|
|
@@ -436,24 +500,6 @@ Honesty first — this is deliberately a small, sharp tool, not a universal code
|
|
|
436
500
|
|
|
437
501
|
---
|
|
438
502
|
|
|
439
|
-
## Benchmark
|
|
440
|
-
|
|
441
|
-
Measured across **7 agent tasks on 3 real public repos** — reproducible with `node benchmark/bench.mjs <repo>`:
|
|
442
|
-
|
|
443
|
-
| Repo | Files | Tokens saved |
|
|
444
|
-
|------|------:|-------------:|
|
|
445
|
-
| [vercel/ai-chatbot](https://github.com/vercel/ai-chatbot) | 154 | **98.3%** |
|
|
446
|
-
| [colinhacks/zod](https://github.com/colinhacks/zod) | 367 | **99.2%** |
|
|
447
|
-
| [shadcn-ui/taxonomy](https://github.com/shadcn-ui/taxonomy) | 125 | **96.0%** |
|
|
448
|
-
|
|
449
|
-
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**.
|
|
450
|
-
|
|
451
|
-
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.
|
|
452
|
-
|
|
453
|
-
Full methodology, per-repo tables, and all caveats: **[`./benchmark/RESULTS.md`](./benchmark/RESULTS.md)**.
|
|
454
|
-
|
|
455
|
-
---
|
|
456
|
-
|
|
457
503
|
## Contributing
|
|
458
504
|
|
|
459
505
|
Issues and PRs welcome. High-value directions:
|
package/agentmap.mjs
CHANGED
|
@@ -545,7 +545,7 @@ function fileBlock(key, f) {
|
|
|
545
545
|
// ---------------------------------------------------------------------------
|
|
546
546
|
// --install-hooks: copy the package post-commit hook into .git/hooks, ensure
|
|
547
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
|
|
548
|
+
// PreToolUse(Grep|Bash) nudge into the project's .claude/settings.json so map
|
|
549
549
|
// enforcement is ON by default (no manual copy-paste). Merge-safe + idempotent.
|
|
550
550
|
// Throws on any failure so the caller can stderr+exit 1.
|
|
551
551
|
// ---------------------------------------------------------------------------
|
|
@@ -574,11 +574,13 @@ function installHooks() {
|
|
|
574
574
|
writeFileSync(".gitignore", IGNORE_LINE + "\n");
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
-
// Auto-wire the PreToolUse(Grep) enforcement nudge into the PROJECT
|
|
578
|
-
// (.claude/settings.json) so "the agent is forced to use the map"
|
|
579
|
-
// default — not a manual paste. Merge-safe + idempotent: preserves
|
|
580
|
-
// existing settings/hooks, never duplicates our entry. Uses a project-
|
|
581
|
-
// command so a committed settings.json stays portable across machines.
|
|
577
|
+
// Auto-wire the PreToolUse(Grep|Bash) enforcement nudge into the PROJECT
|
|
578
|
+
// settings (.claude/settings.json) so "the agent is forced to use the map"
|
|
579
|
+
// is ON by default — not a manual paste. Merge-safe + idempotent: preserves
|
|
580
|
+
// any existing settings/hooks, never duplicates our entry. Uses a project-
|
|
581
|
+
// relative command so a committed settings.json stays portable across machines.
|
|
582
|
+
// Both the Grep tool AND raw Bash searchers (grep/rg/ag/ack) are covered by
|
|
583
|
+
// a single hook file — the nudge routes internally based on tool_name.
|
|
582
584
|
const NUDGE_CMD = "node node_modules/@raymondchins/agentmap/hooks/agentmap-nudge.mjs";
|
|
583
585
|
const settingsPath = ".claude/settings.json";
|
|
584
586
|
let settings = {};
|
|
@@ -588,11 +590,21 @@ function installHooks() {
|
|
|
588
590
|
}
|
|
589
591
|
settings.hooks ??= {};
|
|
590
592
|
settings.hooks.PreToolUse ??= [];
|
|
591
|
-
|
|
592
|
-
|
|
593
|
+
// Check whether BOTH matchers are already present.
|
|
594
|
+
const hasGrep = settings.hooks.PreToolUse.some(
|
|
595
|
+
(e) => e?.matcher === "Grep" && Array.isArray(e?.hooks) && e.hooks.some((h) => typeof h?.command === "string" && h.command.includes("agentmap-nudge")),
|
|
593
596
|
);
|
|
594
|
-
|
|
597
|
+
const hasBash = settings.hooks.PreToolUse.some(
|
|
598
|
+
(e) => e?.matcher === "Bash" && Array.isArray(e?.hooks) && e.hooks.some((h) => typeof h?.command === "string" && h.command.includes("agentmap-nudge")),
|
|
599
|
+
);
|
|
600
|
+
const alreadyWired = hasGrep && hasBash;
|
|
601
|
+
if (!hasGrep) {
|
|
595
602
|
settings.hooks.PreToolUse.push({ matcher: "Grep", hooks: [{ type: "command", command: NUDGE_CMD }] });
|
|
603
|
+
}
|
|
604
|
+
if (!hasBash) {
|
|
605
|
+
settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [{ type: "command", command: NUDGE_CMD }] });
|
|
606
|
+
}
|
|
607
|
+
if (!alreadyWired) {
|
|
596
608
|
mkdirSync(".claude", { recursive: true });
|
|
597
609
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
598
610
|
}
|
|
@@ -601,8 +613,8 @@ function installHooks() {
|
|
|
601
613
|
console.log(`installed post-commit hook → ${dest}`);
|
|
602
614
|
console.log(ignoredAlready ? `.gitignore already has ${IGNORE_LINE}` : `added ${IGNORE_LINE} to .gitignore`);
|
|
603
615
|
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)`);
|
|
616
|
+
? `${settingsPath} already wires the PreToolUse(Grep|Bash) → agentmap nudge — left as-is`
|
|
617
|
+
: `wired PreToolUse(Grep|Bash) → agentmap nudge into ${settingsPath} (map enforcement on by default)`);
|
|
606
618
|
console.log("\nDone — the map auto-refreshes on commit, and greps are nudged to agentmap first.");
|
|
607
619
|
}
|
|
608
620
|
|
package/hooks/agentmap-nudge.mjs
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
// ============================================================================
|
|
4
|
-
// agentmap — PreToolUse(Grep
|
|
4
|
+
// agentmap — PreToolUse nudge hook (Grep tool + Bash text-searchers)
|
|
5
5
|
//
|
|
6
|
-
// Steers dependency / who-imports / reuse / component-usage
|
|
7
|
-
// agentmap repo-map instead of serial grep. NON-BLOCKING:
|
|
8
|
-
// reminder via `additionalContext`; never denies the
|
|
9
|
-
// path. Dependency-free (Node stdlib only) — Claude Code
|
|
10
|
-
// JSON on stdin.
|
|
6
|
+
// Steers dependency / who-imports / reuse / component-usage / where-is-symbol
|
|
7
|
+
// searches toward the agentmap repo-map instead of serial grep. NON-BLOCKING:
|
|
8
|
+
// only ever injects a reminder via `additionalContext`; never denies the call.
|
|
9
|
+
// Exits 0 on every path. Dependency-free (Node stdlib only) — Claude Code
|
|
10
|
+
// pipes the tool-call JSON on stdin.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
12
|
+
// Why the Bash branch: the original hook only watched the Grep TOOL, so any
|
|
13
|
+
// search run as raw `grep`/`rg` via Bash bypassed the nudge entirely — the
|
|
14
|
+
// exact gap that let an agent forget agentmap and fall back to manual
|
|
15
|
+
// Read/sed/awk. This closes it.
|
|
16
|
+
//
|
|
17
|
+
// Heuristic: fires when the search looks like (a) a dependency hunt
|
|
13
18
|
// (import/require/export / "from '..." / who-imports), (b) a component /
|
|
14
19
|
// "where-is" / reuse lookup (a JSX component tag like <Heading, or where-is /
|
|
15
|
-
// who-uses / reuse / existing-component intent words)
|
|
16
|
-
//
|
|
17
|
-
//
|
|
20
|
+
// who-uses / reuse / existing-component intent words), (c) — Bash only — a
|
|
21
|
+
// bare multi-hump PascalCase identifier (ProviderCard, TopProviders), almost
|
|
22
|
+
// always a "where is this symbol / who uses it" hunt. A raw string or
|
|
23
|
+
// Tailwind-class search (bg-white, text-3xl) and lowercase HTML-tag sweeps
|
|
24
|
+
// (<div, <h1) produce NO output — no nagging.
|
|
25
|
+
//
|
|
26
|
+
// Bash branch only fires when grep/rg/ag is the PRIMARY command (at start, or
|
|
27
|
+
// after `;` / `&&` — NOT after a pipe, so `… | grep SomeError` log-filtering
|
|
28
|
+
// stays silent).
|
|
18
29
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
30
|
+
// Injection-safe: the user's pattern/command is ONLY regex-tested, never
|
|
31
|
+
// interpolated into the emitted message or executed. Output is a single fixed
|
|
32
|
+
// JSON object.
|
|
22
33
|
// ============================================================================
|
|
23
34
|
|
|
24
35
|
let raw = "";
|
|
@@ -27,14 +38,8 @@ process.stdin.on("data", (c) => (raw += c));
|
|
|
27
38
|
process.stdin.on("end", () => {
|
|
28
39
|
try {
|
|
29
40
|
const payload = JSON.parse(raw || "{}");
|
|
41
|
+
const tool = String(payload.tool_name || "");
|
|
30
42
|
const ti = payload.tool_input || {};
|
|
31
|
-
const pattern = String(ti.pattern || "");
|
|
32
|
-
|
|
33
|
-
// Defensive guard: pathological-input belt-and-suspenders.
|
|
34
|
-
// If the pattern is unreasonably long, skip nudging entirely.
|
|
35
|
-
if (pattern.length > 2000) {
|
|
36
|
-
process.exit(0);
|
|
37
|
-
}
|
|
38
43
|
|
|
39
44
|
// (a) Dependency / who-imports / reuse intent signals in the pattern itself.
|
|
40
45
|
const DEP_RE =
|
|
@@ -45,11 +50,14 @@ process.stdin.on("end", () => {
|
|
|
45
50
|
// raw HTML/content sweeps of <div>/<h1> silent, so it stays high-signal for
|
|
46
51
|
// "where is this component used/defined".
|
|
47
52
|
//
|
|
48
|
-
// Denylist: common TS generic/utility type containers
|
|
49
|
-
//
|
|
50
|
-
//
|
|
53
|
+
// Denylist: common TS generic/utility type containers (e.g. <Promise<,
|
|
54
|
+
// <Record<string) that look like a component tag but are NOT React components.
|
|
55
|
+
// NOT ^-anchored — matches ANYWHERE, because the Bash branch tests the whole
|
|
56
|
+
// command (the generic sits mid-string, e.g. `rg "<Promise<Foo>"`) and a
|
|
57
|
+
// generic can also appear mid-pattern in Grep (e.g. useState<Promise>). The
|
|
58
|
+
// `\b` after the name keeps real components like <PromiseCard / <MapView firing.
|
|
51
59
|
const GENERIC_DENYLIST =
|
|
52
|
-
|
|
60
|
+
/<(Promise|Array|Map|Set|Record|Partial|Readonly|Pick|Omit|Required|Exclude|Extract|NonNullable|ReturnType|Awaited|Parameters|InstanceType)\b/;
|
|
53
61
|
const COMPONENT_TAG_RE = /<[A-Z][\w.]*/;
|
|
54
62
|
|
|
55
63
|
// (c) Explicit reuse / "where-is" intent words (case-insensitive): "where is",
|
|
@@ -58,21 +66,55 @@ process.stdin.on("end", () => {
|
|
|
58
66
|
const INTENT_RE =
|
|
59
67
|
/\bwhere\s+is\b|\bwho\s+(imports|uses|renders)\b|\breuse\b|\b(existing|shared)\s+(util|component|hook|helper)\b|\bis\s+there\s+(an?\s+)?(existing|shared)\b/i;
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
// (d) Bare multi-hump PascalCase identifier (e.g. ProviderCard, TopProviders).
|
|
70
|
+
// Bash branch only. Two humps required so all-caps (TODO, API), single-word
|
|
71
|
+
// (Error, Button) and lowercase (useState) stay silent → high signal, no
|
|
72
|
+
// Tailwind/raw-string noise.
|
|
73
|
+
const SYMBOL_RE = /\b[A-Z][a-z0-9]+[A-Z][A-Za-z0-9]*\b/;
|
|
74
|
+
|
|
75
|
+
let fire = false;
|
|
76
|
+
|
|
77
|
+
if (tool === "Grep") {
|
|
78
|
+
const pattern = String(ti.pattern || "");
|
|
79
|
+
|
|
80
|
+
// Defensive guard: pathological-input belt-and-suspenders.
|
|
81
|
+
// If the pattern is unreasonably long, skip nudging entirely.
|
|
82
|
+
if (pattern.length > 2000) {
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fire =
|
|
87
|
+
!!pattern &&
|
|
88
|
+
(DEP_RE.test(pattern) ||
|
|
89
|
+
(COMPONENT_TAG_RE.test(pattern) && !GENERIC_DENYLIST.test(pattern)) ||
|
|
90
|
+
INTENT_RE.test(pattern));
|
|
91
|
+
} else if (tool === "Bash") {
|
|
92
|
+
const cmd = String(ti.command || "");
|
|
93
|
+
// Only when grep/rg/ag is the PRIMARY command (start, or after ; / && — NOT
|
|
94
|
+
// after a pipe, so `… | grep SomeError` log-filtering stays silent). Then
|
|
95
|
+
// test the whole command, plus the symbol rule for bare-identifier symbol
|
|
96
|
+
// hunts.
|
|
97
|
+
const SEARCHER_RE = /(^|[;&]\s*)(rg|ripgrep|grep|egrep|fgrep|ag|ack)\b/;
|
|
98
|
+
if (SEARCHER_RE.test(cmd)) {
|
|
99
|
+
fire =
|
|
100
|
+
DEP_RE.test(cmd) ||
|
|
101
|
+
(COMPONENT_TAG_RE.test(cmd) && !GENERIC_DENYLIST.test(cmd)) ||
|
|
102
|
+
INTENT_RE.test(cmd) ||
|
|
103
|
+
SYMBOL_RE.test(cmd);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (fire) {
|
|
108
|
+
const AM = "node node_modules/@raymondchins/agentmap/agentmap.mjs";
|
|
67
109
|
const msg =
|
|
68
|
-
"This
|
|
69
|
-
"Use agentmap FIRST — it's faster than serial grep. Easiest: " +
|
|
70
|
-
"`
|
|
71
|
-
"
|
|
72
|
-
"Or be specific:
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"Rebuild the map with `
|
|
110
|
+
"This looks like a dependency / component / who-imports / reuse / where-is-symbol " +
|
|
111
|
+
"search. Use agentmap FIRST — it's faster than serial grep. Easiest: " +
|
|
112
|
+
"`" + AM + " --any <query>` " +
|
|
113
|
+
"(one command — auto-routes file → symbol → feature → live content). " +
|
|
114
|
+
"Or be specific: `" + AM + " --relates <path>` (blast radius / who-imports), " +
|
|
115
|
+
"`" + AM + " --find <symbol>` (reuse-before-rebuild / where a component is defined), " +
|
|
116
|
+
"`" + AM + " --feature <name>` (files in a feature). " +
|
|
117
|
+
"Rebuild the map with `npm run agentmap` (or `npx @raymondchins/agentmap`) if it's stale. " +
|
|
76
118
|
"Only fall back to grep if agentmap doesn't cover it.";
|
|
77
119
|
process.stdout.write(
|
|
78
120
|
JSON.stringify({
|