Files
rustytokens/README.md
T
endergebnisandCopilot f1531a64d4 Add caller/reference analysis, C# imports, and de-noised search
Agent-workflow tooling and search quality improvements:

- find_callers / find_references (by id or exact name), with caller
  attribution to the innermost enclosing symbol and import-graph narrowing
- index file (single-file re-index) and workspace glob member resolution
- search symbols: exact > prefix > fulltext > substring fallback, inline
  source and line numbers, --limit on all search paths
- search text: reads live from disk (no stale FTS snapshot), collapses
  duplicate snippets, correct multi-word phrase matching
- find dead-symbols: excludes framework-wired entry points (pytest test_*,
  #[test] tests, FastAPI/Flask routes, Celery tasks, click/typer commands)
- find references: filters the definition line and string/comment
  occurrences (incl. Rust raw strings)
- C# import edges via new extract_csharp (using directives)
- richer MCP tool descriptions; brace-in-string tokenizer fix

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-08 19:49:36 +02:00

211 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# rustytokens
A native Rust CLI + MCP server that indexes a codebase with
[`universal-ctags`](https://github.com/universal-ctags/ctags) and lets an AI
coding agent retrieve **symbols and functions** instead of whole files — saving
context tokens.
Language coverage (via ctags): Rust, Java, Python, C/C++, TypeScript,
JavaScript, HTML, C#. HTML import edges are extracted directly (not via ctags),
and `<link>`/`<script>`/`<img>` references to local `.css`/`.js`/image files are
tracked as imports. CSS files are discovered and import-tracked, but ctags has
no CSS parser — `search symbols` returns nothing for `.css` files.
`rustytokens` is an independent, clean-room implementation. It does not share
code with any AGPL-licensed project.
## Prerequisites
- Rust (MSRV: current stable; built and tested on nightly)
- `universal-ctags` on `PATH` (`ctags --version` must report Universal Ctags)
```bash
# Debian/Ubuntu
sudo apt install universal-ctags
# macOS
brew install universal-ctags
```
No system SQLite needed — `rusqlite` bundles SQLite with FTS5.
## Build
```bash
cargo build --release
# binary at target/release/rustytokens
```
## Commands
| Command | Purpose |
|---|---|
| `index create <path>` | Index a project from scratch |
| `index update <path>` | Re-index only changed files (content-hash diff) |
| `index file <file>` | Re-index a single file |
| `search symbols <query> [--include-source] [--limit n]` | Symbol search (exact > prefix > full-text > substring) |
| `search text <query> [--limit n]` | Full-text search with line numbers, read live from disk |
| `inspect symbol <id\|name>` | Source of a single symbol (id or exact name) |
| `inspect outline <file>` | Symbol outline of a file |
| `inspect file <file> [--lines a-b]` | Read a file or line range |
| `inspect tree [--depth n]` | Indexed file tree |
| `inspect blast-radius <file> [--depth n]` | Files transitively affected by a change |
| `find importers <file>` | Who imports this file |
| `find dead` | Files nobody imports (entry points excluded) |
| `find dead-symbols` | Symbols never referenced (framework-wired entry points excluded) |
| `find callers <id\|name>` | Symbols that call the given symbol |
| `find references <identifier>` | Code lines referencing an identifier (definitions/strings/comments excluded) |
| `inspect cycles` | Circular imports |
| `serve` | MCP server over stdio |
| `stats` | Index statistics |
Global options: `--project <dir>` (default `.`), `--format json|table`
(default `json`), `--compact`.
## Examples
```bash
# Index a project
rustytokens index create ./myproject
# Find the `authenticate` function
rustytokens --project ./myproject search symbols authenticate
# Get only that function's source (id from the search result)
rustytokens --project ./myproject inspect symbol 42
# Which files import auth.rs?
rustytokens --project ./myproject find importers src/auth.rs
# What's dead / circular?
rustytokens --project ./myproject find dead
rustytokens --project ./myproject inspect cycles
# Who calls `authenticate` (id from `search symbols`) and where is it referenced?
rustytokens --project ./myproject find callers 42
rustytokens --project ./myproject find references authenticate
# Human-readable output
rustytokens --project ./myproject --format table search symbols authenticate
```
## MCP integration
Register the binary as a stdio MCP server (e.g. in Claude Code / Finir
`mcp.json`):
```json
{
"mcpServers": {
"rustytokens": {
"command": "/path/to/rustytokens",
"args": ["--project", "/path/to/your/project", "serve"]
}
}
}
```
Exposed tools: `index_create`, `index_update`, `index_file`,
`search_symbols` (with `include_source` and `limit`), `search_text` (with
`limit`), `inspect_symbol`, `inspect_outline`, `inspect_file`, `inspect_tree`,
`find_importers`, `find_dead`, `find_dead_symbols`, `find_callers`,
`find_references`, `inspect_cycles`, `inspect_blast_radius`, `stats`.
## Why it saves tokens
Instead of sending a whole file, the agent fetches only the symbol it needs.
Measured on `rustytokens`' own `src/ops.rs` (396 lines, ~11.7 KB):
- Whole file: 396 lines, 11,675 chars (~2,900 tokens)
- `inspect symbol` for `build_tree`: 14 lines, 437 chars (~110 tokens)
That single retrieval is ~96% smaller than reading the file. Across a large
codebase and a long session, per-symbol retrieval keeps context focused on the
code actually being edited.
## Performance
Measured on a Xeon E5-2667 v3 @ 3.2 GHz (16 cores), `cargo build --release`,
`universal-ctags` on `PATH`. Wall time, CPU time and peak RSS via
`/usr/bin/time -v`.
| Operation | Repo (files) | Wall | CPU | Peak RSS |
|---|---|---|---|---|
| `index create` | nlohmann/json (497) | 1.61 s | 0.98 s | 22 MB |
| `index create` | gson (264) | 0.92 s | 0.46 s | 13 MB |
| `index create` (warm) | nlohmann/json (497) | 1.55 s | — | — |
| `index update` (no-op) | nlohmann/json (497) | 0.02 s | — | — |
| `search symbols` | — | 8.7 ms | — | 7.8 MB |
| `find dead` | — | 4.4 ms | — | — |
| `inspect cycles` | — | 3.7 ms | — | — |
Notes:
- `index update` is ~50× faster than a full rebuild when nothing changed: it
hashes every file in parallel (rayon) and re-indexes only changed ones, while
`index create` drops and rebuilds the whole database. A warm `index create`
(≈1.55 s) shows that hashing dominates the cost.
- Hardware cycle counters were not available (`perf` tools missing on this
kernel), so cycle figures are estimates: CPU time × 3.2 GHz nominal.
## Scope and known gaps
`rustytokens` covers the same *core* retrieval surface for Java/Python/Rust/
C/C++/HTML as `toktoken`, but intentionally implements a smaller tool set: 17
tools vs `toktoken`'s 27. The following `toktoken` tools have **no equivalent
here yet** (deliberately, not an oversight):
- `inspect_bundle`, `inspect_dependencies`, `inspect_hierarchy` (graph views)
- `search_similar`, `search_cooccurrence` (similarity search)
- GitHub indexing and centrality ranking
Add them only if the agent workflow actually needs them; they are not required
for the "roughly cover Java/Python/Rust/C/C++/HTML" goal.
Known limits that are by design, not bugs:
- `.css` files are discovered and import-tracked, but `ctags` has no CSS
parser — `search symbols` returns nothing for `.css`.
- `find dead` over-counts for C/C++ header-only entry points (documented
conservatism); treat results as candidates, not verdicts.
- `find callers` / `find references` are word-boundary heuristics: they match a
symbol's name in text, so same-name symbols in unrelated files can produce
false positives (`find callers` narrows this via the import graph).
- `find dead-symbols` treats pytest `test_*` functions, `#[test]`-wired Rust
tests and framework-decorated symbols (FastAPI/Flask routes, Celery tasks,
click/typer commands, …) as live entry points. A pure name-scan cannot see
these call sites, so this is a documented heuristic.
- `find references` filters the definition line and occurrences inside strings
and comments; multi-line docstrings/block comments are approximated
line-by-line, so a symbol name inside a long string block may still appear.
## Development
```bash
cargo fmt
cargo clippy
cargo test --release # unit (in-memory SQLite) + CLI integration tests
```
Test fixtures live in `tests/fixtures/` (own small Rust/Python/TS repos).
## Licensing
`rustytokens` is an independent, clean-room implementation — no code is taken
from any AGPL-licensed project. The project's own source is licensed under the
MIT License (see `LICENSE`); `license = "MIT"` is set in `Cargo.toml`.
Dependencies are all permissive (MIT / Apache-2.0 / BSL-1.0 / Unlicense):
`clap`, `rusqlite`, `ignore`, `rayon`, `xxhash-rust`, `serde`, `serde_json`,
`anyhow`, `regex`. None is copyleft — commercial use and closed-source
distribution are fine. The only obligation is to ship each crate's copyright
notice and license text (e.g. a `THIRD_PARTY_NOTICES.md`).
`universal-ctags` is GPL-licensed, but `rustytokens` invokes it only as an
external program over `PATH` (subprocess + stdio, no linking, no bundling). That
keeps it a separate program and does not impose GPL on `rustytokens` itself. Do
not bundle the `ctags` binary into a distributed product — that is what would
trigger GPL obligations for that binary. Keep it a user-installed prerequisite.
Naming: `rustytokens` is distinct from `toktoken`. If this goes commercial,
avoid "toktoken-compatible" or similar wording in marketing.