@raymondchins/agentmap 0.2.3 → 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 +24 -13
- package/agentmap.mjs +23 -11
- package/hooks/agentmap-nudge.mjs +80 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,15 +133,24 @@ the installed `agentmap` binary, then `npx --no-install @raymondchins/agentmap`.
|
|
|
133
133
|
|
|
134
134
|
### 2. Force the agent to use it — `PreToolUse` hook
|
|
135
135
|
|
|
136
|
-
[`hooks/agentmap-nudge.mjs`](./hooks/agentmap-nudge.mjs) is a **non-blocking**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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:
|
|
145
154
|
|
|
146
155
|
```json
|
|
147
156
|
{
|
|
@@ -149,9 +158,11 @@ it by hand:
|
|
|
149
158
|
"PreToolUse": [
|
|
150
159
|
{
|
|
151
160
|
"matcher": "Grep",
|
|
152
|
-
"hooks": [
|
|
153
|
-
|
|
154
|
-
|
|
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" }]
|
|
155
166
|
}
|
|
156
167
|
]
|
|
157
168
|
}
|
|
@@ -159,7 +170,7 @@ it by hand:
|
|
|
159
170
|
```
|
|
160
171
|
|
|
161
172
|
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.
|
|
173
|
+
agent is steered to it the moment it reaches for a dependency-shaped grep or Bash search.
|
|
163
174
|
|
|
164
175
|
---
|
|
165
176
|
|
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({
|