@raymondchins/agentmap 0.1.0 → 0.2.1
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/NOTICE +24 -0
- package/README.md +75 -47
- package/agentmap.mjs +894 -0
- package/hooks/INSTALL.md +211 -0
- package/hooks/agentmap-nudge.mjs +90 -0
- package/hooks/post-commit +61 -0
- package/mcp.mjs +206 -0
- package/package.json +11 -6
- package/repomap.mjs +0 -461
package/hooks/INSTALL.md
ADDED
|
@@ -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,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raymondchins/agentmap",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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
|
|
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": "
|
|
10
|
+
"agentmap": "agentmap.mjs"
|
|
11
11
|
},
|
|
12
|
-
"main": "
|
|
12
|
+
"main": "agentmap.mjs",
|
|
13
13
|
"files": [
|
|
14
|
-
"
|
|
14
|
+
"agentmap.mjs",
|
|
15
|
+
"mcp.mjs",
|
|
16
|
+
"hooks",
|
|
17
|
+
"NOTICE"
|
|
15
18
|
],
|
|
16
19
|
"scripts": {
|
|
17
|
-
"map": "node
|
|
20
|
+
"map": "node agentmap.mjs",
|
|
21
|
+
"test": "node --test test/*.test.mjs"
|
|
18
22
|
},
|
|
19
23
|
"engines": {
|
|
20
24
|
"node": ">=18"
|
|
@@ -23,6 +27,7 @@
|
|
|
23
27
|
"ts-morph": "28.0.0"
|
|
24
28
|
},
|
|
25
29
|
"keywords": [
|
|
30
|
+
"agentmap",
|
|
26
31
|
"repo-map",
|
|
27
32
|
"repomap",
|
|
28
33
|
"code-map",
|