0
0
via GitHub · Posted Jul 26, 2026 · 1 min read

AgentMap: Compiler-Precise Repository Navigator

raymondchins/agentmap
Tool

Stop your coding agent reading the wrong files. Compiler-grade TS/JS repo map — 100% precision on blast radius vs grep's 60%, measured on public repos. CLI + MCP server, fully local, no vector DB.

45Stars
10Forks
3Open issues
JavaScript MIT v0.16.1 Updated 1 month ago
View on GitHub

AgentMap is a TypeScript/JavaScript repository mapping tool that identifies code dependencies with 100% precision using the actual TypeScript compiler, outperforming grep-based approaches by 40 percentage points. It provides both a CLI and MCP server for agents to query relevant code without context waste, operating entirely locally with no vector databases or external APIs.

0 comments

README

agentmap

Your agent burns most of its context just finding code. This gives it the answer in one line.

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, 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 too (367 files, 99.2%) and taxonomy (125 files, 96.0%). Captured output, pinned shas → 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 <anything>

--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

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.

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.

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 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 cats 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.
  • 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.

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, ~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 RepoMapper Repomix 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 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). 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 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/) 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 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 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:

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:

# 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 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 (<Hero, <ProviderCard), explicit intent words (where is, who imports, reuse, existing component), and — in both the Grep tool and the Bash branch — bare multi-hump PascalCase identifiers (ProviderCard, TopProviders) that almost always mean "where is this symbol / who uses it". The Bash branch additionally only fires when the searcher is the primary command (at the start, or after ;/&&); piped log-filters stay silent.

All four nudge/gate variants (this one, Codex, Gemini, OpenCode) also self-gate on project presence: since they ship at user/global scope too (plugin bundle, ~/.gemini, ~/.codex, ~/.config/opencode), they walk up from the tool call's cwd to the filesystem root looking for node_modules/@raymondchins/agentmap or a built .claude/agentmap/map.json before doing anything else, so a repo with no agentmap stays silent instead of nagging (or, for Codex, denying a grep it has no business denying).

--install-hooks writes both matchers into .claude/settings.json for you (merge-safe — preserves existing settings, won't duplicate on re-run). The single hook file dispatches internally on tool_name. For reference, or to wire it by hand:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Grep",
        "hooks": [{ "type": "command", "command": "node ./hooks/agentmap-nudge.mjs" }]
      },
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "node ./hooks/agentmap-nudge.mjs" }]
      }
    ]
  }
}

That's the "forced to use it" in the tagline: the map stays current on its own, and the agent is steered to it the moment it reaches for a dependency-shaped grep or Bash search.

3. Agent skills (Cursor, Claude Code, Codex, OpenCode, Gemini, Antigravity, Copilot)

npx @raymondchins/agentmap --install-skill

…or grab just the skill (no agentmap flags) via the skills CLI — agentmap ships the skills/agentmap/SKILL.md layout it expects:

npx skills add raymondchins/agentmap

--install-skill copies packaged SKILL.md files and a Cursor rule (.cursor/rules/agentmap.mdc, alwaysApply: true) into the current repo or global agent directories. Paths follow each platform's official skill-directory conventions. Options:

agentmap --install-skill --platform cursor           # Cursor rule + .cursor/hooks.json shell gate (project)
agentmap --install-skill --platform claude           # .claude/skills/agentmap/SKILL.md
agentmap --install-skill --platform codex            # .codex/skills/ (project) or ~/.codex/skills/ (global)
agentmap --install-skill --platform opencode         # .opencode/skills/ (project) or ~/.config/opencode/skills/ (global)
agentmap --install-skill --platform gemini           # .gemini/skills/ (project); global ~/.gemini/skills/ (Windows global: ~/.agents/skills/)
agentmap --install-skill --platform antigravity      # .agents/skills/ (project) or ~/.gemini/config/skills/ (global)
agentmap --install-skill --platform copilot          # .copilot/skills/ or ~/.copilot/skills/
agentmap --install-skill --global --platform claude  # ~/.claude/skills/...
agentmap --install-skill --platform agents           # legacy .agents/skills/ (project or global); excluded from default `all`
agentmap --install-skill --dry-run                   # preview paths, no writes

--platform all installs: claude, cursor, codex, opencode, gemini, antigravity, copilot (not legacy agents).

Some platforms also get always-on docs and hooks in the same command:

--platform Skill Also installs (project) Global docs
gemini .gemini/skills/…/SKILL.md GEMINI.md + .gemini/settings.json BeforeTool nudge ~/.gemini/GEMINI.md
codex .codex/skills/…/SKILL.md AGENTS.md merge-safe <!-- agentmap:begin/end --> 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:

# 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 hookPreToolUse 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):

{
  "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 <!-- agentmap:begin/end --> (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 .gitignored 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 <query>
   │
   ├─ 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 (sha 2becdb4).

--any <q> — the router (file → symbol → feature → live content)

See The --any router above. Default first move for any "where/what/who" question.

--find <q> — 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: "<path>" or external: true on the match, present only when the entry is a pass-through — a real definition carries neither.

--search <q> — 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 <path> — 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 <sym> — 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 <path> 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. <Foo /> 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. <Foo.Bar /> resolves to Bar, not the Foo namespace; <Foo>...</Foo> counts once (the closing tag isn't a second call site); an intrinsic tag (<div>) 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 '<Button' beat the tool outright. Captured on vercel/ai-chatbot at c2f8235; reproduce it by running the query against that commit.

A deliberate deep query: it lazily spins up the TS type-checker (a few seconds on a large repo) only when invoked — the map build and every other query never pay that cost, and nothing is persisted. Accurate on statically-resolvable calls; dynamic dispatch, reflection, and string-keyed access are beyond any static tool. Also available as the callers MCP tool.

--calls <sym> — 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 <Container><Sidebar /></Container> has no CallExpression in it, so before this fix it reported zero outgoing calls even though it clearly depends on both. Each <Foo /> / <Foo>...</Foo> 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 <name> — 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 <path> — 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

Comments (0)

Sign in to join the discussion.

No comments yet

Be the first to share your take.