@raymondchins/agentmap 0.1.0 → 0.2.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.
@@ -0,0 +1,211 @@
1
+ # agentmap — agent-loop install
2
+
3
+ The differentiator over "pack the repo into a prompt" tools is **agent-loop
4
+ integration**: the map stays fresh on its own (git `post-commit`) and the agent
5
+ is *nudged* to use it instead of serial grep (Claude Code `PreToolUse` hook).
6
+ Two small, dependency-free files wire both up.
7
+
8
+ ```
9
+ hooks/
10
+ agentmap-nudge.mjs PreToolUse(Grep) nudge → "use agentmap --any first"
11
+ post-commit git hook → rebuilds .claude/agentmap.json after each commit
12
+ INSTALL.md ← you are here
13
+ ```
14
+
15
+ Both are pure Node/POSIX sh stdlib. The only runtime dependency is `agentmap`
16
+ itself (`ts-morph`), used when the map (re)builds.
17
+
18
+ ---
19
+
20
+ ## 0. Prerequisites
21
+
22
+ - **Node 18+** on PATH.
23
+ - **agentmap available in the repo.** Either:
24
+ - drop `agentmap.mjs` at the repo root (or `scripts/agentmap.mjs`), or
25
+ - install it: `npm i -D @raymondchins/agentmap` (then `npx @raymondchins/agentmap` works), or
26
+ - install it globally: `npm i -g @raymondchins/agentmap` (then `agentmap` works).
27
+ - The repo must have a `tsconfig.json` (agentmap reads it to find source files).
28
+
29
+ **Caveats:**
30
+
31
+ - **git hooks run under a non-login shell.** If you manage Node via `nvm`,
32
+ `nvm` won't be sourced and `node` may not be on PATH inside the hook. Use
33
+ a system-level Node install (or Volta / Corepack) so the hook can find
34
+ `node` without shell profile sourcing. Add an explicit `export PATH=...`
35
+ line at the top of the hook if needed.
36
+ - **Windows:** Git for Windows runs hooks under its bundled `sh`, not bash.
37
+ The hook script is POSIX sh — do **not** use bash-specific syntax if you
38
+ customise it.
39
+
40
+ Smoke-test it builds:
41
+
42
+ ```bash
43
+ node agentmap.mjs # or: npx @raymondchins/agentmap
44
+ # → agentmap: N files | M features | top hub: ...
45
+ ```
46
+
47
+ This writes `.claude/agentmap.json`. Add it to `.gitignore` (it's a derived
48
+ artifact, rebuilt on every commit):
49
+
50
+ ```bash
51
+ echo ".claude/agentmap.json" >> .gitignore
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 1. PreToolUse nudge (Claude Code)
57
+
58
+ Steers `who-imports` / dependency / reuse / `<Component>` greps toward
59
+ `agentmap --any` before the agent fans out into serial grep. **Non-blocking** —
60
+ it only injects a reminder, never denies the Grep.
61
+
62
+ ### a. Place the hook script
63
+
64
+ Keep it in the repo so it's version-controlled and path-stable:
65
+
66
+ ```bash
67
+ mkdir -p .claude/hooks
68
+ cp hooks/agentmap-nudge.mjs .claude/hooks/agentmap-nudge.mjs
69
+ ```
70
+
71
+ ### b. Register it in `.claude/settings.json`
72
+
73
+ Add (or merge) this `hooks` block. The matcher `Grep` runs the hook before
74
+ every Grep tool call; the script decides whether to nudge.
75
+
76
+ ```json
77
+ {
78
+ "hooks": {
79
+ "PreToolUse": [
80
+ {
81
+ "matcher": "Grep",
82
+ "hooks": [
83
+ {
84
+ "type": "command",
85
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/agentmap-nudge.mjs\""
86
+ }
87
+ ]
88
+ }
89
+ ]
90
+ }
91
+ }
92
+ ```
93
+
94
+ `$CLAUDE_PROJECT_DIR` is set by Claude Code to the project root, so the path
95
+ resolves no matter the agent's cwd. Restart the session (or `/hooks` →
96
+ reload) to pick it up.
97
+
98
+ ### c. Verify
99
+
100
+ ```bash
101
+ echo '{"tool_input":{"pattern":"<Heading"}}' | node .claude/hooks/agentmap-nudge.mjs
102
+ # → {"hookSpecificOutput":{...,"additionalContext":"This Grep looks like ..."}}
103
+
104
+ echo '{"tool_input":{"pattern":"bg-white"}}' | node .claude/hooks/agentmap-nudge.mjs
105
+ # → (no output — raw-string sweeps are left alone)
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 2. post-commit auto-refresh (git)
111
+
112
+ Rebuilds the map after each commit so the agent never reads a stale map.
113
+ Runs detached + silenced (commit returns instantly) and **skips during
114
+ rebase / merge / cherry-pick / bisect / revert** so it doesn't fire on every
115
+ replayed commit.
116
+
117
+ **Easiest way — use the built-in installer flag:**
118
+
119
+ ```bash
120
+ agentmap --install-hooks
121
+ ```
122
+
123
+ This copies `hooks/post-commit` to `.git/hooks/post-commit`, chmods it, ensures
124
+ `.claude/agentmap.json` is in `.gitignore`, and prints the Claude Code
125
+ `settings.json` PreToolUse snippet — all in one step.
126
+
127
+ **Manual alternative:**
128
+
129
+ ```bash
130
+ cp hooks/post-commit .git/hooks/post-commit
131
+ chmod +x .git/hooks/post-commit
132
+ ```
133
+
134
+ It auto-locates the builder: `./agentmap.mjs` → `./scripts/agentmap.mjs` →
135
+ global `agentmap` → `npx --no-install @raymondchins/agentmap`. If none is found it no-ops.
136
+
137
+ ### Verify
138
+
139
+ ```bash
140
+ git commit --allow-empty -m "test: agentmap post-commit"
141
+ # wait a moment for the background rebuild, then:
142
+ git rev-parse --short HEAD
143
+ node -e "console.log(require('./.claude/agentmap.json').generatedSha)"
144
+ # the two SHAs should match
145
+ ```
146
+
147
+ > **Husky / shared hooks:** if the repo uses Husky or `core.hooksPath`, append
148
+ > the body of `hooks/post-commit` to your existing `post-commit` (e.g.
149
+ > `.husky/post-commit`) instead of overwriting `.git/hooks/post-commit`.
150
+
151
+ ---
152
+
153
+ ## 3. One-liner installer (idea)
154
+
155
+ Drop this as `hooks/install.sh` in your repo (or run inline) to wire both at
156
+ once from the repo root:
157
+
158
+ ```sh
159
+ #!/usr/bin/env sh
160
+ set -eu
161
+ ROOT="$(git rev-parse --show-toplevel)"
162
+ HOOKS="$ROOT/hooks" # where these files live in your repo
163
+
164
+ # PreToolUse nudge
165
+ mkdir -p "$ROOT/.claude/hooks"
166
+ cp "$HOOKS/agentmap-nudge.mjs" "$ROOT/.claude/hooks/agentmap-nudge.mjs"
167
+
168
+ # Merge the PreToolUse(Grep) hook into .claude/settings.json (needs jq).
169
+ SETTINGS="$ROOT/.claude/settings.json"
170
+ [ -f "$SETTINGS" ] || echo '{}' > "$SETTINGS"
171
+ CMD='node "$CLAUDE_PROJECT_DIR/.claude/hooks/agentmap-nudge.mjs"'
172
+ jq --arg cmd "$CMD" '
173
+ .hooks.PreToolUse = ((.hooks.PreToolUse // []) + [{
174
+ matcher: "Grep",
175
+ hooks: [{ type: "command", command: $cmd }]
176
+ }])
177
+ ' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
178
+
179
+ # git post-commit auto-refresh (skip if Husky/core.hooksPath is in use)
180
+ cp "$HOOKS/post-commit" "$ROOT/.git/hooks/post-commit"
181
+ chmod +x "$ROOT/.git/hooks/post-commit"
182
+
183
+ # Ignore the derived map + first build
184
+ grep -qxF ".claude/agentmap.json" "$ROOT/.gitignore" 2>/dev/null \
185
+ || echo ".claude/agentmap.json" >> "$ROOT/.gitignore"
186
+ ( cd "$ROOT" && { node agentmap.mjs || npx @raymondchins/agentmap; } ) || true
187
+
188
+ echo "agentmap wired: PreToolUse nudge + post-commit refresh installed."
189
+ ```
190
+
191
+ Run it:
192
+
193
+ ```bash
194
+ sh hooks/install.sh
195
+ ```
196
+
197
+ (The `jq` merge is idempotent-ish but appends — run once. Without `jq`, paste
198
+ the snippet from step 1b by hand.)
199
+
200
+ ---
201
+
202
+ ## How they reinforce each other
203
+
204
+ 1. You commit → **post-commit** rebuilds `.claude/agentmap.json` in the
205
+ background → the map is always current.
206
+ 2. The agent reaches for a who-imports / reuse / `<Component>` grep →
207
+ **PreToolUse nudge** fires → the agent runs `agentmap --any <query>` and
208
+ reads the fresh map instead of a slow serial grep.
209
+
210
+ That loop — fresh map + enforced usage — is the part a static "repo digest"
211
+ tool can't reproduce.
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: MIT
3
+ // ============================================================================
4
+ // agentmap — PreToolUse(Grep) nudge hook
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.
11
+ //
12
+ // Heuristic: fires when the grep PATTERN looks like (a) a dependency hunt
13
+ // (import/require/export / "from '..." / who-imports), (b) a component /
14
+ // "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.
18
+ //
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.
22
+ // ============================================================================
23
+
24
+ let raw = "";
25
+ process.stdin.setEncoding("utf8");
26
+ process.stdin.on("data", (c) => (raw += c));
27
+ process.stdin.on("end", () => {
28
+ try {
29
+ const payload = JSON.parse(raw || "{}");
30
+ 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
+
39
+ // (a) Dependency / who-imports / reuse intent signals in the pattern itself.
40
+ const DEP_RE =
41
+ /\b(import|require\s*\(|imported\s+by|depends|dependents?|dependency)\b|from\s+["']|(^|\|)\s*export\b/i;
42
+
43
+ // (b) JSX component open tag in the pattern (PascalCase, e.g. <Heading, <Hero,
44
+ // <Motion.div). CASE-SENSITIVE on purpose (NO /i flag) — PascalCase-only keeps
45
+ // raw HTML/content sweeps of <div>/<h1> silent, so it stays high-signal for
46
+ // "where is this component used/defined".
47
+ //
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.
51
+ const GENERIC_DENYLIST =
52
+ /^<(Promise|Array|Map|Set|Record|Partial|Readonly|Pick|Omit|Required|Exclude|Extract|NonNullable|ReturnType|Awaited|Parameters|InstanceType)\b/;
53
+ const COMPONENT_TAG_RE = /<[A-Z][\w.]*/;
54
+
55
+ // (c) Explicit reuse / "where-is" intent words (case-insensitive): "where is",
56
+ // "who imports/uses/renders", "reuse", "existing/shared util|component|hook|helper",
57
+ // "is there an existing/shared".
58
+ const INTENT_RE =
59
+ /\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
+
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
+ ) {
67
+ 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. " +
76
+ "Only fall back to grep if agentmap doesn't cover it.";
77
+ process.stdout.write(
78
+ JSON.stringify({
79
+ hookSpecificOutput: {
80
+ hookEventName: "PreToolUse",
81
+ additionalContext: msg,
82
+ },
83
+ }),
84
+ );
85
+ }
86
+ } catch {
87
+ // Never block on parse/other errors — stay silent.
88
+ }
89
+ process.exit(0);
90
+ });
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env sh
2
+ # ============================================================================
3
+ # agentmap — git post-commit hook
4
+ #
5
+ # Rebuilds .claude/agentmap.json after each commit so the map an agent reads is
6
+ # never stale. Runs in the background and detached so it never slows the commit.
7
+ #
8
+ # Guards:
9
+ # - Skips during rebase / merge / cherry-pick / bisect (avoids rebuilding on
10
+ # every replayed commit — the map rebuilds once the operation finishes and
11
+ # you commit normally).
12
+ # - No-ops cleanly if Node or agentmap.mjs is missing.
13
+ # - nvm caveat: git hooks run in a non-login shell that does not source nvm
14
+ # (or ~/.bashrc / ~/.zshrc), so `node` may be absent on PATH. The
15
+ # `command -v node || exit 0` guard below no-ops cleanly in that case.
16
+ #
17
+ # Install: copy to .git/hooks/post-commit and `chmod +x` it (see hooks/INSTALL.md).
18
+ # ============================================================================
19
+
20
+ # Resolve the repo root from the hook's own location (.git/hooks/post-commit).
21
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
22
+ GITDIR="$(git rev-parse --git-dir 2>/dev/null)" || exit 0
23
+
24
+ # Guard: don't rebuild while a multi-commit operation is replaying commits.
25
+ for state in rebase-merge rebase-apply MERGE_HEAD CHERRY_PICK_HEAD BISECT_LOG REVERT_HEAD; do
26
+ if [ -e "$GITDIR/$state" ]; then
27
+ exit 0
28
+ fi
29
+ done
30
+
31
+ # Locate the builder: prefer a local agentmap.mjs, else the installed `agentmap`.
32
+ # We cd "$ROOT" before running, so use RELATIVE paths — avoids word-splitting on
33
+ # spaces in the repo path (POSIX sh has no arrays; quoting $RUNNER at invocation
34
+ # would bundle cmd+args into one token and break argument passing).
35
+ RUNNER=""
36
+ if [ -f "$ROOT/agentmap.mjs" ]; then
37
+ RUNNER="node ./agentmap.mjs"
38
+ elif [ -f "$ROOT/scripts/agentmap.mjs" ]; then
39
+ RUNNER="node ./scripts/agentmap.mjs"
40
+ elif command -v agentmap >/dev/null 2>&1; then
41
+ # Bare binary name — the installed binary is named `agentmap` (no scope).
42
+ RUNNER="agentmap"
43
+ elif command -v npx >/dev/null 2>&1; then
44
+ # Fetch form MUST use the scoped package name to avoid the unrelated agentmap@0.11.0.
45
+ RUNNER="npx --no-install @raymondchins/agentmap"
46
+ else
47
+ exit 0
48
+ fi
49
+
50
+ # Need Node for the .mjs paths; nvm users may not have it in hook PATH — no-op cleanly.
51
+ case "$RUNNER" in
52
+ node*) command -v node >/dev/null 2>&1 || exit 0 ;;
53
+ esac
54
+
55
+ # Rebuild detached + silenced so the commit returns instantly.
56
+ (
57
+ cd "$ROOT" || exit 0
58
+ $RUNNER >/dev/null 2>&1
59
+ ) &
60
+
61
+ exit 0
package/mcp.mjs ADDED
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: MIT
3
+ // ============================================================================
4
+ // agentmap — stdio MCP (Model Context Protocol) server.
5
+ //
6
+ // Exposes the agentmap repo-map as first-class MCP tools so any MCP client
7
+ // (Cursor, Cline, Claude Desktop, …) can query a TS/JS codebase. Launched by
8
+ // `agentmap --mcp` (agentmap.mjs dynamically imports this file + calls serve()).
9
+ //
10
+ // Transport: JSON-RPC 2.0 over newline-delimited JSON on stdin/stdout — the
11
+ // simplest MCP stdio transport (one JSON object per line each way).
12
+ //
13
+ // Each tool is implemented by SPAWNING the agentmap CLI in `--json` mode
14
+ // (`node agentmap.mjs --json <flag> <args…>`), capturing its single-object
15
+ // stdout, and returning it verbatim. This file depends ONLY on the documented
16
+ // --json CLI surface — never on agentmap.mjs internals. Node stdlib only.
17
+ // ============================================================================
18
+ import { execFile } from "node:child_process";
19
+ import { readFileSync } from "node:fs";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ const PROTOCOL_VERSION = "2024-11-05";
23
+ // agentmap.mjs lives next to this file; run it as a subprocess for every tool.
24
+ const AGENTMAP = fileURLToPath(new URL("./agentmap.mjs", import.meta.url));
25
+
26
+ // Server version = package.json version (resolve relative to this file, not cwd).
27
+ function pkgVersion() {
28
+ try {
29
+ const p = fileURLToPath(new URL("./package.json", import.meta.url));
30
+ return JSON.parse(readFileSync(p, "utf8")).version || "0.0.0";
31
+ } catch { return "0.0.0"; }
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Tool registry. Each entry: an MCP inputSchema + a fn mapping the call's
36
+ // args → the agentmap CLI argv (always `--json` first so stdout is one object).
37
+ // ---------------------------------------------------------------------------
38
+ const str = (description) => ({ type: "string", description });
39
+ const TOOLS = [
40
+ {
41
+ name: "any",
42
+ description:
43
+ "Unified router: resolve a query against the repo map (file → symbol → feature) then fall back to a live git-grep for string/copy/data literals. Best default for 'where is X' / reuse-before-rebuild.",
44
+ inputSchema: { type: "object", properties: { query: str("File path, symbol name, feature name, or any literal string to search for.") }, required: ["query"] },
45
+ argv: (a) => ["--any", String(a.query ?? "")],
46
+ },
47
+ {
48
+ name: "find",
49
+ description: "Find every exported symbol whose name matches (substring, case-insensitive). Use to locate a function/class/type before rebuilding it.",
50
+ inputSchema: { type: "object", properties: { symbol: str("Symbol name or substring to match against exports.") }, required: ["symbol"] },
51
+ argv: (a) => ["--find", String(a.symbol ?? "")],
52
+ },
53
+ {
54
+ name: "relates",
55
+ description: "Blast radius for a file: its exports, imports, direct dependents, and the files most related to it by random-walk relevance. Use before editing to see who breaks.",
56
+ inputSchema: { type: "object", properties: { path: str("File path, basename, or unique substring identifying the target file.") }, required: ["path"] },
57
+ argv: (a) => ["--relates", String(a.path ?? "")],
58
+ },
59
+ {
60
+ name: "map",
61
+ description: "Token-budgeted ranked digest of the codebase (PageRank + Aider-style symbol ranking). Optionally focus toward a file and/or set a token budget.",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ focus: str("Optional file path/substring to personalize the ranking toward."),
66
+ tokens: { type: "integer", description: "Optional token budget for the digest (default 8192 global / 1024 focused)." },
67
+ },
68
+ },
69
+ // --map takes optional --focus and --tokens; only pass what's provided.
70
+ argv: (a) => {
71
+ const out = ["--map"];
72
+ if (a.focus != null && String(a.focus) !== "") out.push("--focus", String(a.focus));
73
+ if (a.tokens != null && Number.isFinite(Number(a.tokens))) out.push("--tokens", String(Math.trunc(Number(a.tokens))));
74
+ return out;
75
+ },
76
+ },
77
+ {
78
+ name: "hubs",
79
+ description: "List the most important files in the repo by PageRank (the hubs everything imports). Read these first to understand a codebase.",
80
+ inputSchema: { type: "object", properties: {} },
81
+ argv: () => ["--hubs"],
82
+ },
83
+ {
84
+ name: "features",
85
+ description: "List every detected feature (top-level app/ route segment) with its file count.",
86
+ inputSchema: { type: "object", properties: {} },
87
+ argv: () => ["--features"],
88
+ },
89
+ {
90
+ name: "feature",
91
+ description: "List all files belonging to a named feature plus its external dependents.",
92
+ inputSchema: { type: "object", properties: { name: str("Feature name (run the 'features' tool to list them).") }, required: ["name"] },
93
+ argv: (a) => ["--feature", String(a.name ?? "")],
94
+ },
95
+ {
96
+ name: "symbols",
97
+ description: "Top N globally ranked symbols (Aider-style importance). Defaults to 30.",
98
+ inputSchema: { type: "object", properties: { n: { type: "integer", description: "How many symbols to return (default 30)." } } },
99
+ // --symbols takes an optional positional count.
100
+ argv: (a) => (a.n != null && Number.isFinite(Number(a.n)) ? ["--symbols", String(Math.trunc(Number(a.n)))] : ["--symbols"]),
101
+ },
102
+ ];
103
+ const TOOL_BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
104
+ // MCP tools/list wants only the public fields (no internal argv builder).
105
+ const toolList = () => TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
106
+
107
+ // Spawn `node agentmap.mjs --json <flag> <args…>` in the client's cwd, resolve
108
+ // with stdout. Rejects (with stderr/message) only on spawn failure; a non-zero
109
+ // exit still resolves so the dispatcher can surface stdout/stderr as isError.
110
+ function runAgentmap(extraArgv) {
111
+ return new Promise((resolve) => {
112
+ execFile(
113
+ process.execPath,
114
+ [AGENTMAP, "--json", ...extraArgv],
115
+ { cwd: process.cwd(), maxBuffer: 64 * 1024 * 1024, windowsHide: true },
116
+ (err, stdout, stderr) => {
117
+ const code = err && typeof err.code === "number" ? err.code : err ? 1 : 0;
118
+ resolve({ code, stdout: (stdout || "").trim(), stderr: (stderr || "").trim(), spawnError: err && err.code === undefined ? err.message : "" });
119
+ },
120
+ );
121
+ });
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // JSON-RPC plumbing. Write one compact JSON object per line to stdout.
126
+ // ---------------------------------------------------------------------------
127
+ function send(obj) { process.stdout.write(JSON.stringify(obj) + "\n"); }
128
+ const result = (id, r) => send({ jsonrpc: "2.0", id, result: r });
129
+ const error = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
130
+
131
+ // Dispatch a tools/call: build argv, run the CLI, wrap stdout as MCP content.
132
+ async function callTool(name, rawArgs) {
133
+ const tool = TOOL_BY_NAME.get(name);
134
+ if (!tool) return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
135
+ const argv = tool.argv(rawArgs && typeof rawArgs === "object" ? rawArgs : {});
136
+ const { code, stdout, stderr, spawnError } = await runAgentmap(argv);
137
+ if (spawnError) return { content: [{ type: "text", text: `failed to launch agentmap: ${spawnError}` }], isError: true };
138
+ // Exit 1 = query returned zero results (a valid answer, not a tool failure):
139
+ // surface stdout when present, else a friendly empty note. Exit ≥2 = real
140
+ // error → mark isError and prefer stderr.
141
+ if (code >= 2) return { content: [{ type: "text", text: stderr || stdout || `agentmap exited ${code}` }], isError: true };
142
+ const text = stdout || (code === 1 ? `no results` : stderr) || "";
143
+ return { content: [{ type: "text", text }] };
144
+ }
145
+
146
+ // Handle one parsed JSON-RPC request object. Notifications (no `id`) get no
147
+ // reply per the spec; everything else returns a result or an error.
148
+ async function handle(msg) {
149
+ const { id, method, params } = msg || {};
150
+ const isNotification = id === undefined || id === null;
151
+ switch (method) {
152
+ case "initialize":
153
+ return result(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: { name: "agentmap", version: pkgVersion() } });
154
+ case "notifications/initialized":
155
+ case "initialized":
156
+ return; // notification — no response
157
+ case "ping":
158
+ return result(id, {});
159
+ case "tools/list":
160
+ return result(id, { tools: toolList() });
161
+ case "tools/call": {
162
+ const name = params && params.name;
163
+ const out = await callTool(name, params && params.arguments);
164
+ return result(id, out);
165
+ }
166
+ default:
167
+ if (isNotification) return; // ignore unknown notifications silently
168
+ return error(id, -32601, `method not found: ${method}`);
169
+ }
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // serve() — read newline-delimited JSON from stdin, dispatch each complete
174
+ // line. Never crash on a malformed line: reply with a JSON-RPC parse error
175
+ // (-32700) when we can, otherwise skip it. Lines are processed sequentially so
176
+ // responses stay ordered.
177
+ // ---------------------------------------------------------------------------
178
+ export async function serve() {
179
+ process.stdin.setEncoding("utf8");
180
+ let buf = "";
181
+ let chain = Promise.resolve(); // serialize handling so output order is stable
182
+
183
+ process.stdin.on("data", (chunk) => {
184
+ buf += chunk;
185
+ let nl;
186
+ while ((nl = buf.indexOf("\n")) >= 0) {
187
+ const line = buf.slice(0, nl).replace(/\r$/, "").trim();
188
+ buf = buf.slice(nl + 1);
189
+ if (!line) continue;
190
+ let msg;
191
+ try { msg = JSON.parse(line); }
192
+ catch { error(null, -32700, "parse error"); continue; }
193
+ // batch requests (array) are valid JSON-RPC — handle each element
194
+ const msgs = Array.isArray(msg) ? msg : [msg];
195
+ for (const m of msgs) chain = chain.then(() => handle(m)).catch((e) => error(m && m.id != null ? m.id : null, -32603, String(e && e.message || e)));
196
+ }
197
+ });
198
+ // keep the process alive until stdin closes
199
+ await new Promise((resolve) => process.stdin.on("end", resolve));
200
+ await chain;
201
+ }
202
+
203
+ // Run directly (`node mcp.mjs`) as well as when imported + invoked via --mcp.
204
+ if (import.meta.url === `file://${process.argv[1]}` || (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1])) {
205
+ serve();
206
+ }
package/package.json CHANGED
@@ -1,20 +1,25 @@
1
1
  {
2
2
  "name": "@raymondchins/agentmap",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "The repo map your coding agent is forced to use. A queryable, ranked ts-morph code-relationship map for TypeScript/JavaScript repos — PageRank hubs, Aider-style symbol ranking, a token-budgeted digest, and a single --any router (file → symbol → feature → live git-grep), wired into the agent loop via post-commit auto-refresh and a PreToolUse hook.",
7
+ "description": "The repo map your coding agent is forced to use — ~98% fewer tokens (up to 99.9% per task) to understand a TS/JS codebase. A queryable, ranked ts-morph code-relationship map: PageRank hubs, Aider-style symbol ranking, a token-budgeted digest, and a single --any router (file → symbol → feature → live git-grep), wired into the agent loop via post-commit auto-refresh and a PreToolUse hook.",
8
8
  "type": "module",
9
9
  "bin": {
10
- "agentmap": "./repomap.mjs"
10
+ "agentmap": "agentmap.mjs"
11
11
  },
12
- "main": "repomap.mjs",
12
+ "main": "agentmap.mjs",
13
13
  "files": [
14
- "repomap.mjs"
14
+ "agentmap.mjs",
15
+ "mcp.mjs",
16
+ "hooks",
17
+ "LICENSE-APACHE",
18
+ "NOTICE"
15
19
  ],
16
20
  "scripts": {
17
- "map": "node repomap.mjs"
21
+ "map": "node agentmap.mjs",
22
+ "test": "node --test test/"
18
23
  },
19
24
  "engines": {
20
25
  "node": ">=18"
@@ -23,6 +28,7 @@
23
28
  "ts-morph": "28.0.0"
24
29
  },
25
30
  "keywords": [
31
+ "agentmap",
26
32
  "repo-map",
27
33
  "repomap",
28
34
  "code-map",