agentmap β€” 98% fewer tokens for a coding agent to find your code

npm CI License: MIT node >= 20 1 runtime dependency zero network calls

# agentmap ### Your agent burns most of its context just finding code. This gives it the answer in one line. ```bash npx @raymondchins/agentmap --relates lib/db/schema.ts ``` ``` relates: lib/db/schema.ts (pr 0.073744) dependents (21): lib/types.ts, lib/utils.ts, lib/db/queries.ts, components/chat/message.tsx, app/(chat)/api/chat/route.ts, … ``` Every file on that list really imports it. `grep` gets **40% of them wrong**. --- ## πŸ’Έ What it saves Token cost of the hidden first step in every agent task β€” *find the relevant code* β€” on a real 154-file Next.js app ([vercel/ai-chatbot](https://github.com/vercel/ai-chatbot), sha `2becdb4`): | The agent needs to know… | Reading files | agentmap | Saved | |---|---:|---:|:---:| | Does a helper for this already exist? | 14,740 | 19 | **99.9%** | | Load the whole repo into context | 150,281 | 1,127 | **99.3%** | | What breaks if I change this file? | 81,038 | 616 | **99.2%** | | Where is this symbol defined? | 1,950 | 20 | **99%** | | What files make up this feature? | 6,121 | 1,025 | **83.3%** | | Give me a repo overview | 3,065 | 1,127 | **63.2%** | | What does this one file import? | 583 | 517 | **11.3%** | | **All 7 combined** | **257,778** | **4,451** | **98.3%** | Holds on [zod](https://github.com/colinhacks/zod) too (367 files, **99.2%**) and [taxonomy](https://github.com/shadcn-ui/taxonomy) (125 files, **96.0%**). Captured output, pinned shas β†’ [`benchmark/RESULTS.md`](./benchmark/RESULTS.md) ## 🎯 …and it's still right Fewer tokens is worthless if they're the wrong ones. Separate eval, ground truth derived live from real repos: | | agentmap | `git grep` | |---|:---:|:---:| | What depends on this file? | **100%** precision | 59.9% precision | | Where is this defined? *(top-1)* | **100%** | 32% | | Where is this defined? *(top-3)* | **100%** | 80% | | Tokens to find a definition | **1.9Γ— fewer** | β€” | n=42 dependents / n=75 definitions across zod, zustand, hono. Re-run: npm run eval Β· method β†’ EVAL.md --- ## ⚑ The five commands | You want | Run | Saves | |---|---|:---:| | "Do we already have this?" | `agentmap --find formatCurrency` | 99.9% | | "What breaks if I touch this?" | `agentmap --relates lib/auth.ts` | 99.2% | | "Where is this defined?" | `agentmap --find ChatMessage` | 99% | | "Give me the repo, cheap" | `agentmap --map --tokens 2000` | 99.3% | | Don't want to pick? | `agentmap --any ` | β€” | `--any` routes it for you: file β†’ symbol β†’ feature β†’ live content search. Cold build **~1.2s**. Cached query **~0.1s**. No server, no vector DB, no API key. --- ## πŸ”Œ Setup ```bash npx @raymondchins/agentmap --install-hooks # rebuild on commit + steer the agent to the map npx @raymondchins/agentmap --install-skill # Claude Code Β· Cursor Β· Codex Β· Gemini Β· OpenCode Β· Copilot ``` Most repo-map tools stop at building the map. These two hooks are why it stays useful: the map **rebuilds itself after every commit**, and the agent gets **nudged to the map the moment it reaches for a dependency-shaped grep**. Claude Code users can get both from the [plugin](#4-claude-code-plugin-one-command-bundle). > **100% local.** Zero network calls, zero telemetry β€” not one `fetch`/`http` in the source. > ⚠️ Install the **scoped** name; unscoped `npx agentmap` is someone else's package.
Where the cache lives, and how it stays fresh
First run caches to `.claude/agentmap/map.json` (`--install-hooks` gitignores it). Later runs serve that cache **only** on a clean tree at an unchanged `HEAD` β€” with uncommitted `.ts/.tsx/.js/…` edits it silently rebuilds, so you never query a stale snapshot. ``` $ npx @raymondchins/agentmap agentmap: 154 files | 4 features | top hub: lib/utils.ts (deg 52, pr 0.105171) ``` From a checkout, every command also works as `node agentmap.mjs …`.
--- ## 🧠 Why the answers are right Built on **`ts-morph` β€” the real TypeScript compiler**, not text matching or tree-sitter guessing. It resolves `tsconfig` path aliases, `vite`/`webpack` aliases, `#imports` subpaths, and monorepo workspaces. Where `grep` sees a string, agentmap sees the resolved module. That's also why barrels don't fool it: `export * from "./x"` looks identical to a real definition to a text search, so your agent edits the re-export and changes nothing. agentmap follows the chain and names the file that actually declares it.
The honest asterisks β€” read these before quoting a number
- **The win scales with the work.** The 63% and 11% rows are the floor. A *trivial single-file* lookup can cost **more** than `cat` + `grep` β€” taxonomy's file-import task hit **βˆ’313%**, and it stays in the table. - **The 98.3% headline is carried by its two biggest rows** β€” repo dump (150,281 β†’ 1,127) and blast radius (81,038 β†’ 616). Drop the repo dump and it's **96.9%**; drop both and it's **89.8%** here, **93.7%** pooled across all three repos, and **73.1%** on the smallest one. All of those are real β€” they answer different questions. The headline is the common worst case: an agent dumping the repo at session start. - **`--relates` returns the full blast radius**, so it costs *more* than a bare `grep -l` file list. That's why the same command reads as 99.2% *saved* in the benchmark and *more expensive* in the eval: the benchmark's baseline is an agent that `cat`s all 65 dependent files, the eval's is a file list nobody reads. Against the list, agentmap trades tokens for precision β€” 100% vs 59.9%, so ~4 in 10 files on the grep list don't belong. Complete-and-correct over short-and-wrong, but it is a trade β†’ [EVAL.md](./EVAL.md). - **Numbers are context-token volume**, not answer quality or wall-clock. - **Token counts are estimates** (`chars / 4`), applied identically to both sides. - **TypeScript/JavaScript only** (+ Vue SFC) β€” see [Scope & limitations](#scope--limitations).
--- ## Why it's different Many "repo context" tools are a photocopy: they dump your repository (or a slice of it) into the prompt once and walk away β€” the copy goes stale the moment you edit a file, and nothing makes the agent actually read it. agentmap is queryable and ranked instead: the agent interrogates it flag-by-flag rather than swallowing a dump. It also reports an `edgeCoverage` map-health signal and warns loudly when a repo's imports mostly *don't* resolve, so a broken map is never quietly framed as success. The self-refreshing side β€” a post-commit rebuild plus a `PreToolUse` hook that steers the agent to the map before it serial-greps β€” is genuinely useful, but it isn't unique: **CodeGraph** ([colbymchenry/codegraph](https://github.com/colbymchenry/codegraph), ~62kβ˜… (2026-07-26)) ships a native OS-event file watcher (FSEvents/inotify) with debounced auto-sync and an installer that auto-configures eight agent CLIs. agentmap's honest edge over the multi-language graph tools is narrower and sharper: **TS/JS resolution the others approximate, with a published accuracy eval.** | | **agentmap** | [Aider repo map](https://github.com/Aider-AI/aider) | [RepoMapper](https://github.com/nuptcode/repomapper) | [Repomix](https://github.com/yamadashy/repomix) | [code2prompt](https://github.com/mufeedvh/code2prompt) | | --- | --- | --- | --- | --- | --- | | **Ranking algorithm** | Personalized PageRank (file + symbol graphs) | PageRank (graph ranking) | Importance heuristics | None (file order) | None (file order) | | **Languages** | TS/JS + Vue SFC (via ts-morph) | Many (tree-sitter) | Many (tree-sitter) | Language-agnostic (text) | Language-agnostic (text) | | **Token-budget output** | Yes β€” `--map [--tokens N]` ranked digest | Yes (built into Aider's context) | Partial | Yes (size caps) | Yes (templates/caps) | | **TS/JS resolution depth** | **Compiler-grade β€” `tsconfig` paths + `vite`/`webpack` alias + `#imports` + workspaces (ts-morph)** | Basename/regex heuristics | Basename/regex heuristics | N/A (text) | N/A (text) | | **Retrieval-accuracy eval** | **Yes β€” published [`EVAL.md`](./EVAL.md) vs live ground truth** | No | No | No | No | | **Agent-loop wiring** | Yes β€” post-commit auto-refresh + PreToolUse hook | In-process (Aider only) | No | MCP server (no auto-refresh, no nudge) | No | | **Dependencies** | `ts-morph` only | Python + tree-sitter stack | Python + tree-sitter | Node | Rust binary | | **Install** | `npx @raymondchins/agentmap` | `pip install aider-chat` | `pip install` | `npx`/global | `cargo`/binary | Comparison as of 2026-07-27, from each project's own docs. These are moving targets β€” if a cell is out of date, that's a bug: open an issue. What that table is **not** claiming: agentmap is TS/JS-only (the others are multi-language), and it's a **file-level import graph**, not a full call-site/reference resolver (see [Scope & limitations](#scope--limitations)). The differentiators are narrow and honest: **(1)** compiler-grade TS/JS resolution (aliases, `vite`/`webpack`, `#imports`, workspaces) with a published accuracy eval, and **(2)** the `--any` router. The agent-loop wiring is real and convenient but **not** unique β€” [CodeGraph](https://github.com/colbymchenry/codegraph) and others auto-sync and auto-configure agent CLIs too; we don't claim it as a moat. --- ## The agent loop (staying current, staying used) A common failure of repo-map tools: they build a beautiful map, and then the agent forgets it exists and greps anyway. A map the agent doesn't open is just dead weight. agentmap closes that loop. Two hooks (in [`./hooks/`](./hooks/)) do the work: the map **refreshes itself after every commit**, and the agent gets **nudged to query it before it serial-greps**. You wire it once β€” then it stays current on its own, and stays used. > This wiring is table stakes, not the moat β€” [CodeGraph](https://github.com/colbymchenry/codegraph) > and other tools also auto-sync (via native OS file watchers) and auto-configure agent CLIs. > agentmap ships it because it's genuinely useful; the actual point of agentmap is the > **compiler-grade TS/JS accuracy** the map is built on. ### 1. Auto-refresh on commit [`hooks/post-commit`](./hooks/post-commit) rebuilds `.claude/agentmap/map.json` after each commit, detached + silenced so it never slows the commit. It skips during rebase/merge/cherry-pick and no-ops if Node is missing. The hooks ship inside the npm package. The simplest setup: ```bash npx @raymondchins/agentmap --install-hooks ``` This copies `hooks/post-commit` into `.git/hooks/`, sets it executable, ensures `.claude/agentmap/` is in `.gitignore`, and **auto-wires the `PreToolUse` nudge hook into `.claude/settings.json`** (merge-safe + idempotent) so map enforcement is on by default β€” no manual paste. Manual alternative for just the post-commit hook: ```bash # from your repo root cp hooks/post-commit .git/hooks/post-commit chmod +x .git/hooks/post-commit ``` The hook resolves the builder to the **installed** package β€” `node_modules/.bin/agentmap`, a PATH `agentmap` binary verified to be `@raymondchins/agentmap`, then `npx @raymondchins/agentmap`. It never runs a repo-local `./agentmap.mjs` unless you opt in with `AGENTMAP_HOOK_ALLOW_LOCAL=1` (for developing agentmap itself), so an attacker-planted `agentmap.mjs` can't execute on your next commit. ### 2. Force the agent to use it β€” `PreToolUse` hook [`hooks/agentmap-nudge.mjs`](./hooks/agentmap-nudge.mjs) is a **non-blocking** hook for Claude Code that covers **both** the `Grep` tool and raw Bash text-searchers (`grep`/`rg`/`egrep`/`fgrep`/`ag`/`ack`). When either looks like a dependency / who-imports / component-usage / reuse / where-is-symbol search, it injects a reminder steering the agent to `agentmap --any` first. It never denies the call, and stays silent for raw-string / Tailwind-class / lowercase-HTML-tag sweeps and for pipe-filtered commands like `ps aux | grep node` β€” so it's high-signal, not nagging. **Fires on:** `import`/`require`/`export`/`from '...'` patterns, JSX component tags (`` block | `~/.codex/AGENTS.md` | | `opencode` | `.opencode/skills/…/SKILL.md` | `AGENTS.md` + `.opencode/plugins/agentmap-nudge.js` | `~/.config/opencode/AGENTS.md` | | `cursor` | `.cursor/rules/agentmap.mdc` | `.cursor/hooks.json` `beforeShellExecution` gate + `.cursor/hooks/agentmap-cursor-nudge.mjs` | β€” (project-scope only) | Codex and OpenCode share one repo-root `AGENTS.md` on project install. Existing content outside the marked block is preserved. Pair with `--install-hooks` (Claude Code) or `--mcp` (Cursor MCP). ### 4. Claude Code plugin (one-command bundle) Prefer the plugin over `--install-skill`/`--install-hooks` if you're on Claude Code and want the skill, the `PreToolUse` grep/Bash nudge, and the stdio MCP server in a single install that auto-updates: ```bash # in Claude Code /plugin marketplace add raymondchins/agentmap /plugin install agentmap@agentmap ``` The plugin bundles: the packaged **SKILL.md**, the **PreToolUse nudge** (both the `Grep` tool and Bash text-searchers, via `${CLAUDE_PLUGIN_ROOT}`), and the **stdio MCP server** (`npx -y @raymondchins/agentmap --mcp`, so `ts-morph` is fetched on demand β€” the plugin cache ships no `node_modules`). > **One thing the plugin can't do: install the git `post-commit` hook.** Claude Code > plugins can't write into `.git/hooks/`, so the auto-refresh-on-commit still needs a > one-time `npx @raymondchins/agentmap --install-hooks` in each repo (it also wires the > nudge into `.claude/settings.json`, harmlessly redundant with the plugin's copy). > Without it the map still rebuilds on any dirty query β€” you just lose the commit-time > refresh. ### Onboarding by platform Enforcement isn't uniform β€” some CLIs get a **live hook** that actively steers grep to agentmap, some get an **MCP server** the agent can call, and some are **docs-only** (a skill/rule the agent may or may not consult). Honest matrix: | Platform | Install | Enforcement | Known gaps | |----------|---------|-------------|------------| | **Claude Code** | `/plugin install agentmap@agentmap` (or `--install-hooks`) | **live hook** β€” `PreToolUse` nudge on `Grep` + Bash searchers | non-blocking (never denies grep); bare-symbol `Grep` nudge requires the #3 hook fix | | **Gemini CLI** | `--install-skill --platform gemini` | **live hook** β€” `.gemini/settings.json` nudge | fires on `BeforeTool` and emits a top-level `systemMessage`; Gemini parses and then **drops** `hookSpecificOutput.additionalContext` on `BeforeTool`, which is why the nudge used to vanish silently | | **OpenCode** | `--install-skill --platform opencode` | **log-only** β€” `.opencode/plugins/agentmap-nudge.js` writes to the log, does not inject context | plugin can't steer the model; relies on the `AGENTS.md` block being read | | **Cursor** | `--install-skill --platform cursor` + `.cursor/mcp.json` (below) | **live gate** β€” `.cursor/hooks.json` `beforeShellExecution` hook, plus the `alwaysApply` rule and the MCP server | denies only high-confidence structural greps; allow-fallback for logs/pipes/non-TS-JS; `AGENTMAP_CURSOR_GATE=0` bypasses; project-scope only | | **Codex CLI** | `--install-skill --platform codex` | **live gate** β€” `.codex/config.toml` PreToolUse hook | denies only high-confidence structural greps; allow-fallback for logs/pipes/non-TS-JS; `AGENTMAP_CODEX_GATE=0` bypasses; needs a trusted dir + Codex hooks-GA | | **Copilot CLI** | `--install-skill --platform copilot` | **docs-only** β€” `.copilot/skills/` | same as Codex β€” no live hook yet | **Cursor MCP β€” copy-paste `.cursor/mcp.json`** (Cursor's `--mcp` wiring is a documented dead-end otherwise; drop this at your repo root): ```json { "mcpServers": { "agentmap": { "command": "npx", "args": ["-y", "@raymondchins/agentmap", "--mcp"] } } } ``` Then Cursor exposes the 11 query tools (`any`, `find`, `relates`, `map`, `hubs`, `features`, `feature`, `symbols`, `search`, `callers`, `calls`). Run `agentmap --doctor` any time to see what's wired vs missing. ### Uninstall agentmap only writes files into your repo/home β€” remove them to fully uninstall. `agentmap --doctor` lists every path it wrote, and every docs merge lives inside an `` (or `# agentmap:begin/end`) fence, so deleting just that block leaves the rest of your `AGENTS.md` / `GEMINI.md` intact. | Platform | Remove | |----------|--------| | Claude Code | `.claude/skills/agentmap/` + the agentmap `PreToolUse` block in `.claude/settings.json` | | Cursor | `.cursor/rules/agentmap.mdc`, `.cursor/hooks/agentmap-cursor-nudge.mjs`, the `beforeShellExecution` entry in `.cursor/hooks.json`, + the `agentmap` entry in `.cursor/mcp.json` | | Codex | `.codex/skills/agentmap/`, the `# agentmap:begin/end` block in `.codex/config.toml`, `.codex/hooks/agentmap-codex-nudge.mjs`, and the fenced block in `AGENTS.md` | | OpenCode | `.opencode/skills/agentmap/`, `.opencode/plugins/agentmap-nudge.js`, the `AGENTS.md` block | | Gemini | `.gemini/skills/agentmap/`, `.gemini/hooks/agentmap-nudge.mjs`, the `BeforeTool` hook in `.gemini/settings.json`, the `GEMINI.md` block | | All | map cache `rm -rf .claude/agentmap/`; npm devDep `npm rm @raymondchins/agentmap`; the agentmap block in `.git/hooks/post-commit` | ### Troubleshooting | Symptom | Cause / fix | |---------|-------------| | `features (0)` | `--features` only detects Next.js `app/` routes; a TanStack `src/routes/` repo legitimately shows 0. Use `--map` / `--symbols` instead. | | Empty or wrong map | Usually no `tsconfig.json` / resolvable aliases in the target repo, so no edges resolved β€” run `agentmap --doctor` and check `edgeCoverage` in `--json`. | | Stale-looking results | By design the map rebuilds from disk on a dirty tree / SHA mismatch. Force a rebuild by just running `agentmap`. | | Codex/Gemini nudge never fires | Codex's gate is opt-in β€” set `[features] hooks = true` in `.codex/config.toml` (`AGENTMAP_CODEX_GATE=0` disables it). Gemini needs the `BeforeTool` hook that `--install-skill` writes. | | Cursor gate blocks a grep you meant | Re-run the same command with `AGENTMAP_CURSOR_GATE=0` prefixed. It only denies high-confidence structural searches; logs, pipes and non-structural sweeps already fall through. Remove the `beforeShellExecution` entry from `.cursor/hooks.json` to turn it off for good. | | Cursor gate never fires | It is project-scope only, so it must be installed from the repo root (`--install-skill --platform cursor`, not `--global`), and Cursor reads `.cursor/hooks.json` at startup β€” restart Cursor after installing. | | Installed the wrong `agentmap` | This is **`@raymondchins/agentmap`** (npm scope) β€” not the unrelated unscoped `agentmap` packages. | | Cursor MCP tools missing | `--mcp` doesn't auto-wire Cursor; add the copy-paste `.cursor/mcp.json` from the matrix above and restart Cursor. | | Hook works in your shell, not in the agent | Almost always **nvm**. Your interactive shell sources `~/.nvm/nvm.sh`; the git hook and the agent's tool runner do not, so `node` isn't on their `PATH`. Point the hook at an absolute node (`which node`) or install a system-wide node. | | `JavaScript heap out of memory` | Raise the ceiling β€” the parse peaks and there is no in-process warning that can fire in time (the process dies inside a single call, with heap use still at ~40% one sample earlier). Re-run as `NODE_OPTIONS=--max-old-space-size=8192 npx @raymondchins/agentmap`. Repo **size is not the axis**: measured, a 252-file Next.js app peaks at 683 MB while 4,000 dependency-free files peak at 756 MB, because the dependency `.d.ts` closure (~300 MB, ~1,800 extra program files on a 393-file app) dominates. A small repo with heavy `@types` can need more than a large plain one. | | Skill file looks out of date | Each installed skill dir carries a `.agentmap_version`. `agentmap --doctor` compares it against the running version and flags the drift; `--install-skill` again overwrites it. | | `0 files mapped` | agentmap indexes `git ls-files --cached --others --exclude-standard`, so uncommitted files *are* included but **`.gitignore`d ones are not** β€” a source tree matched by an ignore rule maps to nothing, as does a directory that is not a git repo at all. Confirm with `git ls-files --others --exclude-standard \| head`. | --- ## The `--any` router Don't want to learn eight flags? You don't have to. Throw anything at `--any` β€” a filename, a function, a feature, even a raw string β€” and it figures out what you meant, returning the first layer that hits: ``` --any β”‚ β”œβ”€ 1. FILE exact path β†’ unique basename β†’ unique substring β”œβ”€ 2. SYMBOL exported name contains the query (across all files) β”œβ”€ 3. FEATURE app/-router feature name contains the query └─ 4. CONTENT live `git grep` (tracked + untracked) β€” never stale ``` Layers 1–3 read the cached structural map (fast, ranked). Layer 4 is a **live disk read** via `git grep -F`, so raw strings, copy, Tailwind classes, and config values the structural graph never indexes still resolve instead of coming up empty. **Symbol hit** (query resolved to a symbol β†’ full block): ``` $ node agentmap.mjs --any cn [structure] 1 symbol, 0 feature match for "cn" lib/utils.ts β†’ cn (FunctionDeclaration) ``` **Ambiguous file hit** (query matched multiple files β†’ narrow it): ``` $ node agentmap.mjs --any utils [structure] "utils" matched 3 files β€” narrow it: lib/utils.ts lib/db/utils.ts tests/prompts/utils.ts ``` **Content fallback** (no file/symbol/feature match β†’ live git-grep): ``` $ node agentmap.mjs --any streamText [content] 13 lines: app/(chat)/api/chat/route.ts:8: streamText, app/(chat)/api/chat/route.ts:194: const result = streamText({ artifacts/code/server.ts:1:import { streamText } from "ai"; artifacts/code/server.ts:18: const { fullStream } = streamText({ artifacts/code/server.ts:40: const { fullStream } = streamText({ artifacts/sheet/server.ts:1:import { streamText } from "ai"; artifacts/sheet/server.ts:11: const { fullStream } = streamText({ ``` --- ## Commands Every snippet below is **representative output** (long lists trimmed) from running agentmap against the public 154-file Next.js repo [vercel/ai-chatbot](https://github.com/vercel/ai-chatbot) (sha 2becdb4). ### `--any ` β€” the router (file β†’ symbol β†’ feature β†’ live content) See [The `--any` router](#the---any-router) above. Default first move for any "where/what/who" question. ### `--find ` β€” reuse-before-rebuild symbol search Find every symbol whose name contains the query β€” exported symbols **plus** non-exported top-level declarations. Use it before writing a new util or component to check what already exists (a private helper counts as reusable too). ``` $ node agentmap.mjs --find Message find "Message": 55 match hooks/use-messages.tsx β†’ useMessages (FunctionDeclaration) lib/errors.ts β†’ getMessageByErrorCode (FunctionDeclaration) lib/types.ts β†’ messageMetadataSchema (VariableDeclaration) lib/types.ts β†’ MessageMetadata (TypeAliasDeclaration) lib/types.ts β†’ ChatMessage (TypeAliasDeclaration) lib/utils.ts β†’ convertToUIMessages (FunctionDeclaration) lib/utils.ts β†’ getTextFromMessage (FunctionDeclaration) tests/helpers.ts β†’ generateTestMessage (FunctionDeclaration) app/(chat)/actions.ts β†’ generateTitleFromUserMessage (FunctionDeclaration) … ``` **Barrels don't hide the real file.** When a match is reached through a re-export (`export * from "./x"`, or a named/renamed re-export, at any depth), the output names the file that actually declares it. The TypeScript checker resolves the chain, so this works where a name search can't β€” `rg` sees the barrel and the origin as two equal hits with no way to tell which one you can edit. An origin outside the repo reports `β†’ defined outside the repo`; a `node_modules` path is never printed. ``` $ node agentmap.mjs --find useComposedRefs # radix-ui/primitives@579c5b84 find "useComposedRefs": 3 match packages/react/compose-refs/src/index.ts β†’ useComposedRefs (FunctionDeclaration) β†’ defined in packages/react/compose-refs/src/compose-refs.tsx packages/react/compose-refs/src/compose-refs.tsx β†’ useComposedRefs (FunctionDeclaration) packages/react/radix-ui/src/internal.ts β†’ useComposedRefs (?) ``` In `--json` this is `definedIn: ""` or `external: true` on the match, present only when the entry is a pass-through β€” a real definition carries neither. ### `--search ` β€” BM25 lexical search for vague queries When you don't know the exact symbol name β€” the query an agent actually types β€” `--search` ranks symbols by **BM25 lexical relevance** over split-identifier tokens (the symbol name, its file's path segments, feature, and kind), fused with file PageRank so a strong hit in an important file wins ties. No embeddings, no vector DB; the index is built into `map.json`. The same ranker is wired into `--any` as a rung that fires **only** when exact file/symbol matching found nothing, so exact routing is unchanged. ``` $ node agentmap.mjs --search "auth retry logic" search "auth retry logic": 3 match src/authRetry.ts β†’ retryWithBackoff (FunctionDeclaration) [6.83] … ``` Stopwords (`the`, `that`, `of`, …) are dropped, so `--search "the function that dedupes symbols"` works. Also available as the `search` MCP tool. ### `--relates ` β€” blast radius + transitive relevance The file's own block (exports / imports / direct dependents) **plus** a random-walk relevance list (personalized PageRank on the bidirectional import graph) β€” the files most related to the target, transitively, not just its direct importers. ``` $ node agentmap.mjs --relates lib/db/schema.ts relates: lib/db/schema.ts (pr 0.073744) exports (14): user(VariableDeclaration), User(TypeAliasDeclaration), chat(VariableDeclaration), Chat(TypeAliasDeclaration), message(VariableDeclaration), DBMessage(TypeAliasDeclaration), … imports (0): β€” dependents (21): hooks/use-active-chat.tsx, lib/types.ts, lib/utils.ts, components/chat/artifact.tsx, components/chat/message.tsx, lib/db/queries.ts, app/(chat)/api/chat/route.ts, … related (random-walk relevance): lib/utils.ts (0.0476) lib/types.ts (0.0376) components/chat/artifact.tsx (0.0372) components/chat/icons.tsx (0.0264) components/chat/message.tsx (0.0237) lib/db/queries.ts (0.0225) app/(chat)/api/chat/route.ts (0.0218) … ``` **Type-only dependencies are listed separately, not silently dropped.** `dependents` means "would break at runtime". A file imported only via `import type` has no runtime dependents at all β€” but renaming or deleting its exports still breaks every consumer at compile time. Those appear under `type-only dependents`, so a types module stops reading like an orphan: ``` $ node agentmap.mjs --relates lib/types.ts # vercel/chatbot@c2f8235e relates: lib/types.ts (pr 0.002898) exports (7): messageMetadataSchema(VariableDeclaration), MessageMetadata(TypeAliasDeclaration), … imports (0): β€” dependents (0): β€” type-only imports (6): components/chat/artifact.tsx, lib/ai/tools/create-document.ts, … type-only dependents (23): hooks/use-active-chat.tsx, hooks/use-auto-resume.ts, lib/utils.ts, … ``` 22.4% of that repo's import statements are type-only. The fields are `typeOnlyImports` / `typeOnlyDependents` in `--json`, omitted entirely when empty, and they never enter PageRank, `--hubs`, symbol ranking or `--export` β€” the ranking graph stays a runtime graph. For a file carrying a React Server Components directive prologue, the output adds one more line β€” `boundary: 'use client' (client component)` or `boundary: 'use server' (server module/actions)` (`rsc: 'client' | 'server'` in `--json`) β€” right after `dependents`. This is additive and optional: repos with no `'use client'`/`'use server'` directives never see the line. ### `--callers ` β€” compiler-accurate call graph (experimental) Who actually **calls** a symbol, resolved by the TypeScript language service (`ts-morph` `findReferencesAsNodes`) β€” not tree-sitter name-matching. This is symbol-level blast radius: a type-position mention (`typeof foo`), a re-export, a bare value reference (`const x = foo`), or a same-named private local in another file is a *different* symbol and is never mis-attributed. `--in ` disambiguates a name defined in more than one file (exported definitions win over same-named private locals); results are ranked by caller-file PageRank and capped. ``` $ node agentmap.mjs --callers getMessageByErrorCode callers of getMessageByErrorCode [lib/errors.ts]: 3 call sites app/(chat)/api/chat/route.ts:88 β†’ POST lib/db/queries.ts:142 β†’ saveMessage components/chat/message.tsx:57 β†’ PureMessage ``` **JSX counts as a call site.** `` compiles to `React.createElement(Foo, …)` (classic runtime) or `jsx(Foo, …)` (automatic runtime) β€” either way it's an invocation, so a component's callers include everywhere it's rendered, not just plain `foo()` calls. `` resolves to `Bar`, not the `Foo` namespace; `...` counts once (the closing tag isn't a second call site); an intrinsic tag (`
`) resolves to nothing in-project and produces no edge. ``` $ node agentmap.mjs --callers Button callers of Button [components/ui/button.tsx]: 25 call sites components/ai-elements/message.tsx:93 β†’ MessageAction components/ai-elements/message.tsx:263 β†’ MessageBranchPrevious components/ui/sidebar.tsx:249 β†’ SidebarTrigger components/ui/alert-dialog.tsx:158 β†’ AlertDialogAction components/ui/dialog.tsx:72 β†’ DialogContent … ``` Before 0.17.0, JSX wasn't a recognized call shape at all, so that same query returned **0 call sites** β€” a plain `rg '` β€” outgoing call graph (experimental) The companion to `--callers`: which in-project symbols a symbol **invokes**. Each call and `new X()` site inside its body is resolved by the type checker (`getDefinitionNodes`), which follows an imported / re-exported binding through to the real declaration β€” so a same-named local elsewhere is never confused for the imported one. `node_modules` and TypeScript built-ins (`console.log`, `Array.map`, …) are excluded; dynamic dispatch, computed member access, and higher-order callees are honestly skipped. ``` $ node agentmap.mjs --calls extractFacts extractFacts calls [agentmap.mjs]: 15 in-project targets agentmap.mjs:756 β†’ makeProject (FunctionDeclaration) agentmap.mjs:944 β†’ rel (VariableDeclaration) agentmap.mjs:952 β†’ excluded (VariableDeclaration) … ``` **JSX counts as an outgoing call too**, for the mirror-image reason: a component whose body is nothing but `return ` has no `CallExpression` in it, so before this fix it reported **zero** outgoing calls even though it clearly depends on both. Each `` / `...` in the body now resolves to its target declaration the same way a plain call does β€” the printed `(kind)` is the target's own declaration kind (`FunctionDeclaration`, etc.), not "JSX", since resolution is unchanged, only call-site detection is: ``` $ node agentmap.mjs --calls AppSidebar AppSidebar calls [components/chat/app-sidebar.tsx]: 35 in-project targets components/ui/tooltip.tsx:21 β†’ Tooltip (FunctionDeclaration) components/ui/tooltip.tsx:33 β†’ TooltipContent (FunctionDeclaration) components/ui/sidebar.tsx:144 β†’ Sidebar (FunctionDeclaration) components/ui/sidebar.tsx:379 β†’ SidebarContent (FunctionDeclaration) … ``` Same repo and commit: this returned **6** targets before 0.17.0 β€” only the plain hook and helper calls β€” and 35 after, because the 29 components it renders now count too. Same lazy, out-of-band model as `--callers` (builds a Project only on the query, nothing persisted). Also the `calls` MCP tool. JSX closes a real gap here β€” it doesn't change what's still out of reach: the `node_modules`/dynamic-dispatch/computed-member/higher-order limits above still apply. **Going transitive β€” `--depth N`.** Both `--callers` and `--calls` accept `--depth N` (default 1, max 5) for an N-hop closure: `--callers foo --depth 3` is the transitive blast radius ("everything that reaches `foo`, up to 3 hops"); `--calls foo --depth 3` is the dependency cone ("everything `foo` pulls in"). It BFS-traverses the same single warm Project β€” no extra build β€” with cycle detection and node caps so a hub can't explode; each result is tagged with its `depth` and a `via` parent. `--depth 1` is the default single-hop query. ``` $ node agentmap.mjs --callers leaf --depth 2 callers of leaf [src/chain.ts]: 2 callers within depth 2 src/chain.ts:2 β†’ mid [depth 1] src/chain.ts:3 β†’ top [depth 2] ``` ### `--feature ` β€” files that make up a feature Resolves a Next.js `app/`-router feature to its file set, plus the external files that depend on it. ``` $ node agentmap.mjs --feature api feature "api": 11 files app/(chat)/api/chat/route.ts app/(chat)/api/chat/schema.ts app/(chat)/api/document/route.ts app/(chat)/api/history/route.ts app/(chat)/api/messages/route.ts app/(chat)/api/models/route.ts app/(chat)/api/suggestions/route.ts app/(chat)/api/vote/route.ts app/(auth)/api/auth/guest/route.ts app/(chat)/api/files/upload/route.ts app/(chat)/api/chat/[id]/stream/route.ts external dependents (0): β€” ``` ### `--features` β€” list features by size ``` $ node agentmap.mjs --features features (4): api (11 files) login (1 files) register (1 files) chat (1 files) ``` ### `--affected ` β€” which tests cover this file Walks the reverse-dependency closure and reports the test files that reach the target, with hop distance. The useful answer is often the empty one: *nothing covers this*, which is what you want to know **before** a risky edit, not after CI. Type-only importers count. Changing an exported type breaks every `import type` consumer at compile time, so the walk follows `dependents` **and** `typeOnlyDependents`. ``` $ node agentmap.mjs --affected agentmap.mjs affected by agentmap.mjs: 3 test files (of 4 transitive dependents) test/doctor.test.mjs [1 hop] test/pkg-imports.test.mjs [1 hop] test/unit.test.mjs [1 hop] ``` A file with no reachable test says so in words, and `--json` carries `covered: false`. ### `--routes` β€” the App Router route table Every URL the repo serves and the file that serves it. **Next.js App Router only** β€” on any other repo it exits 1 with an explicit `reason` rather than an empty list, so an agent can tell "not applicable" from "nothing found". ``` $ node agentmap.mjs --routes routes: no app/ or src/app/ directory β€” not a Next.js App Router project ``` ### `--route ` β€” resolve a URL to the code that serves it Goes from a bug report naming a URL straight to the handler, its layout chain, and the server modules it can reach. ``` $ node agentmap.mjs --route /dashboard/settings /dashboard/settings (page) serves: app/dashboard/settings/page.tsx boundary: 'client' layouts (outer->inner): app/layout.tsx -> app/dashboard/layout.tsx server modules: lib/server/settings.ts ``` ### `--kind ` β€” narrow `--find` / `--search` by declaration kind A modifier, not a command. Matched loosely and case-insensitively against the ts-morph kind name, so `--kind function` finds `FunctionDeclaration` and `--kind type` finds `TypeAliasDeclaration` β€” you never have to know the enum spelling. ``` $ node agentmap.mjs --find pagerank --kind function find "pagerank" kind~function: 1 match agentmap.mjs β†’ pagerank (FunctionDeclaration) ``` Used alone it is a usage error (exit 2) β€” it has nothing to narrow. ### `--hubs` β€” most important files (PageRank) The files that matter most, ranked by PageRank importance (raw dependent degree shown alongside). ``` $ node agentmap.mjs --hubs agentmap: 154 files (sha 2becdb4) hubs (PageRank importance): lib/utils.ts (deg 52, pr 0.105171) lib/db/schema.ts (deg 21, pr 0.073744) lib/types.ts (deg 23, pr 0.067589) components/chat/artifact.tsx (deg 15, pr 0.036882) components/chat/icons.tsx (deg 27, pr 0.035378) lib/errors.ts (deg 9, pr 0.032787) lib/db/queries.ts (deg 14, pr 0.030085) … ``` ### `--symbols [N]` β€” top ranked symbols (Aider-style) The most important individual symbols across the repo, ranked by the identifier graph (defaults to 30). ``` $ node agentmap.mjs --symbols 10 top 10 ranked symbols (Aider-style): 0.109902 lib/utils.ts β†’ cn (FunctionDeclaration) 0.036013 lib/types.ts β†’ ChatMessage (TypeAliasDeclaration) 0.025686 components/chat/artifact.tsx β†’ ArtifactKind (TypeAliasDeclaration) 0.022461 lib/errors.ts β†’ ChatbotError (ClassDeclaration) 0.021068 lib/types.ts β†’ CustomUIDataTypes (TypeAliasDeclaration) 0.020872 lib/db/schema.ts β†’ Document (TypeAliasDeclaration) 0.020555 components/ai-elements/suggestion.tsx β†’ Suggestion (VariableDeclaration) 0.020555 lib/db/schema.ts β†’ Suggestion (TypeAliasDeclaration) 0.018124 lib/db/schema.ts β†’ DBMessage (TypeAliasDeclaration) 0.015034 lib/errors.ts β†’ ErrorCode (TypeAliasDeclaration) ``` `map.json` persists the top 80. Asking for more re-ranks from the cached map rather than truncating, so `--symbols 200` really does return 200 where the repo has them. When a repo has fewer ranked symbols than you asked for, the header says so and `--json` carries `requested` / `shown` / `truncated`: ``` $ node agentmap.mjs --symbols 200 top 62 ranked symbols (Aider-style) β€” asked for 200, this repo only ranks 62: ``` ### `--map [--tokens N] [--focus ]` β€” token-budgeted ranked digest The token-budgeted digest (Aider's killer feature): a ranked, files-and-symbols summary that fits a token budget. Default budget is 8192 (1024 with `--focus`). `--focus ` personalizes the ranking toward a file you're working on. ``` $ node agentmap.mjs --map --tokens 400 # agentmap (154 files, sha 2becdb4) β€” focus: global, budget ~400 tok lib/utils.ts: cn (FunctionDeclaration) generateUUID (FunctionDeclaration) lib/types.ts: ChatMessage (TypeAliasDeclaration) CustomUIDataTypes (TypeAliasDeclaration) ChatTools (TypeAliasDeclaration) Attachment (TypeAliasDeclaration) components/chat/artifact.tsx: ArtifactKind (TypeAliasDeclaration) UIArtifact (TypeAliasDeclaration) Artifact (VariableDeclaration) lib/errors.ts: ChatbotError (ClassDeclaration) ErrorCode (TypeAliasDeclaration) lib/db/schema.ts: Document (TypeAliasDeclaration) Suggestion (TypeAliasDeclaration) DBMessage (TypeAliasDeclaration) # ~387 tokens (14 files shown) ``` Focused on a working file β€” the ranking re-centers on what `lib/db/queries.ts` actually touches: ``` $ node agentmap.mjs --map --focus lib/db/queries.ts --tokens 350 # agentmap (154 files, sha 2becdb4) β€” focus: lib/db/queries.ts, budget ~350 tok lib/utils.ts: cn (FunctionDeclaration) generateUUID (FunctionDeclaration) getDocumentTimestampByIndex (FunctionDeclaration) fetcher (VariableDeclaration) getTextFromMessage (FunctionDeclaration) convertToUIMessages (FunctionDeclaration) fetchWithErrorHandlers (FunctionDeclaration) sanitizeText (FunctionDeclaration) lib/db/schema.ts: DBMessage (TypeAliasDeclaration) Suggestion (TypeAliasDeclaration) Document (TypeAliasDeclaration) Chat (TypeAliasDeclaration) User (TypeAliasDeclaration) chat (VariableDeclaration) document (VariableDeclaration) message (VariableDeclaration) lib/errors.ts: ChatbotError (ClassDeclaration) ErrorCode (TypeAliasDeclaration) # ~324 tokens (8 files shown) ``` ### `--print` β€” full map as JSON Dumps the cached map (`hubs`, `features`, `rankedSymbols`, `files`) as one JSON object β€” for piping into other tools. Also includes a top-level `fileCount`. ``` $ node agentmap.mjs --print | jq '.hubs[0]' "lib/utils.ts (deg 52, pr 0.105171)" ``` ### `--export ` β€” visualize the import graph Serializes the file import graph (nodes = files, edges = imports, top-N by PageRank, with three light style tiers) as **Graphviz DOT** or **Mermaid** β€” paste straight into [mermaid.live](https://mermaid.live), a GitHub README mermaid block, or `dot -Tsvg`. `--focus ` scopes to a file's 1-hop neighborhood. It reads the cached map only (no ts-morph Project), and prints graph text to stdout (so it isn't combined with `--json`). ``` $ node agentmap.mjs --export mermaid --focus lib/auth.ts %% agentmap import graph β€” 154 files, sha a1b2c3d, focus lib/auth.ts flowchart TD classDef hub fill:#d9d9d9,stroke:#333,stroke-width:2px; n0["lib/auth.ts"]:::hub … ``` ### Global flags | Flag | Description | |------|-------------| | `--help` / `-h` | Print a usage block listing every flag and exit 0. | | `--version` / `-v` | Print the version from `package.json` and exit 0. | | `--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}`. | | `--no-locals` | Hide non-exported top-level declarations from `--find`/`--any` results (shown by default). Never affects `--map`/`--symbols`/`--hubs` ranking. | | `--include-dts` | Include `.d.ts` declaration files in the symbol/ranking pass (excluded by default so generated types don't flood `--find`/`--symbols`/`--hubs`). | | `--install-hooks` `[--dry-run]` | Copy `hooks/post-commit` into `.git/hooks/` (chmod 0755), ensure `.claude/agentmap/` is in `.gitignore`, and auto-wire the Claude Code `PreToolUse(Grep)` nudge into `.claude/settings.json` (merge-safe + idempotent). `--dry-run` previews without writing. Exit 0 on success, stderr + exit 3 on failure. | | `--hook-status` | Report whether the post-commit hook, PreToolUse nudge, and `.gitignore` entry are installed (no writes). | | `--doctor` | Read-only harness health report: git/Claude hook wiring, installed skills + Cursor rule freshness vs `package.json` version, MCP config entries for OpenCode/Antigravity, and map-cache presence/freshness hints. Always exits 0; suggests fix commands (`agentmap --install-hooks`, `--install-skill`, `--setup-mcp`, `agentmap`) but never runs them. Combine with `--json` for a structured report. | | `--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). | | `--setup-mcp` `[--dry-run]` | Configure agentmap as an MCP server for OpenCode and the Antigravity IDE (merge-safe). `--dry-run` previews without writing. | | `--mcp` | Start agentmap as a **stdio MCP server** so non-Claude-Code agents (Cursor, Cline, any MCP client) can query the map. Exposes 11 query tools β€” `any`, `find`, `relates`, `map`, `hubs`, `features`, `feature`, `symbols`, `search`, `callers`, `calls`. | **Exit-code contract:** `0` = success / match / help / version; `1` = query returned zero results (`--any`, `--find`, `--relates`, `--feature` with no match, or `--map --focus` that resolves to no file β€” the global digest still prints, with `focusResolved:false` in `--json`); `2` = usage error (missing required arg, unknown flag, two commands at once, or a sub-flag without its parent command); `3` = maintenance command failed (`--install-hooks`, `--install-skill`, `--setup-mcp`, `--hook-status`, `--mcp`). Any token starting with `-` that matches no known flag prints an error to stderr and exits 2. --- ## Scope & limitations Honesty first β€” this is deliberately a small, sharp tool, not a universal code-graph. - **TS/JS (+ Vue SFC), by design.** Built on `ts-morph`. Indexes `.ts/.tsx/.mts/.cts/` `.js/.jsx/.mjs/.cjs` and the `