Skip to content

raymondchins/agentmap

Repository files navigation

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.

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-3) 93.3% 80%
Tokens to find a definition 2.4× 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.

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

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) MCP + docsalwaysApply rule + the MCP server Cursor's own hooks aren't wired; the rule is advisory
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 + 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.
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)

--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 <path>] — 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 <path> 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 <mermaid|dot> — 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, a GitHub README mermaid block, or dot -Tsvg. --focus <path> 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 <script> blocks of .vue single-file components (best-effort). No Python, Go, Rust, etc. — if your repo isn't TypeScript/JavaScript, use a tree-sitter-based tool instead. Want another language? Vote in #43 — and read what a non-TS language would actually get first, because it would not be the same product.
  • The persisted map is a file-level import graph; the call graph is opt-in. The cached map's edges come from static import / re-export declarations and the named symbols crossing them — --relates answers the file-level question ("who imports this module"). Symbol-level, compiler-accurate call-site resolution is available on demand via --callers (who calls a symbol) and --calls (what a symbol invokes) — both experimental, lazy, out-of-band queries that spin up the type-checker only when invoked and are never folded into the fast map build. The file-level graph additionally records a React Server Components client/server boundary tag (from 'use client'/'use server' directive prologues) where present.
  • Alias & workspace resolution. Resolves tsconfig/jsconfig paths, vite/vitest/ webpack resolve.alias (string entries, parsed from the AST — the config is never executed), and pnpm/npm/yarn workspace cross-package imports (@org/pkg → its source). A build reports edgeCoverage (the share of repo-local imports that resolved) and prints a one-line warning when a repo's imports mostly don't resolve — so a broken/empty map is never silently framed as success.
  • Scoping — .agentmapignore + .d.ts. Generated .d.ts declaration files are excluded from the symbol ranking by default (so a 200-symbol generated types file, or next-env.d.ts, doesn't flood --find/--symbols/--hubs); --include-dts restores them, and they stay live import-resolution targets either way. A repo-root .agentmapignore (gitignore-style subset: anchored /, dir /, * globs, # comments) excludes extra paths.
  • PageRank + symbol ranking are real and implemented (damping 0.85, deterministic power iteration; personalized variants for --relates and --map --focus). The symbol ranking is a faithful port of Aider's identifier-graph approach (credit: Aider, Apache-2.0).
  • Feature detection assumes the Next.js app/ router. --feature / --features derive features from the first real route segment under app/ (or src/app/), skipping route groups (...), dynamic [...], and parallel @... segments. Repos without an app/ directory simply report zero features — every other command still works.
  • Token counts are estimates (chars / 4), not a real BPE tokenizer. Treat --map/--tokens budgets as approximate (±10%).
  • The PreToolUse hook is Claude Code-specific (it speaks Claude Code's hook JSON). The post-commit hook is generic git.

What another language would actually get

Published before any of it is built, because the cheapest way to find out you're being asked for a different product is to describe the product accurately first.

agentmap's accuracy comes from ts-morph — a real TypeScript compiler with a type checker. Another language would be parsed with tree-sitter: syntax, no types, no module resolver. That difference decides what each query can honestly return.

Query TS / JS today Another language
--relates (blast radius) full full — ports best; this is the one to lead with
--search (BM25) full full
--hubs / --map / --symbols (PageRank) full full
--print / --export full full — reads the cached map only
--find full partial — no transitive re-export/barrel chains
--any full partial
--callers / --calls full none — refuses, explicitly
--features / --feature Next.js App Router none
incremental rebuild yes no — full rebuild every time

--callers / --calls are ~259 lines of TypeScript language-service calls. Tree-sitter cannot reproduce them. The options are to refuse loudly or to guess by name-matching, and name-matching will not ship: a silently mis-wired graph is worse than no graph, and it would falsify every accuracy claim here.

Two non-TS languages is the honest ceiling for one part-time maintainer — each one is an ongoing tax, not a one-off. Reasoning, including the arguments against doing this at all, is in ROADMAP.md Part II.

No telemetry. agentmap makes zero network calls. When an unsupported language dominates your repo it counts the files locally, prints a one-line pointer to the vote, and forgets. That count never leaves your machine. Silence it with AGENTMAP_NO_CENSUS=1.

Forks & ports

Two people ported agentmap to another language rather than open an issue, which is the strongest evidence that the "nobody asked" reading was wrong:

Neither is affiliated with this repo and neither is endorsed — listed because pretending they don't exist would be dishonest about demand. If you maintain one: upstreaming beats competing, please open an issue.


Contributing

Issues and PRs welcome. High-value directions:

  • Retrieval-accuracy eval — done (EVAL.md, npm run eval). Next: a type-aware dependents mode (the eval excludes type-only edges to match the value-import graph) and an app/-router fixture so --feature retrieval can be scored too.
  • A real tokenizer behind the --map budget.
  • Hardening feature detection for non-app/-router layouts.

Keep the dependency footprint minimal — ts-morph is the only runtime dependency (it bundles the TypeScript compiler, ~10 MB installed), and keeping it that way is a feature.

License

MIT. Symbol-ranking algorithm credit: Aider (Apache-2.0).

About

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.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages