@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 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** `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:
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
- { "type": "command", "command": "node ./hooks/agentmap-nudge.mjs" }
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 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.
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
- 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
+ // 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
- if (!alreadyWired) {
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
 
@@ -1,24 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  // SPDX-License-Identifier: MIT
3
3
  // ============================================================================
4
- // agentmap — PreToolUse(Grep) nudge hook
4
+ // agentmap — PreToolUse nudge hook (Grep tool + Bash text-searchers)
5
5
  //
6
- // Steers dependency / who-imports / reuse / component-usage greps toward the
7
- // agentmap repo-map instead of serial grep. NON-BLOCKING: only ever injects a
8
- // reminder via `additionalContext`; never denies the Grep. Exits 0 on every
9
- // path. Dependency-free (Node stdlib only) — Claude Code pipes the tool-call
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
- // Heuristic: fires when the grep PATTERN looks like (a) a dependency hunt
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). A raw string or
16
- // Tailwind-class search (e.g. "bg-white", "text-3xl") and lowercase HTML-tag
17
- // sweeps (<div, <h1) produce NO output no nagging.
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
- // agentmap's `--any` router falls back to a live git-grep on its own, so it
20
- // still covers the raw-string / copy case but only when the agent reaches
21
- // for it deliberately, not on every content sweep.
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 that start with an
49
- // uppercase letter but are NOT React components. Without this, a grep for
50
- // `<Promise<` or `<Record<string` fires the nudge spuriously.
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
- /^<(Promise|Array|Map|Set|Record|Partial|Readonly|Pick|Omit|Required|Exclude|Extract|NonNullable|ReturnType|Awaited|Parameters|InstanceType)\b/;
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
- if (
62
- pattern &&
63
- (DEP_RE.test(pattern) ||
64
- (COMPONENT_TAG_RE.test(pattern) && !GENERIC_DENYLIST.test(pattern)) ||
65
- INTENT_RE.test(pattern))
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 Grep looks like a dependency / component / who-imports / reuse search. " +
69
- "Use agentmap FIRST — it's faster than serial grep. Easiest: " +
70
- "`npx @raymondchins/agentmap --any <query>` (or `node agentmap.mjs --any <query>`) " +
71
- "one command, auto-routes file → symbol → feature → live content. " +
72
- "Or be specific: `--relates <path>` (blast radius / who-imports), " +
73
- "`--find <symbol>` (reuse-before-rebuild / where a component is defined), " +
74
- "`--feature <name>` (files in a feature), `--hubs` (most-imported files). " +
75
- "Rebuild the map with `npx @raymondchins/agentmap` (or `node agentmap.mjs`) if it's stale. " +
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raymondchins/agentmap",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },