Zero-install, deterministic repo-indexing engine — one vendorable
file, a library, a CLI, and 29 MCP tools.
Walks a repo, extracts symbols and imports across 16 languages (21
tree-sitter grammars), resolves cross-file references into a typed
link-graph, and renders byte-identical artifacts every time it runs.
Vendor the single engine.mjs, install it from npm or
Homebrew, or run it from a container — nothing else to install, ever.
docker run --rm -v "$PWD":/work \
ghcr.io/maxgfr/codeindex scan --repo /work
Same zero-dependency engine.mjs bundle behind all
three — CLI, library import, and Docker image.
Key measured numbers
Fig. 01 — measured, reproducible
Benchmarks
Measured against universal-ctags,
Serena (LSP over MCP) and Graphify
with a reproducible harness
(scripts/bench/) — median of 5 runs, one warmup
discarded. Every number on this page comes from the same
site/benchmarks.json committed to this repo; nothing
here is rounded or typed in by hand. Full methodology and every
scenario:
BENCHMARKS.md.
Cold index — code-du-travail
2,823 files, every tool measured on the same repo. Lower is faster.
Warm reindex — against codeindex's own cold build
No competitor here exposes an incremental reindex, so the baseline is
the cold build of the same repo: what a rerun costs once the cache is
warm, and what one touched file costs. Lower is faster.
Axis by axis
One row per thing an agent-tooling team weighs. A filled tile marks the
leader on that axis — hover any cell for the measured number. Where a
rival wins it stays at the top of the table, unedited: ctags takes both
cold-index rows, because a flat tags pass is a smaller job
than a typed cross-file graph. Speed is the axis that matters least
here, and the file cap showed why — codeindex used to finish next.js
ahead of ctags by leaving 7,952 of its files out of the index, so every
lookup answered for a prefix of the repo and called it the repo. The
cap is gone: slower, and correct about the whole tree. The rows below
are the axes that time is spent on.
codeindex leads a competitor leads tie / not this tool's job— = n/a (not measurable)
Fig. 02 — scored against tools that are not us
Measured quality
Speed is easy to measure and easy to trust. Extraction quality is
neither — an engine scored only against expectations its own authors
wrote cannot find the construct nobody thought to label. So the checks
that lead here are the ones with an outside authority: an index built
by the real TypeScript compiler, a declaration-level differential
against a mature independent indexer over real repositories, the
code-navigation queries each grammar's own authors publish, and each
grammar's declared node vocabulary read straight from the parser.
Those found real gaps — C headers indexing to nothing, a JS/TS walk
stopping one node short of every callback block — and each was fixed
rather than argued with. Below them sits the hand-labelled corpus that
covers what no external indexer reports at all (doc comments, complete
signatures, call edges), shown with the corpus it is measured on,
because a bare percentage hides the thing a reader most needs to judge
it. Every number is reproducible with pnpm quality:report
and ratcheted in both directions: losing quality fails CI, and
gaining it fails too until the recorded baseline is refreshed
in the same commit. The blind spots are listed rather than left
implicit.
And what the labels say. The corpus below is written
here, so it is published with its denominators.
Fig. 03 — capabilities
Features
Everything below ships in the same zero-dependency bundle — no
feature is an optional add-on package.
Keyless search + deterministic fuzzy
BM25 lexical ranking over symbol names, path segments and
headings. A term with zero document frequency falls back to a
byte-deterministic character-trigram match (Dice ≥ 0.6) — typo
tolerance with no embeddings. Disable with
--no-fuzzy.
Deterministic semantic tier
An opt-in static token → vector lookup table — no
neural forward pass. Encode → mean-pool → L2-normalize →
round-half-to-even int8 quantize; ranking is a pure integer dot
product, byte-identical everywhere. No model on disk, no
behavior change.
Rich embedding tier — v2.11.0
Point at a containerized HTTP endpoint
(ghcr.io/maxgfr/codeindex-embed, all-MiniLM-L6-v2)
via CODEINDEX_EMBED_ENDPOINT. Wins over the static
tier by explicit intent; degrades to lexical on exit 0 if
unreachable — never a silent fallback to the wrong model.
Call graph & caller index
Typed import / call / use
/ doc-link / mention edges at file and
module level. Per-symbol callers and references resolve in-proc
in sub-millisecond time on an already-loaded warm scan.
Graph analytics
Louvain communities, PageRank & betweenness centrality,
dead-code detection, complexity × churn hotspots, change
coupling, surprise-edge detection, and a tests→code map — all
derived from the same link-graph.
SCIP export
index.scip via a hand-rolled zero-dependency
protobuf encoder — validated against the official
scip CLI's stats/lint.
Multi-ecosystem workspaces
npm/yarn, pnpm, lerna, nx, cargo, go.work, maven, gradle, uv and
Composer — detected and named without running any of their own
tooling.
Symbolic edits over MCP
replace_symbol_body, insert_after_symbol, insert_before_symbol — precise edits addressed by
symbol name, not by line ranges that drift.
mermaid & repo map
Render a mermaid dependency diagram or a plain-text repo map
directly from the graph — no separate diagramming step.
Byte-deterministic outputs
Two cold builds of an unchanged repo produce identical
graph.json/symbols.json — confirmed on
7 of 7 measured repos this session. Graphify's graph.json
differed on 5 of the 6 measurable repos; determinism is a value we
hold across every repo, not just where it's easy.
Fig. 04 — usage
Quickstart
Five ways in — pick the one matching your integration.
Install it as a regular dependency and import the pure functions.
Multi-arch (linux/amd64, linux/arm64) —
nothing inside but node and the bundle, no
npm install.
docker run --rm -v "$PWD":/work ghcr.io/maxgfr/codeindex scan --repo /work
docker run --rm -v "$PWD":/work ghcr.io/maxgfr/codeindex index --repo /work --out /work/.codeindex
docker run -i --rm -v "$PWD":/work ghcr.io/maxgfr/codeindex mcp
Pin by digest in CI or anywhere reproducibility matters:
ghcr.io/maxgfr/codeindex@sha256:…. The rich
semantic-search tier ships separately as
ghcr.io/maxgfr/codeindex-embed.
Consumers who don't want an npm dependency vendor
scripts/engine.mjs + engine.d.mts at a
pinned release tag instead — the model downstream consumer
tools use, so each stays a standalone, single-file install.
These are architecturally different tools, not differently tuned ones,
so the comparison is split across the page rather than collapsed into
one verdict: the axis-by-axis scoreboard sits with the
benchmarks, where its numbers come from, and
the declaration-level differential against a mature third-party indexer
sits with measured quality. What is left is the
shape of each tool: what it models, what it covers, what it costs to
install.
Tool comparison
codeindex vs. universal-ctags, Serena and Graphify.
Tool
Model
Language coverage
Install
Worth knowing
codeindex
Static byte-stable artifacts
(graph.json/symbols.json), rebuilt
each run with an incremental cache
Slower to build than a flat tags file, and its extraction is
the only one here scored against outside oracles (see
measured quality)
universal-ctags
A flat tags file
~40 via broad, generic parser rules
Binary install
Fastest cold index at every size measured, and mature enough to
serve as an oracle here — but definitions only: no call graph,
no import resolution, no cross-file links
Serena
LSP client orchestrating per-language language servers
Any language with an LSP server
114 MB venv + per-language language servers
Type-aware references no static tool claims — bought with the
slowest activation and per-call latency measured here, and no
on-disk artifact to reproduce
Graphify
tree-sitter knowledge graph (graph.json);
code-only mode is keyless, the docs/PDF tier needs an LLM and is
excluded here
36 via tree-sitter
140 MB Python venv (uv tool)
~1 ms per lookup, but graph.json is not
byte-identical across rebuilds on any of the 6 measurable repos,
and file nodes are basename-keyed (no file-overview)