@raymondchins/agentmap 0.7.0 → 0.8.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 +26 -8
- package/agentmap.mjs +95 -20
- package/hooks/agentmap-gemini-nudge.mjs +71 -0
- package/package.json +1 -1
- package/skills/guidance.md +24 -0
- package/skills/install-helpers.mjs +117 -0
- package/skills/install.mjs +155 -38
- package/skills/opencode-agentmap-nudge.js +38 -0
package/README.md
CHANGED
|
@@ -185,23 +185,41 @@ 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)
|
|
188
|
+
### 3. Agent skills (Cursor, Claude Code, Codex, OpenCode, Gemini, Antigravity, Copilot)
|
|
189
189
|
|
|
190
190
|
```bash
|
|
191
191
|
npx @raymondchins/agentmap --install-skill
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
Copies
|
|
195
|
-
|
|
196
|
-
|
|
194
|
+
Copies packaged **SKILL.md** files and a **Cursor rule** (`.cursor/rules/agentmap.mdc`,
|
|
195
|
+
`alwaysApply: true`) into the current repo or global agent directories. Paths follow
|
|
196
|
+
each platform's official skill-directory conventions. Options:
|
|
197
197
|
|
|
198
198
|
```bash
|
|
199
|
-
agentmap --install-skill --platform cursor
|
|
200
|
-
agentmap --install-skill --platform claude
|
|
199
|
+
agentmap --install-skill --platform cursor # Cursor rule only (project)
|
|
200
|
+
agentmap --install-skill --platform claude # .claude/skills/agentmap/SKILL.md
|
|
201
|
+
agentmap --install-skill --platform codex # .codex/skills/ (project) or ~/.codex/skills/ (global)
|
|
202
|
+
agentmap --install-skill --platform opencode # .opencode/skills/ (project) or ~/.config/opencode/skills/ (global)
|
|
203
|
+
agentmap --install-skill --platform gemini # .gemini/skills/ (project); global ~/.gemini/skills/ (Windows global: ~/.agents/skills/)
|
|
204
|
+
agentmap --install-skill --platform antigravity # .agents/skills/ (project) or ~/.gemini/config/skills/ (global)
|
|
205
|
+
agentmap --install-skill --platform copilot # .copilot/skills/ or ~/.copilot/skills/
|
|
201
206
|
agentmap --install-skill --global --platform claude # ~/.claude/skills/...
|
|
202
|
-
agentmap --install-skill --
|
|
207
|
+
agentmap --install-skill --platform agents # legacy .agents/skills/ (project or global); excluded from default `all`
|
|
208
|
+
agentmap --install-skill --dry-run # preview paths, no writes
|
|
203
209
|
```
|
|
204
210
|
|
|
211
|
+
`--platform all` installs: claude, cursor, codex, opencode, gemini, antigravity, copilot (not legacy `agents`).
|
|
212
|
+
|
|
213
|
+
Some platforms also get **always-on** docs and hooks in the same command:
|
|
214
|
+
|
|
215
|
+
| `--platform` | Skill | Also installs (project) | Global docs |
|
|
216
|
+
|--------------|-------|-------------------------|-------------|
|
|
217
|
+
| `gemini` | `.gemini/skills/…/SKILL.md` | `GEMINI.md` + `.gemini/settings.json` BeforeTool nudge | `~/.gemini/GEMINI.md` |
|
|
218
|
+
| `codex` | `.codex/skills/…/SKILL.md` | `AGENTS.md` merge-safe `<!-- agentmap:begin/end -->` block | `~/.codex/AGENTS.md` |
|
|
219
|
+
| `opencode` | `.opencode/skills/…/SKILL.md` | `AGENTS.md` + `.opencode/plugins/agentmap-nudge.js` | `~/.config/opencode/AGENTS.md` |
|
|
220
|
+
|
|
221
|
+
Codex and OpenCode share one repo-root `AGENTS.md` on project install. Existing content outside the marked block is preserved.
|
|
222
|
+
|
|
205
223
|
Pair with `--install-hooks` (Claude Code) or `--mcp` (Cursor MCP).
|
|
206
224
|
|
|
207
225
|
---
|
|
@@ -501,7 +519,7 @@ $ node agentmap.mjs --print | jq '.hubs[0]'
|
|
|
501
519
|
| `--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}`. |
|
|
502
520
|
| `--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
521
|
| `--hook-status` | Report whether the post-commit hook, PreToolUse nudge, and `.gitignore` entry are installed (no writes). |
|
|
504
|
-
| `--install-skill` | Install
|
|
522
|
+
| `--install-skill` | Install skills + always-on docs/hooks per platform (`--platform claude\|cursor\|codex\|opencode\|gemini\|antigravity\|copilot\|agents\|all`, default `all`; `--project` default, or `--global`; `--dry-run` preview). |
|
|
505
523
|
| `--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. |
|
|
506
524
|
|
|
507
525
|
**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
|
@@ -281,6 +281,77 @@ function identMul(ident, defineCount, mentioned) {
|
|
|
281
281
|
return mul;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// Read baseUrl+paths from a tsconfig/jsconfig file. Returns null when absent.
|
|
285
|
+
// Follows `extends` recursively (depth-capped) so a package tsconfig that only
|
|
286
|
+
// `extends` a shared base (Turborepo tsconfig.base.json holding all `paths`)
|
|
287
|
+
// still contributes its inherited baseUrl/paths. Child overrides parent.
|
|
288
|
+
function readTsconfigAliasOpts(cfgPath, _depth = 0) {
|
|
289
|
+
try {
|
|
290
|
+
const raw = JSON.parse(readFileSync(cfgPath, "utf8")) || {};
|
|
291
|
+
const co = raw.compilerOptions || {};
|
|
292
|
+
// Resolve inherited opts from `extends` first (parent), then layer self on top.
|
|
293
|
+
let inherited = null;
|
|
294
|
+
if (raw.extends && _depth < 10) {
|
|
295
|
+
const exts = Array.isArray(raw.extends) ? raw.extends : [raw.extends];
|
|
296
|
+
const here = dirname(cfgPath);
|
|
297
|
+
for (const ext of exts) {
|
|
298
|
+
if (typeof ext !== "string" || !ext) continue;
|
|
299
|
+
// Only resolve path-like extends (./, ../, absolute). Bare package
|
|
300
|
+
// extends (e.g. "@tsconfig/strict") live in node_modules and don't
|
|
301
|
+
// carry repo-local `paths`, so skip them safely.
|
|
302
|
+
if (!/^(\.\.?\/|\/)/.test(ext)) continue;
|
|
303
|
+
let base = join(here, ext);
|
|
304
|
+
if (!existsSync(base) && existsSync(base + ".json")) base += ".json";
|
|
305
|
+
else if (!/\.json$/.test(base) && existsSync(join(base, "tsconfig.json"))) base = join(base, "tsconfig.json");
|
|
306
|
+
if (!existsSync(base)) continue;
|
|
307
|
+
const parent = readTsconfigAliasOpts(base, _depth + 1);
|
|
308
|
+
if (parent) inherited = { ...(inherited || {}), ...parent };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const self = {};
|
|
312
|
+
if (co.baseUrl) self.baseUrl = co.baseUrl;
|
|
313
|
+
if (co.paths) self.paths = co.paths;
|
|
314
|
+
const out = { ...(inherited || {}), ...self };
|
|
315
|
+
if (!Object.keys(out).length) return null;
|
|
316
|
+
return out;
|
|
317
|
+
} catch { return null; }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Collect package-level alias configs from tsconfig/jsconfig files in the repo.
|
|
321
|
+
// Deepest-dir-first sort so nearestAliasConfig can pick the longest prefix match.
|
|
322
|
+
function discoverPackageAliasConfigs(rootAbs, listed) {
|
|
323
|
+
const root = rootAbs.replace(/\\/g, "/");
|
|
324
|
+
const configs = [];
|
|
325
|
+
const cfgRels = listed.length
|
|
326
|
+
? listed.filter((f) => /(^|\/)tsconfig\.json$/.test(f) || /(^|\/)jsconfig\.json$/.test(f))
|
|
327
|
+
: [];
|
|
328
|
+
for (const rel of cfgRels) {
|
|
329
|
+
const full = join(root, rel);
|
|
330
|
+
if (!existsSync(full)) continue;
|
|
331
|
+
const opts = readTsconfigAliasOpts(full);
|
|
332
|
+
if (!opts) continue;
|
|
333
|
+
configs.push({
|
|
334
|
+
dir: join(root, dirname(rel)).replace(/\\/g, "/"),
|
|
335
|
+
baseUrl: opts.baseUrl || ".",
|
|
336
|
+
paths: opts.paths || {},
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
configs.sort((a, b) => b.dir.length - a.dir.length);
|
|
340
|
+
return configs;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Longest matching tsconfig dir wins (monorepo package boundary).
|
|
344
|
+
function nearestAliasConfig(fromAbsDir, configs, rootAbs, rootOpts) {
|
|
345
|
+
const norm = fromAbsDir.replace(/\\/g, "/");
|
|
346
|
+
let best = null;
|
|
347
|
+
for (const c of configs) {
|
|
348
|
+
const d = c.dir;
|
|
349
|
+
if (norm === d || norm.startsWith(d + "/")) { best = c; break; } // configs sorted deepest-first
|
|
350
|
+
}
|
|
351
|
+
if (best) return best;
|
|
352
|
+
return { dir: rootAbs, baseUrl: rootOpts.baseUrl || ".", paths: rootOpts.paths || {} };
|
|
353
|
+
}
|
|
354
|
+
|
|
284
355
|
// Construct a ts-morph Project robustly: use tsconfig.json when present + valid;
|
|
285
356
|
// else (missing / malformed / solution-style references that index 0 files) fall
|
|
286
357
|
// back to broad source globs so the tool degrades gracefully instead of crashing.
|
|
@@ -296,11 +367,8 @@ function makeProject() {
|
|
|
296
367
|
for (const cfg of ["tsconfig.json", "jsconfig.json"]) {
|
|
297
368
|
try {
|
|
298
369
|
if (!existsSync(cfg)) continue;
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
if (co.baseUrl) out.baseUrl = co.baseUrl;
|
|
302
|
-
if (co.paths) out.paths = co.paths;
|
|
303
|
-
if (Object.keys(out).length) return out;
|
|
370
|
+
const opts = readTsconfigAliasOpts(cfg);
|
|
371
|
+
if (opts) return opts;
|
|
304
372
|
} catch { /* ignore — proceed without paths */ }
|
|
305
373
|
}
|
|
306
374
|
return {};
|
|
@@ -329,6 +397,7 @@ function makeProject() {
|
|
|
329
397
|
const loaded = new Set(project.getSourceFiles().map((s) => s.getFilePath()));
|
|
330
398
|
const cwdp = process.cwd().replace(/\\/g, "/");
|
|
331
399
|
const listed = sh("git ls-files --cached --others --exclude-standard").split("\n").filter(Boolean);
|
|
400
|
+
const packageAliasConfigs = discoverPackageAliasConfigs(cwdp, listed);
|
|
332
401
|
// `.vue` discovery: same channel as TS/JS (git ls-files when available, else
|
|
333
402
|
// a broad glob fallback). We do NOT hand `.vue` straight to ts-morph (it is
|
|
334
403
|
// not TS/JS). Instead, for each `.vue` file we read its `<script>` block via
|
|
@@ -390,7 +459,7 @@ function makeProject() {
|
|
|
390
459
|
vueMap[`${cwdp}/${vpath}`] = `${cwdp}/${f}`;
|
|
391
460
|
vueReal[`${cwdp}/${f}`] = true;
|
|
392
461
|
}
|
|
393
|
-
return { project, vueMap, vueReal, aliasOpts };
|
|
462
|
+
return { project, vueMap, vueReal, aliasOpts, packageAliasConfigs };
|
|
394
463
|
}
|
|
395
464
|
|
|
396
465
|
// ---------------------------------------------------------------------------
|
|
@@ -400,7 +469,7 @@ function makeProject() {
|
|
|
400
469
|
// ---------------------------------------------------------------------------
|
|
401
470
|
function build() {
|
|
402
471
|
const t0 = Date.now();
|
|
403
|
-
const { project, vueMap, vueReal, aliasOpts } = makeProject();
|
|
472
|
+
const { project, vueMap, vueReal, aliasOpts, packageAliasConfigs } = makeProject();
|
|
404
473
|
const { SyntaxKind } = tsMorph();
|
|
405
474
|
const CallExpression = SyntaxKind.CallExpression;
|
|
406
475
|
const cwd = process.cwd().replace(/\\/g, "/");
|
|
@@ -439,14 +508,15 @@ function build() {
|
|
|
439
508
|
if (vueReal[`${abs}.vue`]) return rel(`${abs}.vue`);
|
|
440
509
|
return null;
|
|
441
510
|
};
|
|
442
|
-
// #3 fix: tsconfig/jsconfig baseUrl+paths alias resolution ("@/x",
|
|
443
|
-
//
|
|
444
|
-
// ts-morph'
|
|
445
|
-
//
|
|
511
|
+
// #3 fix + monorepo: tsconfig/jsconfig baseUrl+paths alias resolution ("@/x",
|
|
512
|
+
// "#/x", "~/x") for side-effect/dynamic/require edges AND static imports when
|
|
513
|
+
// ts-morph can't resolve (cwd tsconfig lacks package paths). Per importing
|
|
514
|
+
// file, use the nearest discovered tsconfig paths.
|
|
446
515
|
const ROOTABS = process.cwd().replace(/\\/g, "/");
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
516
|
+
const resolveAlias = (spec, fromAbsDir) => {
|
|
517
|
+
const cfg = nearestAliasConfig(fromAbsDir, packageAliasConfigs, ROOTABS, aliasOpts);
|
|
518
|
+
const aliasBase = joinPosix(cfg.dir, cfg.baseUrl || ".");
|
|
519
|
+
const aliasEntries = Object.entries(cfg.paths || {});
|
|
450
520
|
for (const [pat, targets] of aliasEntries) {
|
|
451
521
|
const star = pat.indexOf("*");
|
|
452
522
|
let sub = null;
|
|
@@ -466,7 +536,7 @@ function build() {
|
|
|
466
536
|
return null;
|
|
467
537
|
};
|
|
468
538
|
const resolveSpec = (fromAbsDir, spec) => {
|
|
469
|
-
if (!spec.startsWith(".")) return resolveAlias(spec); // alias (baseUrl/paths) or null for non-relative
|
|
539
|
+
if (!spec.startsWith(".")) return resolveAlias(spec, fromAbsDir); // alias (baseUrl/paths) or null for non-relative
|
|
470
540
|
// normalize fromAbsDir + spec into an absolute-ish posix path
|
|
471
541
|
const join = (a, b) => {
|
|
472
542
|
const parts = (a + "/" + b).split("/"); const st = [];
|
|
@@ -524,11 +594,16 @@ function build() {
|
|
|
524
594
|
if (imp.getNamespaceImport()) names.push("*");
|
|
525
595
|
addEdge(rel(t.getFilePath()), names.length ? names : ["*"]);
|
|
526
596
|
} else {
|
|
527
|
-
// 6b: side-effect
|
|
528
|
-
//
|
|
597
|
+
// 6b: side-effect or alias import — ts-morph may not resolve when cwd
|
|
598
|
+
// tsconfig lacks package paths; resolveSpec uses nearest tsconfig paths.
|
|
529
599
|
const spec = imp.getModuleSpecifierValue();
|
|
530
600
|
const tp = resolveSpec(fromDir, spec);
|
|
531
|
-
if (tp)
|
|
601
|
+
if (tp) {
|
|
602
|
+
const names = imp.getNamedImports().filter((n) => !n.isTypeOnly()).map((n) => n.getName());
|
|
603
|
+
if (imp.getDefaultImport()) names.push("default");
|
|
604
|
+
if (imp.getNamespaceImport()) names.push("*");
|
|
605
|
+
addEdge(tp, names.length ? names : ["*"]);
|
|
606
|
+
}
|
|
532
607
|
}
|
|
533
608
|
}
|
|
534
609
|
for (const exp of sf.getExportDeclarations()) {
|
|
@@ -1090,8 +1165,8 @@ Maintenance:
|
|
|
1090
1165
|
--install-hooks [--dry-run]
|
|
1091
1166
|
install git post-commit + copy the PreToolUse nudge +
|
|
1092
1167
|
wire .claude/settings.json (--dry-run = preview, no writes)
|
|
1093
|
-
--install-skill [--platform claude|cursor|agents|all] [--project|--global] [--dry-run]
|
|
1094
|
-
install
|
|
1168
|
+
--install-skill [--platform claude|cursor|codex|opencode|gemini|antigravity|copilot|agents|all] [--project|--global] [--dry-run]
|
|
1169
|
+
install skills + always-on docs/hooks per platform
|
|
1095
1170
|
--hook-status report whether agentmap git/nudge wiring is installed
|
|
1096
1171
|
--setup-mcp [--dry-run]
|
|
1097
1172
|
configure MCP server for OpenCode & Antigravity IDE
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Gemini CLI BeforeTool nudge — non-blocking context when a search looks like
|
|
4
|
+
// dependency / blast-radius / reuse work. Stdlib only; copied into the project
|
|
5
|
+
// by `agentmap --install-skill --platform gemini`.
|
|
6
|
+
|
|
7
|
+
let raw = "";
|
|
8
|
+
process.stdin.setEncoding("utf8");
|
|
9
|
+
process.stdin.on("data", (c) => (raw += c));
|
|
10
|
+
process.stdin.on("end", () => {
|
|
11
|
+
try {
|
|
12
|
+
const payload = JSON.parse(raw || "{}");
|
|
13
|
+
const tool = String(payload.tool_name || "");
|
|
14
|
+
const ti = payload.tool_input || {};
|
|
15
|
+
|
|
16
|
+
const DEP_RE =
|
|
17
|
+
/\b(import|require\s*\(|imported\s+by|depends|dependents?|dependency)\b|from\s+["']|(^|\|)\s*export\b/i;
|
|
18
|
+
const GENERIC_DENYLIST =
|
|
19
|
+
/<(Promise|Array|Map|Set|Record|Partial|Readonly|Pick|Omit|Required|Exclude|Extract|NonNullable|ReturnType|Awaited|Parameters|InstanceType)\b/;
|
|
20
|
+
const COMPONENT_TAG_RE = /<[A-Z][\w.]*/;
|
|
21
|
+
const INTENT_RE =
|
|
22
|
+
/\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;
|
|
23
|
+
const SYMBOL_RE = /\b[A-Z][a-z0-9]+[A-Z][A-Za-z0-9]*\b/;
|
|
24
|
+
|
|
25
|
+
let fire = false;
|
|
26
|
+
const pattern = String(ti.pattern || ti.query || ti.content || "");
|
|
27
|
+
const cmd = String(ti.command || ti.cmd || "");
|
|
28
|
+
|
|
29
|
+
if (/grep|search|ripgrep/i.test(tool)) {
|
|
30
|
+
if (pattern.length > 0 && pattern.length <= 2000) {
|
|
31
|
+
fire =
|
|
32
|
+
DEP_RE.test(pattern) ||
|
|
33
|
+
(COMPONENT_TAG_RE.test(pattern) && !GENERIC_DENYLIST.test(pattern)) ||
|
|
34
|
+
INTENT_RE.test(pattern);
|
|
35
|
+
}
|
|
36
|
+
} else if (/shell|bash|terminal|command/i.test(tool) && cmd) {
|
|
37
|
+
const SEARCHER_RE = /(^|[;&]\s*)(rg|ripgrep|grep|egrep|fgrep|ag|ack)\b/;
|
|
38
|
+
if (SEARCHER_RE.test(cmd)) {
|
|
39
|
+
const DATA_FILE_RE = /\.(log|txt|out|csv|tsv|jsonl|ndjson|json|md|ya?ml|xml)(\b|$)/i;
|
|
40
|
+
const hasDataFileTarget = cmd.split(/\s+/).some((tok) => DATA_FILE_RE.test(tok));
|
|
41
|
+
if (!hasDataFileTarget) {
|
|
42
|
+
fire =
|
|
43
|
+
DEP_RE.test(cmd) ||
|
|
44
|
+
(COMPONENT_TAG_RE.test(cmd) && !GENERIC_DENYLIST.test(cmd)) ||
|
|
45
|
+
INTENT_RE.test(cmd) ||
|
|
46
|
+
SYMBOL_RE.test(cmd);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (fire) {
|
|
52
|
+
const msg =
|
|
53
|
+
"Dependency / blast-radius / reuse search detected. Prefer agentmap first: " +
|
|
54
|
+
"`npx @raymondchins/agentmap --any <query>` (file → symbol → feature → git-grep), " +
|
|
55
|
+
"`--relates <path>`, `--find <symbol>`. Rebuild with `npx @raymondchins/agentmap` if stale.";
|
|
56
|
+
process.stdout.write(
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
hookSpecificOutput: {
|
|
59
|
+
hookEventName: "BeforeTool",
|
|
60
|
+
additionalContext: msg,
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
process.stdout.write("{}");
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
process.stdout.write("{}");
|
|
69
|
+
}
|
|
70
|
+
process.exit(0);
|
|
71
|
+
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## agentmap (repo map)
|
|
2
|
+
|
|
3
|
+
This project can use **agentmap** (`@raymondchins/agentmap`) — a queryable import/symbol map at `.claude/agentmap/`.
|
|
4
|
+
|
|
5
|
+
**Before Grep/Glob for structural questions** (where is a symbol, who imports a file, reuse check, blast radius), prefer:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
agentmap --any <query> # file → symbol → feature → git-grep fallback
|
|
9
|
+
agentmap --find <Symbol> # exported symbols matching name
|
|
10
|
+
agentmap --relates <file.ts> # imports, dependents, related files
|
|
11
|
+
agentmap --map --tokens 400 # cheap repo orientation
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Use Read/Grep directly when:
|
|
15
|
+
|
|
16
|
+
1. agentmap already oriented you and you need exact lines to edit
|
|
17
|
+
2. The map is missing — run `agentmap` once to build `.claude/agentmap/`
|
|
18
|
+
3. Searching raw strings, logs, or non-TS files
|
|
19
|
+
|
|
20
|
+
**MCP (optional):** `agentmap --mcp` exposes the same queries as tools.
|
|
21
|
+
|
|
22
|
+
**Note:** `--features` only detects Next.js `app/` routes; TanStack `src/routes/` repos often show `features (0)`.
|
|
23
|
+
|
|
24
|
+
Package: `@raymondchins/agentmap` — not the unrelated PyPI/npm `agentmap` package.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Shared helpers for --install-skill (docs merge, hooks, plugins).
|
|
3
|
+
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from "node:fs";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const SKILLS_DIR = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
export const GUIDANCE = join(SKILLS_DIR, "guidance.md");
|
|
10
|
+
const GEMINI_NUDGE_SRC = join(SKILLS_DIR, "..", "hooks", "agentmap-gemini-nudge.mjs");
|
|
11
|
+
const OPENCODE_PLUGIN_SRC = join(SKILLS_DIR, "opencode-agentmap-nudge.js");
|
|
12
|
+
|
|
13
|
+
export const MARK_BEGIN = "<!-- agentmap:begin -->";
|
|
14
|
+
export const MARK_END = "<!-- agentmap:end -->";
|
|
15
|
+
|
|
16
|
+
export function atomicWrite(dest, body) {
|
|
17
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
18
|
+
const tmp = `${dest}.tmp`;
|
|
19
|
+
writeFileSync(tmp, body, "utf8");
|
|
20
|
+
renameSync(tmp, dest);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stripJsonComments(src) {
|
|
24
|
+
let out = "";
|
|
25
|
+
let inStr = false, esc = false, inLine = false, inBlock = false;
|
|
26
|
+
for (let i = 0; i < src.length; i++) {
|
|
27
|
+
const c = src[i], n = src[i + 1];
|
|
28
|
+
if (inLine) { if (c === "\n") { inLine = false; out += c; } continue; }
|
|
29
|
+
if (inBlock) { if (c === "*" && n === "/") { inBlock = false; i++; } continue; }
|
|
30
|
+
if (inStr) {
|
|
31
|
+
out += c;
|
|
32
|
+
if (esc) esc = false;
|
|
33
|
+
else if (c === "\\") esc = true;
|
|
34
|
+
else if (c === '"') inStr = false;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (c === '"') { inStr = true; out += c; continue; }
|
|
38
|
+
if (c === "/" && n === "/") { inLine = true; i++; continue; }
|
|
39
|
+
if (c === "/" && n === "*") { inBlock = true; i++; continue; }
|
|
40
|
+
out += c;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseSettings(text, settingsPath) {
|
|
46
|
+
try { return JSON.parse(text) || {}; }
|
|
47
|
+
catch {
|
|
48
|
+
try { return JSON.parse(stripJsonComments(text)) || {}; }
|
|
49
|
+
catch { throw new Error(`${settingsPath} is not valid JSON — fix or remove it, then re-run`); }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function readGuidanceSection() {
|
|
54
|
+
if (!existsSync(GUIDANCE)) throw new Error(`packaged guidance missing: ${GUIDANCE}`);
|
|
55
|
+
return readFileSync(GUIDANCE, "utf8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function mergeGuidanceBlock(existing, section, title) {
|
|
59
|
+
const block = `${MARK_BEGIN}\n${section.trim()}\n${MARK_END}`;
|
|
60
|
+
const re = /<!-- agentmap:begin -->[\s\S]*?<!-- agentmap:end -->/;
|
|
61
|
+
if (existing && re.test(existing)) return existing.replace(re, block);
|
|
62
|
+
const header = title ? `# ${title}\n\n` : "";
|
|
63
|
+
if (!existing?.trim()) return `${header}${block}\n`;
|
|
64
|
+
return `${existing.trimEnd()}\n\n${block}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @returns {string[]} relative paths touched */
|
|
68
|
+
export function installGeminiHooks(root, dryRun) {
|
|
69
|
+
if (root !== process.cwd()) return [];
|
|
70
|
+
const nudgeRel = ".gemini/hooks/agentmap-nudge.mjs";
|
|
71
|
+
const nudgeDest = join(root, nudgeRel);
|
|
72
|
+
const settingsPath = ".gemini/settings.json";
|
|
73
|
+
const NUDGE_CMD = `node "$GEMINI_PROJECT_DIR/.gemini/hooks/agentmap-nudge.mjs"`;
|
|
74
|
+
const targets = [nudgeRel, settingsPath];
|
|
75
|
+
|
|
76
|
+
let settings = {};
|
|
77
|
+
if (existsSync(settingsPath)) {
|
|
78
|
+
settings = parseSettings(readFileSync(settingsPath, "utf8"), settingsPath);
|
|
79
|
+
}
|
|
80
|
+
settings.hooks ??= {};
|
|
81
|
+
settings.hooks.BeforeTool ??= [];
|
|
82
|
+
const matcher = "run_shell_command|grep|search";
|
|
83
|
+
const already = settings.hooks.BeforeTool.some(
|
|
84
|
+
(e) => e?.matcher === matcher && Array.isArray(e?.hooks) &&
|
|
85
|
+
e.hooks.some((h) => typeof h?.command === "string" && h.command.includes("agentmap-nudge")),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (dryRun) return targets;
|
|
89
|
+
if (!existsSync(GEMINI_NUDGE_SRC)) throw new Error(`packaged hook missing: ${GEMINI_NUDGE_SRC}`);
|
|
90
|
+
mkdirSync(dirname(nudgeDest), { recursive: true });
|
|
91
|
+
writeFileSync(nudgeDest, readFileSync(GEMINI_NUDGE_SRC, "utf8"));
|
|
92
|
+
|
|
93
|
+
if (!already) {
|
|
94
|
+
settings.hooks.BeforeTool.push({
|
|
95
|
+
matcher,
|
|
96
|
+
hooks: [{
|
|
97
|
+
name: "agentmap-nudge",
|
|
98
|
+
type: "command",
|
|
99
|
+
command: NUDGE_CMD,
|
|
100
|
+
timeout: 5000,
|
|
101
|
+
description: "Nudge structural searches toward agentmap",
|
|
102
|
+
}],
|
|
103
|
+
});
|
|
104
|
+
atomicWrite(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
105
|
+
}
|
|
106
|
+
return targets;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @returns {string[]} relative paths touched */
|
|
110
|
+
export function installOpencodePlugin(root, dryRun) {
|
|
111
|
+
if (root !== process.cwd()) return [];
|
|
112
|
+
const dest = ".opencode/plugins/agentmap-nudge.js";
|
|
113
|
+
if (dryRun) return [dest];
|
|
114
|
+
if (!existsSync(OPENCODE_PLUGIN_SRC)) throw new Error(`packaged plugin missing: ${OPENCODE_PLUGIN_SRC}`);
|
|
115
|
+
atomicWrite(join(root, dest), readFileSync(OPENCODE_PLUGIN_SRC, "utf8"));
|
|
116
|
+
return [dest];
|
|
117
|
+
}
|
package/skills/install.mjs
CHANGED
|
@@ -1,50 +1,122 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// --install-skill:
|
|
3
|
-
// agent directories (project or global scope).
|
|
2
|
+
// --install-skill: skill files + always-on docs/hooks per platform (project or global).
|
|
4
3
|
|
|
5
|
-
import { readFileSync, writeFileSync,
|
|
6
|
-
import { homedir } from "node:os";
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { homedir, platform as osPlatform } from "node:os";
|
|
7
6
|
import { join, dirname } from "node:path";
|
|
8
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import {
|
|
9
|
+
atomicWrite,
|
|
10
|
+
readGuidanceSection,
|
|
11
|
+
mergeGuidanceBlock,
|
|
12
|
+
installGeminiHooks,
|
|
13
|
+
installOpencodePlugin,
|
|
14
|
+
} from "./install-helpers.mjs";
|
|
9
15
|
|
|
10
16
|
const SKILLS_DIR = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const SKILL_MD = join(SKILLS_DIR, "SKILL.md");
|
|
18
|
+
const CURSOR_RULE = join(SKILLS_DIR, "cursor-rule.mdc");
|
|
11
19
|
|
|
12
|
-
/** @
|
|
20
|
+
/** @param {string} root @param {boolean} _globalScope */
|
|
21
|
+
function skillPath(root, _globalScope, ...segments) {
|
|
22
|
+
return join(root, ...segments);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @type {Record<string, {
|
|
26
|
+
* label: string;
|
|
27
|
+
* src: string;
|
|
28
|
+
* dest: (root: string, globalScope: boolean) => string;
|
|
29
|
+
* projectOnly?: boolean;
|
|
30
|
+
* legacy?: boolean;
|
|
31
|
+
* docs?: (root: string, globalScope: boolean) => string;
|
|
32
|
+
* hooks?: boolean;
|
|
33
|
+
* plugin?: boolean;
|
|
34
|
+
* }>} */
|
|
13
35
|
const PLATFORMS = {
|
|
14
36
|
claude: {
|
|
15
37
|
label: "Claude Code",
|
|
16
|
-
src:
|
|
17
|
-
dest: (root) =>
|
|
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"),
|
|
38
|
+
src: SKILL_MD,
|
|
39
|
+
dest: (root) => skillPath(root, false, ".claude", "skills", "agentmap", "SKILL.md"),
|
|
23
40
|
},
|
|
24
41
|
cursor: {
|
|
25
42
|
label: "Cursor",
|
|
26
|
-
src:
|
|
27
|
-
dest: (root) =>
|
|
43
|
+
src: CURSOR_RULE,
|
|
44
|
+
dest: (root) => skillPath(root, false, ".cursor", "rules", "agentmap.mdc"),
|
|
28
45
|
projectOnly: true,
|
|
29
46
|
},
|
|
47
|
+
codex: {
|
|
48
|
+
label: "OpenAI Codex",
|
|
49
|
+
src: SKILL_MD,
|
|
50
|
+
dest: (root) => skillPath(root, false, ".codex", "skills", "agentmap", "SKILL.md"),
|
|
51
|
+
docs: (root, globalScope) =>
|
|
52
|
+
globalScope ? join(root, ".codex", "AGENTS.md") : join(root, "AGENTS.md"),
|
|
53
|
+
},
|
|
54
|
+
opencode: {
|
|
55
|
+
label: "OpenCode",
|
|
56
|
+
src: SKILL_MD,
|
|
57
|
+
dest: (root, globalScope) =>
|
|
58
|
+
globalScope
|
|
59
|
+
? join(root, ".config", "opencode", "skills", "agentmap", "SKILL.md")
|
|
60
|
+
: join(root, ".opencode", "skills", "agentmap", "SKILL.md"),
|
|
61
|
+
docs: (root, globalScope) =>
|
|
62
|
+
globalScope ? join(root, ".config", "opencode", "AGENTS.md") : join(root, "AGENTS.md"),
|
|
63
|
+
plugin: true,
|
|
64
|
+
},
|
|
65
|
+
gemini: {
|
|
66
|
+
label: "Gemini CLI",
|
|
67
|
+
src: SKILL_MD,
|
|
68
|
+
dest: (root, globalScope) => {
|
|
69
|
+
if (!globalScope) return skillPath(root, false, ".gemini", "skills", "agentmap", "SKILL.md");
|
|
70
|
+
if (osPlatform() === "win32") return skillPath(root, true, ".agents", "skills", "agentmap", "SKILL.md");
|
|
71
|
+
return skillPath(root, true, ".gemini", "skills", "agentmap", "SKILL.md");
|
|
72
|
+
},
|
|
73
|
+
docs: (root, globalScope) => {
|
|
74
|
+
if (!globalScope) return join(root, "GEMINI.md");
|
|
75
|
+
if (osPlatform() === "win32") return join(root, ".agents", "GEMINI.md");
|
|
76
|
+
return join(root, ".gemini", "GEMINI.md");
|
|
77
|
+
},
|
|
78
|
+
hooks: true,
|
|
79
|
+
},
|
|
80
|
+
antigravity: {
|
|
81
|
+
label: "Antigravity",
|
|
82
|
+
src: SKILL_MD,
|
|
83
|
+
dest: (root, globalScope) =>
|
|
84
|
+
globalScope
|
|
85
|
+
? skillPath(root, true, ".gemini", "config", "skills", "agentmap", "SKILL.md")
|
|
86
|
+
: skillPath(root, false, ".agents", "skills", "agentmap", "SKILL.md"),
|
|
87
|
+
},
|
|
88
|
+
agents: {
|
|
89
|
+
label: "Amp / legacy .agents",
|
|
90
|
+
src: SKILL_MD,
|
|
91
|
+
dest: (root) => skillPath(root, false, ".agents", "skills", "agentmap", "SKILL.md"),
|
|
92
|
+
legacy: true,
|
|
93
|
+
},
|
|
94
|
+
copilot: {
|
|
95
|
+
label: "GitHub Copilot CLI",
|
|
96
|
+
src: SKILL_MD,
|
|
97
|
+
dest: (root) => skillPath(root, false, ".copilot", "skills", "agentmap", "SKILL.md"),
|
|
98
|
+
},
|
|
30
99
|
};
|
|
31
100
|
|
|
32
|
-
|
|
33
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
34
|
-
const tmp = `${dest}.tmp`;
|
|
35
|
-
writeFileSync(tmp, body, "utf8");
|
|
36
|
-
renameSync(tmp, dest);
|
|
37
|
-
}
|
|
101
|
+
const DEFAULT_PLATFORMS = ["claude", "cursor", "codex", "opencode", "gemini", "antigravity", "copilot"];
|
|
38
102
|
|
|
39
103
|
function parsePlatforms(raw) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
104
|
+
const keys = Object.keys(PLATFORMS).join(", ");
|
|
105
|
+
if (!raw || raw === "all") return [...DEFAULT_PLATFORMS];
|
|
106
|
+
const tokens = raw.split(/[,+]/).map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
107
|
+
if (tokens.includes("all")) {
|
|
108
|
+
const extra = tokens.filter((t) => t !== "all");
|
|
109
|
+
for (const n of extra) {
|
|
110
|
+
if (!PLATFORMS[n]) throw new Error(`unknown platform '${n}' — choose: ${keys}, all`);
|
|
111
|
+
}
|
|
112
|
+
return [...new Set([...DEFAULT_PLATFORMS, ...extra])];
|
|
113
|
+
}
|
|
114
|
+
for (const n of tokens) {
|
|
43
115
|
if (!PLATFORMS[n]) {
|
|
44
|
-
throw new Error(`unknown platform '${n}' — choose: ${
|
|
116
|
+
throw new Error(`unknown platform '${n}' — choose: ${keys}, all`);
|
|
45
117
|
}
|
|
46
118
|
}
|
|
47
|
-
return
|
|
119
|
+
return tokens;
|
|
48
120
|
}
|
|
49
121
|
|
|
50
122
|
function gitAddHint(paths) {
|
|
@@ -52,18 +124,58 @@ function gitAddHint(paths) {
|
|
|
52
124
|
if (unique.length) console.log(`\nOptional: git add ${unique.map((p) => `"${p}"`).join(" ")}`);
|
|
53
125
|
}
|
|
54
126
|
|
|
127
|
+
function installDocsForPlatform(cfg, { root, globalScope, dryRun, guidance, mergedDocs, targets }) {
|
|
128
|
+
if (!cfg.docs) return;
|
|
129
|
+
const dest = cfg.docs(root, globalScope);
|
|
130
|
+
if (mergedDocs.has(dest)) return;
|
|
131
|
+
mergedDocs.add(dest);
|
|
132
|
+
const title = dest.endsWith("GEMINI.md") ? "agentmap" : undefined;
|
|
133
|
+
if (dryRun) {
|
|
134
|
+
console.log(` ${cfg.label} docs: ${dest}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const existing = existsSync(dest) ? readFileSync(dest, "utf8") : "";
|
|
138
|
+
atomicWrite(dest, mergeGuidanceBlock(existing, guidance, title));
|
|
139
|
+
console.log(` ${cfg.label} docs → ${dest}`);
|
|
140
|
+
targets.push(dest);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function installExtrasForPlatform(name, cfg, { root, globalScope, dryRun, targets }) {
|
|
144
|
+
if (cfg.hooks && !globalScope) {
|
|
145
|
+
const hookTargets = installGeminiHooks(root, dryRun);
|
|
146
|
+
for (const t of hookTargets) {
|
|
147
|
+
if (dryRun) console.log(` ${cfg.label} hooks: ${t}`);
|
|
148
|
+
else {
|
|
149
|
+
console.log(` ${cfg.label} hooks → ${t}`);
|
|
150
|
+
targets.push(t);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (cfg.plugin && !globalScope) {
|
|
155
|
+
const pluginTargets = installOpencodePlugin(root, dryRun);
|
|
156
|
+
for (const t of pluginTargets) {
|
|
157
|
+
if (dryRun) console.log(` ${cfg.label} plugin: ${t}`);
|
|
158
|
+
else {
|
|
159
|
+
console.log(` ${cfg.label} plugin → ${t}`);
|
|
160
|
+
targets.push(t);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
55
166
|
/**
|
|
56
167
|
* @param {{ platforms?: string; project?: boolean; global?: boolean; dryRun?: boolean }} opts
|
|
57
168
|
*/
|
|
58
169
|
export function installSkill({ platforms: platformsArg = "all", project = true, global: globalScope = false, dryRun = false } = {}) {
|
|
59
170
|
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
171
|
const VERSION = JSON.parse(readFileSync(join(SKILLS_DIR, "..", "package.json"), "utf8")).version;
|
|
63
172
|
const scope = globalScope ? "global" : "project";
|
|
64
173
|
const root = globalScope ? homedir() : process.cwd();
|
|
65
174
|
const names = parsePlatforms(platformsArg);
|
|
66
175
|
const targets = [];
|
|
176
|
+
const seenDest = new Set();
|
|
177
|
+
const mergedDocs = new Set();
|
|
178
|
+
const guidance = readGuidanceSection();
|
|
67
179
|
|
|
68
180
|
if (dryRun) console.log(`--dry-run: would install agentmap skill (${scope} scope):`);
|
|
69
181
|
|
|
@@ -74,19 +186,24 @@ export function installSkill({ platforms: platformsArg = "all", project = true,
|
|
|
74
186
|
continue;
|
|
75
187
|
}
|
|
76
188
|
if (!existsSync(cfg.src)) throw new Error(`packaged skill missing: ${cfg.src}`);
|
|
77
|
-
const dest = cfg.dest(root);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
189
|
+
const dest = cfg.dest(root, globalScope);
|
|
190
|
+
const skipSkill = seenDest.has(dest);
|
|
191
|
+
if (skipSkill) {
|
|
192
|
+
console.log(` skip ${cfg.label} skill: same path as another platform (${dest})`);
|
|
193
|
+
} else {
|
|
194
|
+
seenDest.add(dest);
|
|
195
|
+
if (dryRun) {
|
|
196
|
+
console.log(` ${cfg.label} skill: ${dest}`);
|
|
197
|
+
} else {
|
|
198
|
+
atomicWrite(dest, readFileSync(cfg.src, "utf8"));
|
|
199
|
+
writeFileSync(join(dirname(dest), ".agentmap_version"), VERSION + "\n", "utf8");
|
|
200
|
+
console.log(` ${cfg.label} skill → ${dest}`);
|
|
201
|
+
targets.push(dest);
|
|
202
|
+
}
|
|
84
203
|
}
|
|
85
204
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
console.log(` ${cfg.label} → ${dest}`);
|
|
89
|
-
targets.push(dest);
|
|
205
|
+
installDocsForPlatform(cfg, { root, globalScope, dryRun, guidance, mergedDocs, targets });
|
|
206
|
+
installExtrasForPlatform(name, cfg, { root, globalScope, dryRun, targets });
|
|
90
207
|
}
|
|
91
208
|
|
|
92
209
|
if (!dryRun && targets.length) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenCode plugin — logs a reminder when bash/grep looks like a structural search.
|
|
3
|
+
// Always-on guidance lives in AGENTS.md (installed by --install-skill).
|
|
4
|
+
// Non-blocking: uses client.app.log when available.
|
|
5
|
+
|
|
6
|
+
const DEP_RE =
|
|
7
|
+
/\b(import|require\s*\(|imported\s+by|depends|dependents?|dependency)\b|from\s+["']|(^|\|)\s*export\b/i;
|
|
8
|
+
const INTENT_RE =
|
|
9
|
+
/\bwhere\s+is\b|\bwho\s+(imports|uses|renders)\b|\breuse\b|\b(existing|shared)\s+(util|component|hook|helper)\b/i;
|
|
10
|
+
const SYMBOL_RE = /\b[A-Z][a-z0-9]+[A-Z][A-Za-z0-9]*\b/;
|
|
11
|
+
|
|
12
|
+
function shouldNudge(tool, args) {
|
|
13
|
+
const pattern = String(args?.pattern || args?.query || "");
|
|
14
|
+
const cmd = String(args?.command || "");
|
|
15
|
+
if (tool === "grep" && pattern) return DEP_RE.test(pattern) || INTENT_RE.test(pattern);
|
|
16
|
+
if (tool === "bash" && cmd) {
|
|
17
|
+
if (!/(^|[;&]\s*)(rg|ripgrep|grep|egrep|fgrep|ag|ack)\b/.test(cmd)) return false;
|
|
18
|
+
return DEP_RE.test(cmd) || INTENT_RE.test(cmd) || SYMBOL_RE.test(cmd);
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const AgentmapNudge = async ({ client }) => {
|
|
24
|
+
return {
|
|
25
|
+
"tool.execute.before": async (input, output) => {
|
|
26
|
+
if (!shouldNudge(input.tool, output.args)) return;
|
|
27
|
+
const message =
|
|
28
|
+
"Structural search: prefer agentmap --any <query>, --relates <path>, or --find <symbol> before serial grep.";
|
|
29
|
+
try {
|
|
30
|
+
await client?.app?.log?.({
|
|
31
|
+
body: { service: "agentmap", level: "info", message },
|
|
32
|
+
});
|
|
33
|
+
} catch {
|
|
34
|
+
// advisory only
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
};
|