Universal Repository Agent Graph
Structure-aware retrieval and impact analysis for software repositories. urag indexes code symbols, documentation, call sites, and source locations into a portable SQLite database. It then returns small, cited result packets that are useful to developers and AI agents without loading whole files into context.
urag is a retrieval layer, not an LLM. It finds the relevant repository context; the developer or agent uses that context to answer the question.
See urag in action for a complete indexing and agent response walkthrough.
Traditional text search is good at finding strings. Generic RAG is good at finding similar text. Repository questions often need both, plus structural information such as:
- Where is a function, class, interface, or type defined?
- How does a feature work across modules?
- What calls this function?
- What is the downstream impact of changing a symbol?
- Which exact source lines support the result?
urag addresses these questions with:
- Tree-sitter parsing for symbol-aware indexing.
- FTS5 lexical search for exact names and identifiers.
- sqlite-vec embeddings for conceptual search.
- Reciprocal Rank Fusion for the default hybrid search.
- A static call graph for direct and multi-hop caller queries.
- Import-alias resolution for selected languages.
- Lazy source retrieval, so search results stay compact.
- Incremental indexing and a file watcher for active repositories.
- A CLI and a stdio MCP server for agent harnesses.
repository files
|
v
file discovery and incremental checks
|
v
tree-sitter extractors
|
+--> symbols, signatures, summaries, spans
+--> call edges and import aliases
|
v
SQLite project index
|
+--> FTS5 lexical index
+--> sqlite-vec dense index
+--> files, units, calls, and aliases
|
v
ranked result packets
|
v
exact source span fetched only when needed
Each indexed unit has three practical layers:
| Layer | Contents | Purpose |
|---|---|---|
| Retrieval key | Name, qualified name, signature, summary, concepts, relationships | Search and embedding input |
| Relationships | File, line, byte span, parent, calls, aliases | Navigation and impact analysis |
| Evidence | Exact source lines on disk | Final context, loaded on demand |
The index is stored per project in .urag/index.db. SQLite uses WAL mode, and
new embeddings are written in batches of 64 units.
Requirements:
- Python 3.12 or newer.
gitis optional, but is used for commit provenance and changed-file detection when available.- The default local embedding model downloads on first use and is cached locally. No API key is required for the default provider.
git clone https://github.com/Abas-Tim/urag.git
cd urag
uv sync
uv run urag --versionTo install the command globally with uv:
uv tool install .
urag --versionThe repository also contains bootstrap installers for macOS, Linux, and
Windows under bootstrap/.
Run these commands from a repository you want to index:
urag init --root /path/to/project --full
urag search "how does authentication work" --root /path/to/project
urag search "TokenValidator.validate" --mode lexical --top-k 3 --root /path/to/project
urag callers index_all --depth 3 --root /path/to/project
urag get UNIT_ID --root /path/to/projecturag init creates .urag/, the project configuration, and the database.
Use --full for the initial index. Later updates are incremental:
urag index --root /path/to/project
urag watch --root /path/to/projectwatch debounces filesystem events and re-indexes changed files. It can also
run a periodic full rescan with --rescan 30.
| Command | Description |
|---|---|
urag init |
Create project configuration and an empty index; add .urag/ to .gitignore |
urag init --full |
Create the project index and index all eligible files |
urag index |
Incrementally index new, changed, and deleted files |
urag watch |
Keep the index updated while files change |
urag search QUERY |
Search symbols and documentation |
urag resolve NAME |
Find an exact symbol definition by name |
urag callers NAME |
Find direct callers of a symbol |
urag callers NAME --depth 3 |
Find callers through multiple call-graph hops |
urag references NAME |
Find who references a symbol (types, constructions, bases, XAML) |
urag deadcode |
List candidate dead symbols (no incoming calls or references) |
urag callees UNIT_ID |
List what a unit calls (its call sites) |
urag dependents NAME |
Find what imports (depends on) a module or symbol |
urag symbols FILE |
List every indexed unit in a file |
urag read FILE |
Read a file (or a --start/--end line range) |
urag get UNIT_ID |
Fetch the exact source span for a search result |
urag recent |
Show recent git changes (branch, working tree, commits) |
urag status |
Show file, unit, embedding, language, and database statistics |
urag doctor |
Check index and embedding health |
urag embed |
Show or change the embedding model/provider |
urag classify QUERY |
Show the query class and selected context budget |
urag eval |
Compare retrieval systems on a question set |
urag mcp |
Run the MCP server over stdio |
Search supports three modes:
| Mode | Best for |
|---|---|
lexical |
Exact names, identifiers, paths, and configuration keys |
dense |
Conceptual or natural-language questions |
hybrid |
The default; combines lexical and dense results with RRF |
Use --json for machine-readable output (search, callers, references,
resolve, symbols, read, status, doctor, recent), --language to filter
results, and --evidence to include trimmed source spans. The get command
returns the full current span for a unit id.
Queries are routed with a deterministic, zero-model-cost classifier. The classifier selects a default result count and evidence budget based on the question shape:
| Class | Typical query | Default results | Evidence budget |
|---|---|---|---|
symbol |
TokenValidator.validate |
3 | 800 tokens |
local |
how does token validation work |
5 | 2,000 tokens |
debugging |
why does this crash across modules |
8 | 4,000 tokens |
impact |
what calls parse_token |
10 | 6,000 tokens |
Exact symbol queries are routed to lexical search. Impact queries are routed
to the call graph when a target symbol can be identified. --top-k and
query_class can override the defaults where supported.
The configured retrieval.max_evidence_tokens is a global ceiling and can
reduce the class budget; the generated default is 1,500 tokens.
During indexing, urag scans supported source files for call expressions and stores:
- The enclosing caller unit.
- The last callee segment, such as
validate. - The full written callee chain, such as
self.validateoros.path.exists. - The call-site line number.
This powers direct and multi-hop queries:
urag callers validate
urag callers validate --depth 3
urag search "what breaks if parse_token changes"Multi-hop traversal uses breadth-first search, records the shortest hop for each result, and terminates safely on cycles. Import aliases are resolved for Python, TypeScript, Go, Rust, and C# when the index contains the relevant bindings:
urag callers os.path.exists
urag callers core.http.fetchThe graph is static and intentionally approximate. Dynamic dispatch, reflection, generated code, and some instance-method chains are not resolved. The graph should be treated as impact evidence, not as a runtime dependency model.
The index also stores reference edges: type mentions, object constructions,
base classes, generic arguments, casts, attributes, and XAML bindings
(element tags, x:Class, DataType, {x:Static}, {StaticResource}, and
event handler attributes). This answers the questions a call graph cannot:
urag references MainWindow
urag references BoolToVisibilityConverter
urag deadcode
urag search "what references TokenValidator"callers also matches constructions (new MainWindow()). deadcode lists
candidate symbols with no incoming calls or references — a heuristic that
must be verified (e.g. with git grep) before code is removed, since
dynamic dispatch, reflection, and unsupported markup dialects can produce
false positives.
| Language | Extensions | Extracted information |
|---|---|---|
| Python | .py, .pyi |
Functions, classes, methods, imports, calls, aliases |
| TypeScript | .ts, .tsx, .mts, .cts |
Functions, classes, interfaces, types, enums, imports, calls, aliases |
| JavaScript | .js, .jsx, .mjs, .cjs |
Functions, classes, methods, imports, calls, aliases |
| Go | .go |
Functions, methods, structs, interfaces, imports, calls, aliases |
| Rust | .rs |
Functions, methods, structs, traits, enums, imports, calls, aliases |
| Java | .java |
Methods, constructors, classes, interfaces, enums, imports, calls |
| C | .c, .h |
Functions, structs, unions, typedefs, includes, calls |
| C++ | .cpp, .cc, .cxx, .hpp, .hh |
Functions, methods, classes, structs, namespaces, includes, calls |
| C# | .cs |
Classes, interfaces, structs, records, enums, methods, usings, calls, aliases, references |
| XML/XAML | .xaml, .axaml, .xml, .csproj, .props, .targets |
x:Class classes, x:Key resources, DataTemplate templates, event handlers, markup references |
| Markdown | .md, .markdown, .mdx |
Heading-based document chunks and hierarchy |
| JSON | .json |
config_key units with dotted qualnames |
| YAML | .yaml, .yml |
config_key units with dotted qualnames |
| TOML | .toml |
config_key units with dotted qualnames |
| INI | .ini, .cfg, .conf, .properties |
config_key units with dotted qualnames |
| Env | .env, .env.* |
config_key units for KEY=value entries |
Files are filtered by .gitignore, built-in exclusions, configured languages,
and a default maximum size of 1 MB. These settings are configurable in
.urag/urag.toml.
The default provider is a local ONNX model through FastEmbed:
[embedding]
provider = "local"
model = "BAAI/bge-base-en-v1.5"
dimension = 768The model is downloaded on first use and cached in %LOCALAPPDATA%/urag on
Windows or ~/.cache/urag on other systems. An OpenAI-compatible HTTP
embedding endpoint is also supported:
[embedding]
provider = "http"
dimension = 768
http_url = "http://localhost:11434/v1"
http_model = "nomic-embed-text"
http_api_key = ""Keep API keys in the local .urag/urag.toml only and do not commit them.
Projects that already have an index can use provider = "none" for
lexical-only retrieval; dense retrieval and embedding new units require an
embedding provider.
Switch models without editing the TOML:
urag embed # show current embedding config
urag embed --model BAAI/bge-small-en-v1.5 # switch model (auto-detects dimension)
urag embed --model BAAI/bge-small-en-v1.5 --reindex
urag embed --provider http --dimension 768 # switch providerurag embed clears the old model's vectors from the index, removes the old
model's files from the local cache, and saves the new configuration. The
next urag index re-embeds everything with the new model; pass --reindex
to do it in the same run, or --keep-cache to keep the old model's files.
Mismatched model/dimension pairs are rejected at startup.
Common local model choices:
| Model | Dimensions | Storage per unit | Model RAM | Notes |
|---|---|---|---|---|
BAAI/bge-small-en-v1.5 |
384 | ~9 KB | ~150 MB | Smallest footprint |
BAAI/bge-base-en-v1.5 (default) |
768 | ~18 KB | ~500 MB | Best quality per byte |
BAAI/bge-large-en-v1.5 |
1024 | ~24 KB | ~1.3 GB | Roughly base quality, 3x cost |
intfloat/multilingual-e5-small |
384 | ~9 KB | ~150 MB | Multilingual codebases |
Vector storage scales linearly with the dimension; model RAM is a one-time cost independent of repository size. Because urag retrieval is hybrid, the lexical index carries exact-name queries even when dense quality is modest.
For Git repositories, indexed files record the commit used at indexing time.
Search and get results include the short commit and a stale flag when the
file differs from that indexed revision. Non-Git projects use file metadata
checks instead.
Freshness detection is best-effort. Run urag index after a branch switch,
pull, or other large repository change before relying on old evidence. Static
analysis also cannot see runtime-generated relationships.
Run urag as a stdio MCP server for an agent harness:
urag mcp --root /path/to/projectExample generic MCP configuration:
{
"mcpServers": {
"urag": {
"command": "urag",
"args": ["mcp", "--root", "/path/to/project"]
}
}
}The server exposes these tools:
search: Search symbols and documentation with compact result packets.fetch_unit: Fetch exact source lines for a result id (includes metadata).fetch_units: Batch-fetch several unit ids in one call.callers: Query direct or multi-hop callers.callees: List what a unit calls (call sites).dependents: Find what imports a module or symbol.resolve: Exact symbol definition lookup by name.children: List the members (methods) of a class/struct.list_files: List indexed files with language and unit counts.list_symbols: List every indexed unit in a file.read_file: Read a file or a line range by path.recent_changes: Report git branch, HEAD, working-tree changes, and recent commits.index_now: Incrementally re-index changed files.status: Return index statistics, config, and git state.init_project: Create and populate an index for a project. Passembed=falsefor a fast lexical-only index.
The intended agent workflow is to search with top_k=3-5, fetch exact spans
only for the most relevant one to three units, and call index_now after
changes. Agents can also browse with list_files/list_symbols/read_file,
resolve known symbols with resolve, and answer impact questions with
callers/callees/dependents.
The repository includes skills/urag/SKILL.md, a small instruction file for
agent harnesses. It teaches the search-first, fetch-evidence-on-demand
workflow and is included in built packages.
For harnesses that use local skill directories, install it with the harness's normal skill installation mechanism. For example:
cp -r skills/urag ~/.config/opencode/skills/urag
cp -r skills/urag ~/.claude/skills/uragThe evaluation harness compares urag with the context-gathering tools an
opencode agent normally uses (grep matching lines, read of whole files
located via grep) plus a naive chunk-RAG baseline, all on the same questions:
urag eval --root . --autogen 10 --top-k 5
urag eval --root . --questions questions.jsonl
urag eval --root . --transitive 25 --alias 25 \
--systems urag-callers,urag-transitive,urag-hybrid
uv run python benchmarks/run_bench.py --self --yesIt measures fractional unit recall@k, file recall@k, precision, MRR, approximate tokens per retrieval (chars/4 of the context actually returned by each system), and p50/p95 latency. Definition and call questions can be generated with provable gold data from the index. Custom conceptual questions can be supplied as JSONL:
{"query": "where is TokenValidator defined", "gold_file": "src/auth.py"}run_bench.py wipes and rebuilds the target .urag/ index on every run
(--yes confirms non-fixture roots), times the index build, writes a JSON
report to benchmarks/reports/, and renders a detailed self-contained HTML
report next to it: aggregate tables, charts, and per-question retrieval
samples from every system with auto-generated explanations of where urag wins
or loses. Before/after comparisons reuse a previous report's questions and
re-derive gold against the new index (unit ids are index-specific), or diff
two finished reports:
uv run python benchmarks/run_bench.py --reuse-questions benchmarks/reports/<old>.json
uv run python benchmarks/run_bench.py --compare <before>.json <after>.jsonThe values below are historical reference measurements from one environment,
using top_k=5 (newer runs also include the read opencode-style baseline,
which these predate). They are not performance guarantees; rerun the
benchmark before comparing new results — reports are generated into
benchmarks/reports/ (gitignored) and the HTML rendering includes per-system
data samples and score breakdowns.
| Dataset | System | Questions | Unit recall | Precision | MRR | p50 | Mean compact tokens |
|---|---|---|---|---|---|---|---|
| Synthetic fixture | urag-callers |
22 | 1.000 | 1.000 | 1.000 | 0.14 ms | 5.0 |
| Synthetic fixture | urag-transitive |
22 | 1.000 | 1.000 | 1.000 | 0.11 ms | 6.4 |
| urag repository | urag-callers |
47 | 1.000 | 1.000 | 1.000 | 0.48 ms | 41.4 |
| urag repository | urag-transitive |
47 | 1.000 | 1.000 | 1.000 | 10.38 ms | 78.2 |
| urag repository | urag-hybrid |
57 | 0.316 | 0.077 | 0.252 | 4.38 ms | 134.7 |
The historical reports predate the current index-lifecycle and retrieval-quality
changes and the default embedding model change (bge-small 384d to bge-base
768d); rerun the benchmark before comparing new results. The transitive system
reached indirect_recall=1.000 on the synthetic
fixture and 0.440 on the checked-in urag repository report. In the current
evaluation harness, unit recall is fractional across the gold units for a
question. Graph systems
also run on graph-eligible questions, while the hybrid row includes the full
generated question set, so the rows are directional rather than a universal
ranking. Multi-hop gold is generated with the same caller-resolution path the
production retrieval uses, so gold is consistent with what urag can find;
caller matching by bare last segment can still conflate same-named symbols
across modules — a caveat for --self runs, not the synthetic fixture.
The efficiency gain urag is designed to provide comes from returning compact metadata first and loading source spans only when requested. Actual latency, token count, and retrieval quality depend on repository size, embedding provider, query type, and index state.
uv sync
uv run pytest -q
uv run urag --versionCI runs the test suite and CLI smoke check on Python 3.12 and 3.13. The test suite covers extractors, call extraction, multi-hop traversal, alias resolution, evaluation metrics, and benchmark fixtures.
MIT. See LICENSE.