@raymondchins/agentmap 0.6.1 → 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 +39 -0
- package/agentmap.mjs +176 -20
- package/hooks/agentmap-gemini-nudge.mjs +71 -0
- package/package.json +2 -1
- package/skills/SKILL.md +71 -0
- package/skills/cursor-rule.mdc +27 -0
- package/skills/guidance.md +24 -0
- package/skills/install-helpers.mjs +117 -0
- package/skills/install.mjs +214 -0
- package/skills/opencode-agentmap-nudge.js +38 -0
package/README.md
CHANGED
|
@@ -185,6 +185,43 @@ 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, OpenCode, Gemini, Antigravity, Copilot)
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npx @raymondchins/agentmap --install-skill
|
|
192
|
+
```
|
|
193
|
+
|
|
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
|
+
|
|
198
|
+
```bash
|
|
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/
|
|
206
|
+
agentmap --install-skill --global --platform claude # ~/.claude/skills/...
|
|
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
|
|
209
|
+
```
|
|
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
|
+
|
|
223
|
+
Pair with `--install-hooks` (Claude Code) or `--mcp` (Cursor MCP).
|
|
224
|
+
|
|
188
225
|
---
|
|
189
226
|
|
|
190
227
|
## Quickstart
|
|
@@ -481,6 +518,8 @@ $ node agentmap.mjs --print | jq '.hubs[0]'
|
|
|
481
518
|
| `--version` / `-v` | Print the version from `package.json` and exit 0. |
|
|
482
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}`. |
|
|
483
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. |
|
|
521
|
+
| `--hook-status` | Report whether the post-commit hook, PreToolUse nudge, and `.gitignore` entry are installed (no writes). |
|
|
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). |
|
|
484
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. |
|
|
485
524
|
|
|
486
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()) {
|
|
@@ -917,6 +992,60 @@ function installHooks({ dryRun = false } = {}) {
|
|
|
917
992
|
console.log("\nDone — the map auto-refreshes on commit, and greps are nudged to agentmap first.");
|
|
918
993
|
}
|
|
919
994
|
|
|
995
|
+
// Marker string baked into hooks/post-commit — used by --hook-status to detect our
|
|
996
|
+
// hook even when chained with other tools in the same file.
|
|
997
|
+
const POST_COMMIT_MARKER = "agentmap — git post-commit hook";
|
|
998
|
+
const NUDGE_REL = ".claude/hooks/agentmap-nudge.mjs";
|
|
999
|
+
const MAP_IGNORE_LINE = ".claude/agentmap/";
|
|
1000
|
+
|
|
1001
|
+
function hookStatus() {
|
|
1002
|
+
const gitDir = sh("git rev-parse --git-dir");
|
|
1003
|
+
if (!gitDir) {
|
|
1004
|
+
console.log("not a git repository — run inside the repo you want to check");
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const hooksDir = `${gitDir}/hooks`;
|
|
1008
|
+
const postCommitPath = `${hooksDir}/post-commit`;
|
|
1009
|
+
|
|
1010
|
+
let postCommit = "not installed";
|
|
1011
|
+
if (existsSync(postCommitPath)) {
|
|
1012
|
+
const body = readFileSync(postCommitPath, "utf8");
|
|
1013
|
+
postCommit = body.includes(POST_COMMIT_MARKER) ? "installed" : "not installed (hook exists but agentmap not found)";
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
let nudge = existsSync(NUDGE_REL) ? "installed" : "not installed";
|
|
1017
|
+
|
|
1018
|
+
let grepWire = "not wired";
|
|
1019
|
+
let bashWire = "not wired";
|
|
1020
|
+
const settingsPath = ".claude/settings.json";
|
|
1021
|
+
if (existsSync(settingsPath)) {
|
|
1022
|
+
try {
|
|
1023
|
+
const settings = parseSettings(readFileSync(settingsPath, "utf8"), settingsPath);
|
|
1024
|
+
const entries = settings.hooks?.PreToolUse || [];
|
|
1025
|
+
const has = (matcher) => entries.some(
|
|
1026
|
+
(e) => e?.matcher === matcher && Array.isArray(e?.hooks) && e.hooks.some((h) => typeof h?.command === "string" && h.command.includes("agentmap-nudge")),
|
|
1027
|
+
);
|
|
1028
|
+
grepWire = has("Grep") ? "wired" : "not wired";
|
|
1029
|
+
bashWire = has("Bash") ? "wired" : "not wired";
|
|
1030
|
+
} catch {
|
|
1031
|
+
grepWire = "not wired (invalid settings.json)";
|
|
1032
|
+
bashWire = "not wired (invalid settings.json)";
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
let gitignore = "missing entry";
|
|
1037
|
+
if (existsSync(".gitignore")) {
|
|
1038
|
+
const ok = readFileSync(".gitignore", "utf8").split(/\r?\n/).some((l) => l.trim() === MAP_IGNORE_LINE);
|
|
1039
|
+
gitignore = ok ? "ok" : "missing entry";
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
console.log(`post-commit: ${postCommit}`);
|
|
1043
|
+
console.log(`nudge (${NUDGE_REL}): ${nudge}`);
|
|
1044
|
+
console.log(`PreToolUse(Grep): ${grepWire}`);
|
|
1045
|
+
console.log(`PreToolUse(Bash): ${bashWire}`);
|
|
1046
|
+
console.log(`.gitignore (${MAP_IGNORE_LINE}): ${gitignore}`);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
920
1049
|
// ---------------------------------------------------------------------------
|
|
921
1050
|
// --setup-mcp: register the agentmap MCP server in the global configs of
|
|
922
1051
|
// MCP-capable IDEs that aren't Claude Code (which uses --install-hooks instead).
|
|
@@ -998,7 +1127,8 @@ const out = (obj, prose) => { if (wantJson) console.log(JSON.stringify(obj)); el
|
|
|
998
1127
|
// NOT in this set is an unknown flag → usage error (exit 2), not a silent build.
|
|
999
1128
|
const KNOWN = new Set([
|
|
1000
1129
|
"--json", "--print",
|
|
1001
|
-
"--help", "-h", "--version", "-v", "--install-hooks", "--
|
|
1130
|
+
"--help", "-h", "--version", "-v", "--install-hooks", "--hook-status", "--install-skill", "--platform", "--project", "--global",
|
|
1131
|
+
"--dry-run", "--setup-mcp", "--mcp",
|
|
1002
1132
|
"--any", "--find", "--relates", "--map", "--focus", "--tokens",
|
|
1003
1133
|
"--symbols", "--feature", "--features", "--hubs",
|
|
1004
1134
|
]);
|
|
@@ -1007,7 +1137,7 @@ const KNOWN = new Set([
|
|
|
1007
1137
|
// so a dash-leading query like `--any "-O/bin/sh"` is bound as the query, not
|
|
1008
1138
|
// mistaken for an unknown flag. (arg() already rejects a "--"-leading value, so
|
|
1009
1139
|
// `--any --foo` still falls through to the missing-arg guard instead.)
|
|
1010
|
-
const VALUE_FLAGS = new Set(["--any", "--find", "--relates", "--feature", "--focus", "--tokens", "--symbols"]);
|
|
1140
|
+
const VALUE_FLAGS = new Set(["--any", "--find", "--relates", "--feature", "--focus", "--tokens", "--symbols", "--platform"]);
|
|
1011
1141
|
const valueIdx = new Set();
|
|
1012
1142
|
for (let i = 0; i < args.length - 1; i++) if (VALUE_FLAGS.has(args[i])) valueIdx.add(i + 1);
|
|
1013
1143
|
|
|
@@ -1035,6 +1165,9 @@ Maintenance:
|
|
|
1035
1165
|
--install-hooks [--dry-run]
|
|
1036
1166
|
install git post-commit + copy the PreToolUse nudge +
|
|
1037
1167
|
wire .claude/settings.json (--dry-run = preview, no writes)
|
|
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
|
|
1170
|
+
--hook-status report whether agentmap git/nudge wiring is installed
|
|
1038
1171
|
--setup-mcp [--dry-run]
|
|
1039
1172
|
configure MCP server for OpenCode & Antigravity IDE
|
|
1040
1173
|
(--dry-run = preview, no writes)
|
|
@@ -1073,6 +1206,29 @@ else if (has("--install-hooks")) {
|
|
|
1073
1206
|
try { installHooks({ dryRun: has("--dry-run") }); process.exit(0); }
|
|
1074
1207
|
catch (e) { console.error(`agentmap --install-hooks failed: ${e?.message || e}`); process.exit(1); }
|
|
1075
1208
|
}
|
|
1209
|
+
// --install-skill: copy packaged SKILL.md / Cursor rule (see skills/install.mjs).
|
|
1210
|
+
else if (has("--install-skill")) {
|
|
1211
|
+
try {
|
|
1212
|
+
// lazy import keeps skills/install.mjs (and its package.json read) OFF the
|
|
1213
|
+
// hot path — warm --any/--find queries must not load it.
|
|
1214
|
+
const { installSkill } = await import("./skills/install.mjs");
|
|
1215
|
+
installSkill({
|
|
1216
|
+
platforms: arg("--platform") || "all",
|
|
1217
|
+
project: !has("--global"),
|
|
1218
|
+
global: has("--global"),
|
|
1219
|
+
dryRun: has("--dry-run"),
|
|
1220
|
+
});
|
|
1221
|
+
process.exit(0);
|
|
1222
|
+
} catch (e) {
|
|
1223
|
+
console.error(`agentmap --install-skill failed: ${e?.message || e}`);
|
|
1224
|
+
process.exit(1);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
// --hook-status: report post-commit / nudge / settings wiring (no writes).
|
|
1228
|
+
else if (has("--hook-status")) {
|
|
1229
|
+
try { hookStatus(); process.exit(0); }
|
|
1230
|
+
catch (e) { console.error(`agentmap --hook-status failed: ${e?.message || e}`); process.exit(1); }
|
|
1231
|
+
}
|
|
1076
1232
|
// --setup-mcp: configure MCP server for OpenCode & Antigravity IDE.
|
|
1077
1233
|
else if (has("--setup-mcp")) {
|
|
1078
1234
|
try { setupMcp({ dryRun: has("--dry-run") }); process.exit(0); }
|
|
@@ -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
package/skills/SKILL.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentmap
|
|
3
|
+
description: >-
|
|
4
|
+
Use for TypeScript/JavaScript codebase navigation — symbol lookup, blast radius,
|
|
5
|
+
reuse checks, and repo orientation. Prefer agentmap before serial grep when
|
|
6
|
+
exploring imports, exports, or where a symbol lives. Package is
|
|
7
|
+
@raymondchins/agentmap (not the unrelated npm package agentmap).
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# agentmap
|
|
11
|
+
|
|
12
|
+
Queryable, ranked **import graph** for TS/JS repos (PageRank hubs, symbol ranking, token-budgeted digest). Faster and more accurate than grep for structural questions.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- **Where is X defined?** / **who imports this file?** / **what breaks if I edit this?**
|
|
17
|
+
- **Reuse check** before adding a util, component, or type
|
|
18
|
+
- **Session start** — orient to a large monorepo cheaply
|
|
19
|
+
|
|
20
|
+
## When not to use
|
|
21
|
+
|
|
22
|
+
- Raw string / config value search (try `agentmap --any` first — layer 4 is live `git grep`)
|
|
23
|
+
- Non-TS/JS files, runtime call graphs, or full semantic "how does it work?" (use codebase search)
|
|
24
|
+
- Next-style `--feature` on TanStack Router / non-`app/` layouts (often empty)
|
|
25
|
+
|
|
26
|
+
## Commands (run in repo root)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Smart router — default first move
|
|
30
|
+
agentmap --any <query>
|
|
31
|
+
|
|
32
|
+
# Reuse / symbol definition
|
|
33
|
+
agentmap --find <SymbolName>
|
|
34
|
+
|
|
35
|
+
# Blast radius (exports, imports, dependents, related files)
|
|
36
|
+
agentmap --relates <path/to/file.ts>
|
|
37
|
+
|
|
38
|
+
# Token-budgeted ranked digest
|
|
39
|
+
agentmap --map --tokens 400
|
|
40
|
+
agentmap --map --focus <path> --tokens 400
|
|
41
|
+
|
|
42
|
+
# Hub files (PageRank)
|
|
43
|
+
agentmap --hubs
|
|
44
|
+
|
|
45
|
+
# JSON for tools
|
|
46
|
+
agentmap --json --any <query>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Install: `npm i -g @raymondchins/agentmap` or `npx @raymondchins/agentmap`. Map cache: `.claude/agentmap/` (gitignored).
|
|
50
|
+
|
|
51
|
+
## Agent platforms
|
|
52
|
+
|
|
53
|
+
| Platform | Setup |
|
|
54
|
+
|----------|--------|
|
|
55
|
+
| **Claude Code** | `agentmap --install-hooks` (post-commit refresh + PreToolUse grep nudge) |
|
|
56
|
+
| **Cursor / MCP clients** | `agentmap --mcp` in MCP config, or Shell + commands above |
|
|
57
|
+
| **This skill** | `agentmap --install-skill` |
|
|
58
|
+
|
|
59
|
+
## Workflow
|
|
60
|
+
|
|
61
|
+
1. If the map may be stale after edits, run `agentmap` (no flags) or rely on post-commit hook.
|
|
62
|
+
2. Start with `agentmap --any <symbol or topic>`.
|
|
63
|
+
3. Before editing a hub file, run `agentmap --relates <that-file>`.
|
|
64
|
+
4. Fall back to grep only when agentmap returns no useful structure hit.
|
|
65
|
+
|
|
66
|
+
## Package name
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx @raymondchins/agentmap --any Procedure
|
|
70
|
+
# NOT: npm install agentmap (different package)
|
|
71
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: agentmap ranked TS/JS repo map
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
This project can use **agentmap** (`@raymondchins/agentmap`) — a queryable import/symbol map at `.claude/agentmap/`.
|
|
7
|
+
|
|
8
|
+
**Before Grep/Glob for structural questions** (where is a symbol, who imports a file, reuse check, blast radius), prefer:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
agentmap --any <query> # file → symbol → feature → git-grep fallback
|
|
12
|
+
agentmap --find <Symbol> # exported symbols matching name
|
|
13
|
+
agentmap --relates <file.ts> # imports, dependents, related files
|
|
14
|
+
agentmap --map --tokens 400 # cheap repo orientation
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Use Read/Grep directly when:
|
|
18
|
+
|
|
19
|
+
1. agentmap already oriented you and you need exact lines to edit
|
|
20
|
+
2. The map is missing — run `agentmap` once to build `.claude/agentmap/`
|
|
21
|
+
3. Searching raw strings, logs, or non-TS files
|
|
22
|
+
|
|
23
|
+
**Cursor MCP (optional):** `agentmap --mcp` exposes the same queries as tools.
|
|
24
|
+
|
|
25
|
+
**Note:** `--features` only detects Next.js `app/` routes; TanStack `src/routes/` repos often show `features (0)`.
|
|
26
|
+
|
|
27
|
+
Package: `@raymondchins/agentmap` — not the unrelated PyPI/npm `agentmap` package.
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// --install-skill: skill files + always-on docs/hooks per platform (project or global).
|
|
3
|
+
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { homedir, platform as osPlatform } from "node:os";
|
|
6
|
+
import { join, dirname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import {
|
|
9
|
+
atomicWrite,
|
|
10
|
+
readGuidanceSection,
|
|
11
|
+
mergeGuidanceBlock,
|
|
12
|
+
installGeminiHooks,
|
|
13
|
+
installOpencodePlugin,
|
|
14
|
+
} from "./install-helpers.mjs";
|
|
15
|
+
|
|
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");
|
|
19
|
+
|
|
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
|
+
* }>} */
|
|
35
|
+
const PLATFORMS = {
|
|
36
|
+
claude: {
|
|
37
|
+
label: "Claude Code",
|
|
38
|
+
src: SKILL_MD,
|
|
39
|
+
dest: (root) => skillPath(root, false, ".claude", "skills", "agentmap", "SKILL.md"),
|
|
40
|
+
},
|
|
41
|
+
cursor: {
|
|
42
|
+
label: "Cursor",
|
|
43
|
+
src: CURSOR_RULE,
|
|
44
|
+
dest: (root) => skillPath(root, false, ".cursor", "rules", "agentmap.mdc"),
|
|
45
|
+
projectOnly: true,
|
|
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
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const DEFAULT_PLATFORMS = ["claude", "cursor", "codex", "opencode", "gemini", "antigravity", "copilot"];
|
|
102
|
+
|
|
103
|
+
function parsePlatforms(raw) {
|
|
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) {
|
|
115
|
+
if (!PLATFORMS[n]) {
|
|
116
|
+
throw new Error(`unknown platform '${n}' — choose: ${keys}, all`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return tokens;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function gitAddHint(paths) {
|
|
123
|
+
const unique = [...new Set(paths.map((p) => p.replace(/\/[^/]+$/, "") || p))];
|
|
124
|
+
if (unique.length) console.log(`\nOptional: git add ${unique.map((p) => `"${p}"`).join(" ")}`);
|
|
125
|
+
}
|
|
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
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {{ platforms?: string; project?: boolean; global?: boolean; dryRun?: boolean }} opts
|
|
168
|
+
*/
|
|
169
|
+
export function installSkill({ platforms: platformsArg = "all", project = true, global: globalScope = false, dryRun = false } = {}) {
|
|
170
|
+
if (project && globalScope) throw new Error("use either --project or --global, not both");
|
|
171
|
+
const VERSION = JSON.parse(readFileSync(join(SKILLS_DIR, "..", "package.json"), "utf8")).version;
|
|
172
|
+
const scope = globalScope ? "global" : "project";
|
|
173
|
+
const root = globalScope ? homedir() : process.cwd();
|
|
174
|
+
const names = parsePlatforms(platformsArg);
|
|
175
|
+
const targets = [];
|
|
176
|
+
const seenDest = new Set();
|
|
177
|
+
const mergedDocs = new Set();
|
|
178
|
+
const guidance = readGuidanceSection();
|
|
179
|
+
|
|
180
|
+
if (dryRun) console.log(`--dry-run: would install agentmap skill (${scope} scope):`);
|
|
181
|
+
|
|
182
|
+
for (const name of names) {
|
|
183
|
+
const cfg = PLATFORMS[name];
|
|
184
|
+
if (cfg.projectOnly && globalScope) {
|
|
185
|
+
console.log(` skip ${cfg.label}: Cursor rule is project-scoped only (re-run with --project)`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (!existsSync(cfg.src)) throw new Error(`packaged skill missing: ${cfg.src}`);
|
|
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
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
installDocsForPlatform(cfg, { root, globalScope, dryRun, guidance, mergedDocs, targets });
|
|
206
|
+
installExtrasForPlatform(name, cfg, { root, globalScope, dryRun, targets });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!dryRun && targets.length) {
|
|
210
|
+
console.log(`\nagentmap --install-skill: installed ${targets.length} file(s) (${scope}).`);
|
|
211
|
+
if (!globalScope) gitAddHint(targets);
|
|
212
|
+
console.log("Pair with: agentmap --install-hooks (Claude Code) or agentmap --mcp (Cursor MCP).");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -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
|
+
};
|