tsindex is a Rust CLI and server for building a lightweight tree-sitter index over one repo or a catalog of many repos. It stores symbols, outlines, and syntactic references in SQLite, then exposes a small retrieval surface for local CLI use, MCP clients, or a simple HTTP API.
- Detects the dominant languages in each configured repo.
- Walks the repo while honoring
.gitignoreand.tsindexignore. - Indexes symbols and references into
.tsindex/index.dbwith repo-aware storage. - Lets you query symbols, file outlines, references, and raw tree-sitter patterns.
- Supports multiple repos in one catalog with per-query
--repofilters. - Supports Python, JavaScript/TypeScript/TSX, HTML, CSS/SCSS, Rust, Go, Java, Kotlin, Swift, C/C++/C#, PHP, Ruby, Bash, SQL, Makefiles, JSON, YAML, and Markdown.
- Can serve the same primitives over MCP or HTTP.
SQL files (.sql, case-insensitive) use the
tree-sitter-sequel grammar.
Symbols include databases, schemas, tables, columns, views, indexes, functions,
sequences, types, triggers, and CTEs. Schema-qualified objects are looked up by
their final name; identifier case and quoting are preserved. Dialect coverage
follows the grammar, so unsupported syntax can produce partial results; SQL
inside string literals is not indexed separately. The grammar treats unqualified
double-quoted expressions as string literals; qualify column references such as
users."User ID" to distinguish them.
When a release is available, download the macOS binary for your architecture
(arm64 or x86_64) and checksums.txt from the
Releases page.
Verify its checksum with shasum -a 256, make it executable with chmod +x,
and place it in a directory on your PATH. Releases also include the MIT and
Apache-2.0 license texts, third-party notices, and build provenance when available.
See SECURITY.md for verification details.
Source builds require Rust 1.89 or later. The checkout pins the tested toolchain in rust-toolchain.toml. Install Rust with rustup if needed:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shAdd cargo to your PATH in your shell profile. For example, add this line to ~/.bashrc or ~/.zshrc:
export PATH="$HOME/.cargo/bin:$PATH"Then clone and install the binary:
git clone https://github.com/legalzoom/tsindex.git
cd tsindex
cargo install --path . --lockedThe binary lands in ~/.cargo/bin/tsindex, which rustup already added to your PATH.
To upgrade later, pull and re-run cargo install --path . --locked.
To remove: cargo uninstall tsindex.
This repo includes shareable Codex skills at skills/tsindex and
skills/goal-tsindex. They refresh the local index on demand, help configure
the MCP command, and tell Codex and spawned agents to prefer tsindex MCP tools
for code navigation. The goal-tsindex skill is the same workflow exposed
through the /goal-tsindex alias.
Install or update the skills locally:
skills_dir="${CODEX_HOME:-$HOME/.codex}/skills"
mkdir -p "$skills_dir"
cp -R skills/tsindex "$skills_dir/"
cp -R skills/goal-tsindex "$skills_dir/"Start a new Codex session after installing so the skill metadata is loaded.
Then invoke $tsindex or /goal-tsindex from a repo, or from ~/dev for a
multi-repo catalog. Set TSINDEX_DEV_ROOT if your multi-repo root lives
somewhere other than ~/dev. The skills expect the tsindex binary on PATH;
set TSINDEX_BIN if you keep the binary somewhere else.
RTK (https://github.com/rtk-ai/rtk) and tsindex solve complementary context
problems: RTK reduces noisy shell-command output before it reaches the model,
while tsindex avoids whole-file reads by serving structured code-navigation
results over MCP. Use both in the same agent session: keep your RTK command hooks
enabled, and configure tsindex as an MCP server with:
tsindex --root /path/to/repo serve --mcpFor a multi-repo catalog, point --root at the catalog root (for example
~/dev) and keep passing repo="<name>" in tsindex tool calls. The HTTP
dashboard's Adoption panel also reports whether repo-local or home RTK config is
present and shows the matching tsindex MCP command for the current root.
The skill startup scripts do not install persistent watchers. By default they
run tsindex --root <target> update as an eager warm-up and exit.
tsindex --root <target> serve --mcp coordinates an in-process watcher with
other servers using the same index and configuration. One owner receives
filesystem notifications; every automatic server also reconciles on demand
before reads when the last complete refresh is older than
worktree_refresh_ms. This keeps missed notifications and watcher exhaustion
from leaving an indefinitely stale index. Use TSINDEX_START_MODE=watch only
when you want a foreground watcher for CLI-only sessions that never start
serve --mcp.
Watch startup rejects unsupported language filters even for configured clones that are still missing. Fix the reported filter and restart the watcher; CLI language overrides and repository/global fallback retain their precedence.
Source registration never follows directory symlinks, matching the index walk,
so linked dependency graphs such as pnpm's node_modules cannot expand
into duplicate watched paths. On Linux the registration itself enforces this:
the watcher walks each workspace with the indexer's ignore rules (.gitignore,
.ignore, .tsindexignore, nested workspace roots, and the control-directory
filter) and registers one non-recursive inotify watch per eligible directory.
Directories that are created, moved, or un-ignored later are registered when
their parent reports the change. A hash registry skips unchanged registrations
and removes watches for deleted or newly ignored directories. Workspace
ownership is resolved again for each batch, so a newly cloned nested repo uses
its own ignore rules. Non-recursive parent probes discover missing configured
clones, including those below ignored paths. The probes also observe Git's
repository and global excludes, shared worktree metadata, inherited ignore
files above workspace roots, and external rule-file targets. Symlinked
.gitignore, .ignore, and .tsindexignore files retain probes on their
targets even while dangling, including directory links along the rule-file
path. Rule edits, replacement, and removal refresh every affected workspace's
index before newly excluded watches are removed. Deleting a rule's governing
subtree also releases its external probes after the index update succeeds.
Other platforms walk eligible directories to discover these input probes while
retaining native recursive source watches. See
architecture.
Notifications are coalesced into distinct changed paths over bounded two-second
windows, including while indexing is busy. Reads are discarded; writable closes
and queue-overflow rescans are retained. Unreadable or vanished child
directories and optional input probes are skipped; a source-root registration
failure or an exhausted inotify watch limit stops that watcher attempt.
Automatic MCP servers retry watching and use on-demand refresh in the meantime;
if reconciliation fails, the request reports that freshness is unavailable.
Other platforms keep their native recursive watcher (FSEvents on macOS).
--no-refresh serve --mcp disables primary automatic refresh and watching when
you need a server that only reads an existing primary index. Explicit linked
checkout requests still reconcile their own indexes on demand.
Initialize a catalog rooted at one repo:
tsindex --root /path/to/repo initinit refuses to overwrite an existing .tsindex/config.toml; pass --force
to regenerate it (this discards [server] tuning and extra [[repos]]).
Indexing honors .gitignore by default, so common dependency, build, cache,
coverage, and runtime output paths usually stay excluded without tsindex-specific
configuration. Add .tsindexignore only for extra index-only excludes.
Independently of ignore files, any path containing one of these directory
components is never indexed and cannot currently be whitelisted:
node_modules, vendor, target, .venv, .git, dist, build, .next,
.turbo, .parcel-cache. Committed vendor/ trees or packages named build
or dist are therefore invisible to the index.
For PHP projects, keep common generated paths such as vendor/,
.phpunit.cache/, .phpstan.cache/, .psalm/cache/, bootstrap/cache/, and
storage/logs/ in .gitignore so both Git and tsindex skip them.
Build the first index:
tsindex --root /path/to/repo buildUpdate incrementally after edits:
tsindex --root /path/to/repo updateupdate is the command to run when watch was not running. It walks the
configured repo(s), compares files against the existing index, indexes new or
changed files, skips unchanged files, and purges rows for files that were
deleted or are no longer indexable. Index-backed CLI commands (symbol,
outline, enclosing, and refs) read the existing SQLite index; they do not
refresh it on every query. query reads live source contents using indexed
file candidates when the index is ready. Before the first build it falls back
to a live walk, so it remains usable without a prior
8000
build.
serve (HTTP) refreshes at startup and reconciles before later reads when the
last successful full refresh is older than worktree_refresh_ms (2000 ms by
default). serve --mcp additionally coordinates a watcher with compatible
servers using the same database. Its startup runs in the background so the MCP
initialize handshake remains responsive; indexed reads establish freshness
before returning. Primary live query requests bypass full-index
reconciliation, including when startup is blocked or unrelated sources fail.
Watch notifications reduce latency, while periodic on-demand
reconciliation covers missing watches. Failed reconciliation returns an
explicit freshness error. Pass --no-refresh to disable primary automatic
maintenance; its index then reflects the last explicit build/update.
Watch for changes and refresh the index on save:
tsindex --root /path/to/repo watchAfter init, the root repo is registered as the first catalog entry. You can add more repos:
tsindex --root /path/to/catalog repos add payments /path/to/payments-repo
tsindex --root /path/to/catalog repos add auth /path/to/auth-repo
tsindex --root /path/to/catalog repos listBuild the shared catalog index:
tsindex --root /path/to/catalog buildQuery across every repo:
tsindex --root /path/to/catalog symbol authenticate_userOr limit a query to one repo:
tsindex --root /path/to/catalog symbol authenticate_user --repo auth
tsindex --root /path/to/catalog refs authenticate_user --repo auth
tsindex --root /path/to/catalog query '(function_definition name: (identifier) @fn)' --language python --repo authMCP and HTTP servers discover linked worktrees from Git metadata, including
worktrees created after startup. In a linked worktree, pass
repo=<worktree directory name> or its absolute root. Omitting repo keeps
queries in the server's primary catalog; results are never merged across
checkouts. Unknown names return an error listing known repos and index status.
The primary keeps its materialized index. A fresh linked checkout stores a
small private overlay in its own .tsindex/index.db and refers to a shared,
immutable snapshot under the main checkout's .tsindex/bases/. Identical files
reuse that snapshot's symbols and references; changed files replace the whole
file's extraction, and deletion or exclusion hides the base file. Overlays pin
the base's persisted identity, so replacing its pathname cannot silently change
an older checkout's index. First use still walks and checks local content, so
a dirty primary cannot supply another
checkout's bodies. Bodies and edits always use the selected checkout's source.
Each overlay also stores an indexed directory of its effective file metadata. Symbol and reference lookups join that directory by logical file ID, avoiding a complete file-list merge for each query. Symbols and references remain shared with the base. The directory is derived state: an older overlay writer makes its generation stale, so readers fall back to merged files until it is rebuilt. See the directory measurement for the lookup, refresh, and storage tradeoff.
Publishing a refresh replaces the private overlay atomically; active readers
retain their earlier complete SQLite snapshot. Worktrees ingest independently,
including across server processes. MCP and HTTP dispatch use bounded worker
pools, so another request can progress while one checkout is refreshing.
Unused worktrees get no index or watcher. Additional worktrees refresh on
demand at most once every worktree_refresh_ms (default 2000 ms); requests wait
up to 30 seconds for a refresh and can retry if it is still running. Compatible
processes reuse successful refresh markers within that interval, preserving the
original refresh age.
Only changed extraction is parsed, but overlay reconciliation checks content
hashes across the complete logical checkout, including unchanged scans. This
adds latency: the synthetic measurement
used about twice as long for first use while saving storage. A source changing
during preparation discards the stage and returns an error asking the caller to retry.
An encountered source-read or extraction failure also blocks freshness success;
managed reads fail until a complete refresh succeeds. Continuous external edits
can therefore require retries. The primary's --no-refresh opt-out remains
available. An index generation is not an atomic snapshot of the live filesystem.
Sharing applies to a fresh, single-checkout configuration with a ready main
index. If that source is not ready, contains schema-stale extraction, or cannot
publish a shared base on first use, the checkout builds a standalone index so
it can still serve requests. That index remains
materialized when the main index later becomes ready. Existing materialized
worktree indexes also remain usable without automatic conversion, and multi-repo
catalogs retain materialized storage. Missing or incompatible bases recover by
rebuilding the selected checkout separately.
An explicit --db pointing at an overlay from another configuration refuses a
recovery that would discard its existing repositories. Use a separate database
destination for that root.
Bases are fixed snapshots and are retained: automatic rebasing, compaction,
and deletion are future work. The first base adds one full copy next to the
primary, so savings grow with checkout count and shared content. Downgrading
to a binary without overlay support requires rebuilding these derived caches.
See the execution trace and
architecture for lifecycle and compatibility details.
Names that collide use <basename>@<git-worktree-id>; the main checkout uses
@main. tsindex worktrees --json lists the resolved names, paths, index status,
and last update times. The HTTP /repos endpoint includes the same worktrees.
tsindex --root /path/to/main worktrees
tsindex --root /path/to/main worktrees --jsoninit: Detects languages and writes.tsindex/config.toml.build: Runs a full index build over the workspace.update: Runs an incremental build, indexing only changed/new files and purging stale files.watch: Uses filesystem notifications to keep the index up to date.languages: Prints the detected language breakdown.symbol: Looks up indexed symbols by name. Accepts several names to batch the lookup into one query.outline: Returns the structure of one file without dumping the full file. Pass--body-for <name>(repeatable) to inline selected symbol bodies in the same call.enclosing: Finds the symbol range that contains a file position. Accepts several rows to resolve a whole stack trace / hunk set in one call.refs: Finds syntactic identifier occurrences (not binding-aware semantic references), batched and paginated.query: Runs a raw tree-sitter query for one or more languages.worktrees: Lists checkout names, paths, index status, and last update times.repos: Lists, adds, and removes repos in the catalog.serve: Starts the MCP or HTTP server.
You can inspect per-command help with:
tsindex <command> --helpExamples:
tsindex symbol authenticate_user --include-body
tsindex symbol authenticate_user --repo auth --include-body
tsindex symbol authenticate_user create_session list_orders # batch several names in one query
tsindex outline src/auth/login.py --repo auth
tsindex outline src/auth/login.py --body-for authenticate_user --body-for create_session # structure + selected bodies in one call
tsindex refs authenticate_user --scope 'src/**'
tsindex refs authenticate_user create_session list_orders # references for several names in one query
tsindex enclosing src/server.py 42 87 130 # enclosing symbol for several lines (e.g. a stack trace)
tsindex query '(function_definition name: (identifier) @fn)' --language python --capture fn--root: Workspace root to index or query.--db: Override the SQLite database path.--languages <lang>: Restrict work to one or more explicit languages (repeatable).queryaccepts either--language <lang>or repeated--languages <lang>options to query several grammars.--json: Emit JSON output for CLI commands.--no-refresh: Disable automatic primary refresh before and duringserve(and the MCP watcher). Explicit linked checkout requests still reconcile.
Several query commands also support --repo to restrict results to one configured repo.
Start the MCP server on stdio:
tsindex --root /path/to/repo serve --mcpThe MCP server runs a session lifecycle handler on a dedicated thread. It records the owning session PID and tsindex PID in stderr and checks the owner every 5 seconds, exiting when that process ends. The handler starts before index initialization and ends with tsindex; it needs no separate daemon or PID file. By default, the owner is tsindex's immediate parent (normally Codex, Claude, or another MCP client). Launches directly under PID 1 keep running without inferred ownership.
When a wrapper launches the server, pass the actual session process PID:
tsindex --root /path/to/repo serve --mcp --session-pid 12345Use the live Codex/Claude/etc. process PID in place of 12345. Explicit
ownership survives wrapper exit and also works with serve --http and watch.
HTTP remains independent unless --session-pid is supplied; watch --exit-with-parent monitors its immediate parent. The watcher's --session-pid
replaces parent monitoring and cannot be combined with --exit-with-parent.
On macOS and Linux, the handler also checks process start time to detect PID reuse and treats fully exited zombie processes as ended. On Linux, an exited main thread does not end ownership while worker threads remain alive, and process names need not be valid UTF-8. If the inferred parent cannot be inspected, the handler falls back to parent-PID-only tracking, without PID-reuse or zombie checks. An explicit owner must exist and allow process inspection or startup fails; this includes PID 1 when inspection is denied. A later permission or inspection error does not count as an exit. Normal MCP stdin EOF still stops the server promptly.
The server exposes five read tools:
get_symbol— passnames: [...]to fetch several symbols in one call (one round-trip; results keyed by each match'sname)list_file_outline— passinclude_bodies_for: [...]to inline selected symbol bodies in the same call (orient + fetch in one round-trip)enclosing_symbol— passrows: [...]to resolve several positions (a whole stack trace / hunk set) in one call; answers come back per row underresultsfind_references— bounded, paginated syntactic occurrences for one or more identifiers; its MCP input requiresnames: ["identifier"]even for a single identifier, avoiding ambiguous conditional schemas in tool-calling clients; verify before dependency-sensitive refactorsquery
Over the MCP/stdio transport it also exposes one write tool:
replace_symbol: rewrites a function/class/method in place by name. It locates the symbol by re-parsing the file live (so it is not fooled by a stale index), replaces the symbol's lines withnew_body, and refuses the write if that would introduce a syntax error the file didn't already have — leaving the file untouched on rejection. When several symbols share a name and kind (twofn newin differentimplblocks, the same method on two classes), passqualified(e.g."K2.dup", as returned byget_symbol) orrow(1-based start row) to pick one; the ambiguity error lists both for each candidate. The write goes through a temp file andrename, so a crash mid-write cannot truncate the source.new_rangeis omitted when the named symbol no longer exists after the edit (empty body, rename). This removes the need to quote the old code just to anchor a whole-symbol replacement. Unlike the built-in editor — which requires reading a file before editing it —replace_symbolneeds no prior read, so it is the cheapest way to rewrite a known symbol body in a large file (it carries the symbol, not the whole file, into context). It is intentionally not offered over HTTP, which stays read-only.
Source ranges are compact arrays [start_row, start_col, end_row, end_col].
Rows are 1-based for editor/grep compatibility; columns are 0-based UTF-8
byte offsets (tree-sitter's convention, not character counts — they differ on
lines containing non-ASCII text) and end coordinates follow tree-sitter's
exclusive-end convention. The col input of enclosing_symbol uses the same
byte convention.
list_file_outline omits import/use rows by default; pass
include_imports: true to include them.
When include_bodies_for names a symbol whose source cannot be read or no
longer matches the index, the node carries body_unavailable with the reason
and the response is marked partial: true instead of failing the whole call.
find_references drops the declaring occurrence by default
(include_declarations: false) for classes, functions, methods, types, and
similar named declarations in every language; value bindings (variables,
properties, constants, JSON/YAML keys, Make targets) are still reported because
their name is the reference site too.
In C/C++ a function prototype is indexed with kind prototype while its
definition keeps function, so kind alone distinguishes them in get_symbol
and replace_symbol.
find_references with snippet_lines greater than 1 returns a block of source
with the reference in the middle, so the single range does not by itself say
which block line is the reference. Those results carry snippet_start_row (the
row of the block's first line, same 1-based convention as range), and the
reference sits at range[0] - snippet_start_row within snippet. Single-line
snippets omit the field, because range already is the line. Assuming range
points at the block's first line silently reports every hit N rows off — that is
a real failure observed in benchmarking, not a hypothetical.
Read result counts and bodies are bounded by default, and paginated responses
expose truncated plus next_offset when more results are available.
list_file_outline paginates whole top-level roots, so a single root carries its
entire requested subtree; the serialized-byte budget rejects an oversized lone
root with guidance to lower depth rather than silently dropping children.
Server limits are configurable under
[server] in .tsindex/config.toml:
[server]
http_port = 7337
query_timeout_ms = 2000
worktree_refresh_ms = 2000
default_result_limit = 200
max_result_limit = 300
default_body_line_limit = 120
max_body_line_limit = 1000
capture_char_limit = 2000
max_response_chars = 45000http_port is the default port for serve --http; query_timeout_ms bounds
the wall-clock time a raw query spends before returning timed_out: true.
The reference-page limits are sized against the MCP client's tool-result cap,
measured on a 1,607-reference identifier at snippet_lines: 0 (~134 chars per
reference): 200 refs (28 KB) and 300 refs (45 KB) are accepted, 500 refs (67 KB)
is hard-rejected by Claude Code with exceeds maximum allowed tokens and spilled
to a file — which makes the call useless to the agent. Raising
max_result_limit past 300 re-opens that failure. Note that limit alone is
not a guarantee: snippet_lines: 3 roughly doubles bytes per reference, so a
300-reference page can still exceed the cap. max_response_chars is the real
guard: the response is additionally capped by a serialized-byte budget which,
when a page would otherwise exceed it, shrinks the page (adjusting
returned/next_offset, or the per-name quota for get_symbol) so the payload
stays within the configured budget. Outlines shrink only by whole top-level
roots.
Set it to 0 to disable the budget — page size is then governed by limit
alone.
Files parsed with tree-sitter error recovery mark their results partial: true.
During the initial background build, the indexed read tools (get_symbol,
list_file_outline, enclosing_symbol, find_references) return an explicit
index is not ready error instead of treating an incomplete index as an
authoritative empty result. query is exempt — it reads live files, so it
returns results immediately even before the first build finishes.
Read responses include diagnostics only when a valid request needs attention.
A bare scope or file_glob accepts a file or directory, including ./dir,
dir/, and absolute paths inside the selected repository. A typo that matches
no indexed files emits filter_matched_no_files. get_symbol rejects unknown
kinds and reports kind_filtered_out with the found kind counts when a kind
removes every candidate for a requested name. enclosing_symbol always emits
matches; an empty column lookup retries by row with col_ignored, and a
top-level row reports no_enclosing_symbol. Omit col unless needed.
MCP query accepts languages: ["typescript", "tsx"] alongside language.
Compilation failures in a language array emit query_compile_failed while
other languages continue. Indexed candidates and required equality literals
avoid unrelated parsing. Responses report files_scanned and files_parsed;
timeout errors also report files_scanned.
Raw query responses set truncated when execution stops before all captures are returned. If this
was caused by the configured query timeout instead of the result limit, the response also includes
timed_out: true.
find_references returns a total_files field: the count of distinct (repo, file) pairs
across the full filtered result set — post-filter (scope, exclude_tests, include_declarations)
and pre-pagination. The groups array covers only the returned slice, so its length is not the
distinct-file count when the response is truncated; total_files is the fix for that. It is
identical whether the caller asks for limit=3 or limit=5000.
When a reference's source no longer matches the indexed SHA (the index is stale
for that file), find_references cannot safely slice a snippet from the wrong
text. Instead of emitting a silently blank snippet, the row carries a
snippet_unavailable string with an actionable reason (e.g. "source no longer
matches the index; run tsindex build before trusting snippets"), and the
response is flagged partial: true. Re-run after tsindex build to restore
snippets.
Start the local dashboard and HTTP API:
tsindex --root /path/to/repo serveBy default, serve starts the HTTP dashboard/API on the configured HTTP port
(7337 unless changed in .tsindex/config.toml). You can also pass the mode
and port explicitly:
tsindex --root /path/to/repo serve --http --port 7337Then open http://127.0.0.1:7337/dashboard.
The server binds 127.0.0.1 and only answers requests whose Host header is a
localhost form on the bound port (a DNS-rebinding mitigation). --bind <addr>
binds another address and --allowed-host <name>
9607
(repeatable) accepts an
additional Host name, for example a Kubernetes Service DNS name. The Host
check is not authentication: a non-loopback bind exposes the read API and the
POST /repos/* mutation endpoints to anyone who can reach the port and set a
matching Host header. Keep the default loopback bind unless the network path
is already access-controlled.
Catalog additions and removals wait for admitted indexed calls and rebuilds to finish, so an older refresh cannot undo a successful catalog change. Indexed calls run concurrently with one another. They and rebuilds wait while an add or remove is in progress, including cloning. Live syntax queries and metadata routes bypass catalog admission but still share the bounded HTTP worker pool.
Available routes:
GET /healthGET /dashboardGET /dashboard/dataGET /reposPOST /repos/addPOST /repos/removePOST /repos/rebuildPOST /tools/listPOST /tools/call
Example:
curl -s http://127.0.0.1:7337/health
curl -s http://127.0.0.1:7337/tools/list -X POST
curl -s http://127.0.0.1:7337/tools/call \
-H 'Content-Type: application/json' \
-d '{"name":"get_symbol","arguments":{"name":"authenticate_user","repo":"auth","include_body":true}}'The dashboard reports savings two ways. Both are heuristic estimates, not accounting totals, and neither measures your actual model billing or any realized saving.
The potential estimate is an indexed-codebase projection: indexed file bytes are approximated as tokens at 4 bytes per token, multiplied by a heuristic cache-read reduction rate (30.8%), then priced at a notional $3 per million input tokens. These are fixed illustrative assumptions, not a validated performance guarantee across languages, repositories, models, or tasks.
The usage-tally figure is also a heuristic estimate: every indexed tool call
appends an event (the source bytes the response delivered vs. the bytes
returned) to ~/.tsindex/<repo>/savings.jsonl (set TSINDEX_HOME to relocate
that directory; cargo test/cargo run point it at target/ via
.cargo/config.toml so development never touches your real home), and the dashboard rolls those up
per repo and globally across all past sessions, priced at the cache-read
$0.30-per-million notional rate. It is a conservative lower bound on the bytes
tsindex returned instead of a full read — an estimate of what you might
otherwise have carried into context, not a realized or observed saving against
your bill.
bench/payload_size_old_vs_new.py compares deterministic MCP response sizes
between two locally built versions. Its docstring contains setup instructions.
Build a separate index with each binary, and use a repository whose contents
you are permitted to process. The output measures bytes and approximate token
counts, not model accuracy, billing, or realized savings.
bench/worktree_overlays.py compares fresh copied indexes with shared overlays
using real Git worktrees and MCP reads. It reports first-use latency, unchanged
reconciliation, later one-file ingestion, and total SQLite storage including
the primary, shared base, private indexes, and sidecars. Both binaries should
use the same build profile. See its --help and the
measurement notes.
See CONTRIBUTING.md for contribution and validation guidance.
Research behind worktree storage overlays is collected in the overlay research. The implementation's execution and baseline tests are described in the overlay trace.
For iterative work, run the CLI through Cargo so you don't have to reinstall on every change:
cargo run -- --help
cargo run -- --root /path/to/repo buildBuild without installing:
cargo build # debug build at ./target/debug/tsindex
cargo build --release # release build at ./target/release/tsindexcargo fmt
cargo testMaintainers publish a release by pushing a vX.Y.Z tag matching Cargo.toml
from the reviewed main branch. GitHub Actions builds the macOS binaries,
generates checksums and provenance, and attaches the project license texts and
third-party notices. Existing release versions are not overwritten.
tsindex is available under either the MIT license or the
Apache License, Version 2.0, at your option
(MIT OR Apache-2.0). Dependency licenses and copyright notices are listed in
THIRD_PARTY_NOTICES.md.
- src/app.rs: CLI definition and command dispatch
- src/index.rs: indexing, SQLite schema, and query API
- src/lang.rs: language registry and tree-sitter queries
- src/mcp.rs: MCP and HTTP serving layer
- tests/integration.rs: fixture-based integration tests