Standalone retrieval engine for coding agents. Parse a multi-language repo into blueprint nodes with tree-sitter (via codeflow-core), embed one document per node, store everything in LanceDB, and serve hybrid vector + lexical search with graph-traversal context expansion. Optional LLM-synthesized answers on top. Ships a CLI, an MCP server, and an HTTP service.
- Indexing.
RepoIndexerwalks a repo, builds aGraphSnapshot(graph + source spans + call sites), synthesizes a markdown document per node, embeds it, and writes to LanceDB. Incremental by default. - Embedding. Three providers:
LocalHashEmbeddingProvider(zero-setup token-hash, 256-dim),OnnxEmbeddingProvider(Xenova/gte-small, 384-dim),GeminiEmbeddingProvider(REST, 768-dim). - Vector store.
LanceVectorStore— singlenode_documentstable backed by LanceDB on disk. - Retrieval. Hybrid search (
searchDocuments): vector candidates + lexical candidates, weighted score, thenrerankResultsfor exact-symbol boost. - Graph traversal.
traverseDependenciesBFS overgraph.edgesfor bothdependenciesanddependents. - Multi-hop. Optional question decomposition → parallel sub-question retrieval → merge → synthesis.
- LLM synthesis.
OpenAiCompatibleTransportandCustomHttpTransportwith retry on 408/425/429/5xx and a system-role fallback. - MCP server. Stdio-only
McpServerwithquery,lookup,explain,impact,statustools. - HTTP service. Bearer-token auth, zod-validated bodies, security headers, in-memory metrics.
- Git hook.
installPostCommitHookwrites anpx coderag reindexhook on first index, idempotent. - CLI.
coderagwithsetup,init,index,reindex,query,serve-mcp,serve-http,doctor. - Config.
coderag.config.jsonwithCODERAG_*env var overrides for every field.
| Subpath | Module |
|---|---|
. |
Barrel: CodeRag class, createCodeRag, providers, store, config, errors, types. |
./cli |
runCli(argv). |
./mcp |
createMcpServer, serveStdioMcpServer. |
The CLI binary coderag ships under bin/.
import { CodeRag, createCodeRag, loadCodeRagConfig } from '@abhinav2203/coderag';
const coderag = await createCodeRag(loadCodeRagConfig());
await coderag.index();
const result = await coderag.query("how does the user login?");
const lookup = await coderag.lookup("authenticateUser");
const explain = await coderag.explain("authenticateUser", { depth: 2 });
const impact = await coderag.impact("authenticateUser", { depth: 2 });
const status = await coderag.status();
await coderag.close();interface CodeRagConfig extends SerializableCodeRagConfig {
logger?: Logger;
embeddingProvider?: EmbeddingProvider;
vectorStore?: VectorStore;
graphProvider?: GraphProvider;
llmTransport?: LlmTransport;
configPath?: string;
}
interface SerializableCodeRagConfig {
repoPath: string;
storageRoot: string; // default ".coderag"
embedding: EmbeddingConfig; // default provider="local-hash", dimensions=256
retrieval: RetrievalConfig; // topK=6, rerankK=3, maxContextChars=50000
multiHop: MultiHopConfig; // enabled=false, maxSubQuestions=5, expansionDepth=1
traversal: TraversalConfig; // defaultDepth=1, maxDepth=3
locking: LockingConfig; // timeoutMs=30000, pollMs=150, staleMs=300000
service: ServiceConfig; // host="127.0.0.1", port=4119
llm: SerializableLlmConfig; // enabled=false, transport="openai-compatible"
docsPath?: string; // external markdown dir
}
interface IndexedNodeDocument {
nodeId: string;
name: string;
kind: BlueprintNodeKind;
filePath: string;
summary: string;
signature?: string;
doc: string; // generated markdown
sourceText?: string; // raw file slice
vector: number[]; // 256 / 384 / 768 dim
startLine: number;
endLine: number;
}
interface GraphSnapshot {
provider: string;
repoPath: string;
generatedAt: string;
graph: BlueprintGraph;
sourceSpans: Record<string, SourceSpan>;
callSites: Record<string, CallSite>;
}
interface RetrievedNodeContext {
nodeId: string;
name: string;
kind: BlueprintNodeKind;
filePath: string;
fullFileContent: string;
startLine: number;
endLine: number;
callSiteLines: number[];
doc: string;
relationship: "primary" | "calls" | "called-by" | "multi-hop";
subQuestionIndex?: number;
}
interface QueryResult {
question: string;
answerMode: "llm" | "context-only";
retrievalMode: "single" | "multi-hop";
answer: string;
context: ContextPackage;
}One document per BlueprintNode produced by analyzeRepo(repoPath) from @abhinav2203/codeflow-core. The graph provider walks the source tree and emits nodes, edges, sourceSpans (nodeId → {filePath, startLine, endLine, symbol}), and callSites (keyed calls:fromId:toId, with lineNumbers and expressions).
Supported languages (declared in the adapter and package.json keywords): TypeScript, JavaScript, Go, Python, C, C++, Rust. Excluded directories: node_modules, .git, .next, dist, build, target, __pycache__, vendor, .venv, artifacts, coverage.
buildNodeDocument(node, span, snapshot) produces a markdown block of:
# <name>
Kind: <kind>
Path: <path>
File Name: <basename>
Lines: <start>-<end>
Signature: <signature>
Summary: <node.summary>
Responsibilities: <list>
Inputs: <list>
Outputs: <list>
Declared Dependencies: <list>
Source References: <list>
Calls: <list>
Called By: <list>
The actual embedding text per node is [doc, sourceText].join("\n\n"), optionally replaced by await fs.readFile(${docsPath}/${nodeId}.md) when docsPath is supplied. The text gets truncated to embeddingProvider.maxInputTokens * 4 chars.
| Provider | Model | Dim | Notes |
|---|---|---|---|
local-hash (default) |
FNV-1a token bucket | 256 | Deterministic, zero setup. |
onnx |
Xenova/gte-small |
384 | Local via @xenova/transformers, mean-pooled, batch size 1. Model under <onnxModelDir>/Xenova/gte-small/. |
gemini |
models/gemini-embedding-2 |
768 | REST to generativelanguage.googleapis.com. 60 RPM / 3 concurrency default. |
- Path:
<storageRoot>/lancedb/ - Table:
node_documents(single table) - Schema:
nodeId, name, kind, filePath, summary, signature, doc, vector, startLine, endLine - Sidecar:
<storageRoot>/lancedb/store-metadata.json— embedding fingerprint - Other persisted files:
index-manifest.json,graph-snapshot.json,documents.json,index.lock.json
checkEmbeddingModelMismatch()— compares{provider, model, dimensions}andschemaVersionagainst the persisted manifest. Mismatch withoutforceFullthrows anIndexingErrordirecting the user tocoderag reindex.IndexLock.withLock("index", ...)— file lock withmtime-based stale detection.buildGraphSnapshot(repoPath, graphProvider)— single tree-sitter pass.buildIndexedDocuments(snapshot, embeddingProvider, docsPath, logger)— walk nodes, prepare, embed in chunks.buildIndexManifest(...)— SHA-256 of each doc and each source file for incremental diff.diffNodeIds(previous, next):forceFull || !previousManifest→vectorStore.reset(records)(table.add({mode: "overwrite"})).- Otherwise →
deleteByNodeIds(removedNodeIds)+upsert(changedNodeIds).
Promise.allwrites manifest, snapshot, documents, vector-store metadata.ensurePostCommitHook()— installs the hook if missing.
ensureLoadedState()returns the cached{snapshot, documents}or rebuilds vialoadState()/waitForUnlockedState()/runIndexJob().embeddingProvider.embed(expandQuestion(question)).expandQuestionadds synonyms fromQUERY_SYNONYMS(e.g.concurrent → [lock, shared, process]).- Candidate generation:
- Vector: up to
max(topK*3, rerankK)viavectorStore.vectorSearch(Float32Array.from(queryVector)). - Lexical: top
max(topK*4, rerankK)sorted bynameScore*0.35 + summaryScore*0.3 + pathScore*0.2 + signatureScore*0.15 + docLexical*0.2.
- Vector: up to
- Scoring — for each candidate, weighted sum of:
vectorScore = cosineSimilarity(...)(0.28)lexicalScore = lexicalOverlapScore(...)(0.18)fieldScore— name + summary + path + signature overlap (0.24)coverageScore = weightedTokenScore(...)(0.15)idfScore = calculateIdfScore(...)(0.05)exactNameBoost(0.18 if symbol-like, else 0.04) andexactPathBoost(0.14 if symbol-like)symbolBoost(0.10 when both exact boosts fire on a symbol-like query)largeNodePenalty—min(0.12, log2(lineSpan/500 + 1) * 0.04)for nodes > 500 lines
- Re-rank —
rerankResultsadds+0.2on exact-name,+0.18on exact-path,+0.08when the query contains the node name, then slices torerankK. - Graph expansion —
traverseDependencies(snapshot, primaryNodeId, depth)for both directions, respectingtraversal.maxDepth. - Context assembly —
buildContextPackage()usesFileCache(mtime-keyed) to read full files, then fitsretrieval.maxContextChars— primary first, then related — recording warnings for truncated or dropped files. - LLM synthesis (when
llm.enabled) —buildMessages()produces{system, user};OpenAiCompatibleTransportPOSTs/chat/completions, streams via SSE ifonTokenis set. If the upstream returns 400 "system role not supported", the system prompt folds into the first user message and retries. - Return —
{question, answerMode, retrievalMode, answer, context}. Withllm.enabled === false,answerMode = "context-only"andansweris a fallback string.
Triggered when options.multiHop === true AND multiHop.enabled === true AND an LLM is configured.
decomposeQuestionWithFallback(question, llmTransport, multiHopConfig, model):shouldDecompose()heuristic gate (score ≥ 2 onand|vs|versus, multiple?, >25 words, multi-topic keyword).decomposeQuestion()asks the LLM for a JSON array of sub-questions, strips code fences, validates, caps atmaxSubQuestions.- Returns
nullon any failure → single retrieval.
multiHopRetrieve(subQuestions, ...)runsPromise.all(retrieveForSubQuestion(sq, ...))for each sub-question withexpansionDepthtraversal.deduplicateAndMerge()walks per-sub-question results in order, first occurrence of eachnodeIdwins.buildMultiHopContextPackage()regroups nodes bysubQuestionIndex, builds a unifiedgraphSummary.buildMultiHopMessages()produces per-sub-question sections, then a system prompt asking the model to address each sub-question and synthesize.
★ Insight ─────────────────────────────────────
The decomposition step is the most fragile part of multi-hop. The heuristic gate is conservative, the JSON parser is strict, and any failure falls back to single retrieval. The reasoning: a partial decomposition that returns a bad sub-question list will produce a worse answer than just answering the original question.
─────────────────────────────────────────────────
Transport: stdio (StdioServerTransport). Server identity: McpServer({ name: "coderag", version: "0.2.1" }).
| Tool | Input | Behavior |
|---|---|---|
query |
{question, depth?, multiHop?} |
Calls coderag.query(); returns the full QueryResult JSON. |
lookup |
{identifier} |
Resolves by exact id / case-insensitive name / case-insensitive path / substring match → LookupResult. |
explain |
{identifier, depth?} |
ExplainResult with dependencies + dependents from BFS. |
impact |
{identifier, depth?} |
ImpactResult with upstream dependents (the things that would be impacted). |
status |
{} |
Returns the status() object. |
serveStdioMcpServer() calls ensureIndexIsCurrent() first:
- If
status.indexed === false→ runscoderag.index(). - If
status.modelMismatch === true→ runscoderag.reindex({ full: true }).
There is no HTTP-mode MCP. For HTTP exposure, the serve-http CLI command exposes the same five operations plus /v1/index, /v1/reindex, /health, /ready, /metrics with optional bearer-token auth.
createHttpServer() exposes:
POST /v1/query,/v1/lookup,/v1/explain,/v1/impact,/v1/index,/v1/reindexGET /v1/status,/health,/ready,/metrics
Bearer-token auth for /v1/* when service.apiKey is set. Security headers: content-security-policy: default-src 'none', x-frame-options: DENY, referrer-policy: no-referrer, cache-control: no-store. 1MB body cap. zod validation on every request body. Timing-safe bearer comparison.
coderag setup
coderag init [--config path] [--json]
coderag index [--config path] [--json]
coderag reindex [--config path] [--full] [--json]
coderag query "question" [--config path] [--depth 2] [--multi-hop] [--json]
coderag serve-mcp [--config path]
coderag serve-http [--config path]
coderag doctor [--config path] [--json]
setup— interactiverunSetupWizard(embedding provider, LLM provider, paths) → writescoderag.config.jsonand.env, installs git hook.init—coderag.index()+installPostCommitHook. Prints indexed count or JSON.index—coderag.index(). Idempotent on matching fingerprint.reindex --full— forces avectorStore.reset.query "question"—coderag.query(). Streams tokens unless--json.serve-mcp/serve-http— start the corresponding server.doctor—indexed, indexedNodeCount, generatedAt, repoPath, storageRoot, provider, llmEnabled(or JSON).
After every command, coderag.close() releases the vector store and file cache.
coderag.config.json (see /Users/abhinavnehra/git/CodeFlow/coderag.config.json for a working example):
Cross-field invariants enforced by loadCodeRagConfig:
retrieval.rerankK <= retrieval.topKtraversal.defaultDepth <= traversal.maxDepthmultiHop.expansionDepth <= traversal.maxDepth
Every field can be overridden by a CODERAG_* environment variable.
coderag/
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── coderag.config.json
└── src/
├── index.ts public barrel
├── cli.ts runCli(argv)
├── types.ts zod schemas + TS interfaces
├── bin/coderag.ts shebang entry, re-imports cli.js
├── adapters/
│ └── codeflow-core.ts CodeflowCoreGraphProvider, buildGraphSnapshot
├── cli/
│ └── setup-wizard.ts runSetupWizard
├── errors/index.ts CodeRagError + ConfigurationError, IndexingError, TransportError, NotFoundError
├── indexer/
│ ├── documents.ts buildNodeDocument, buildIndexedDocuments
│ ├── embedder.ts LocalHashEmbeddingProvider
│ ├── onnx-embedder.ts OnnxEmbeddingProvider
│ ├── gemini-embedder.ts GeminiEmbeddingProvider + RateLimiter
│ ├── indexer.ts RepoIndexer
│ └── git-hook.ts installPostCommitHook
├── llm/
│ ├── prompt.ts buildMessages, buildMultiHopMessages
│ ├── transports.ts OpenAiCompatibleTransport, CustomHttpTransport
│ ├── context-builder.ts buildContextPackage
│ └── multi-hop-context-builder.ts
├── mcp/
│ └── server.ts createMcpServer, serveStdioMcpServer
├── retrieval/
│ ├── search.ts searchDocuments, rerankResults
│ ├── traversal.ts traverseDependencies
│ ├── multi-hop.ts parallelRetrieve, deduplicateAndMerge, multiHopRetrieve
│ ├── decompose.ts shouldDecompose, decomposeQuestionWithFallback
│ └── page-index.ts createRetrievedNodeContext
├── service/
│ ├── coderag.ts CodeRag class (public high-level API)
│ ├── config.ts loadCodeRagConfig, loadSerializableConfig, resolveRuntimeConfig
│ ├── http.ts createHttpServer, serveHttpServer
│ └── http-metrics.ts HttpMetricsCollector
├── store/
│ ├── manifest-store.ts ManifestStore
│ ├── index-lock.ts IndexLock.withLock
│ ├── vector-store.ts LanceVectorStore
│ └── file-cache.ts mtime-keyed FileCache
├── utils/
│ ├── filesystem.ts ensureDir, fileExists, readJson, writeJson, hashContent
│ ├── text.ts tokenizeMeaningfully, tokensRoughlyMatch, cosineSimilarity
│ └── logger.ts createConsoleLogger
└── test/ 34 vitest files
- Schema version 2 is enforced; any mismatch forces a full reindex.
- The ONNX model download is ~33 MB into
.coderag-models/models/Xenova/gte-small/. First index is slow. Subsequent indexes are incremental. - The git post-commit hook is idempotent and marked
# Added by CodeRagso it survives upgrades. The previous hook backs up topost-commit.coderag.previous. - The HTTP server defaults to
127.0.0.1:4119. There is no HTTPS support; put it behind a reverse proxy if exposing it. engines.noderequires Node 20+.
{ "repoPath": "<absolute or relative path>", "storageRoot": ".coderag", "embedding": { "provider": "onnx" | "local-hash" | "gemini", "dimensions": 384, "geminiModel": "models/gemini-embedding-2", "timeoutMs": 30000, "onnxModelDir": ".coderag-models/models" }, "retrieval": { "topK": 6, "rerankK": 3, "maxContextChars": 16000, "primaryDocLimit": 1200, "primaryFileLimit": 4000, "relatedDocLimit": 320, "relatedFileLimit": 1200 }, "multiHop": { "enabled": false, "minQuestionLength": 25, "maxSubQuestions": 5, "expansionDepth": 1 }, "traversal": { "defaultDepth": 1, "maxDepth": 3 }, "locking": { "timeoutMs": 30000, "pollMs": 150, "staleMs": 300000 }, "service": { "host": "127.0.0.1", "port": 4119, "apiKey": "optional" }, "llm": { "enabled": true, "transport": "openai-compatible" | "custom-http", "baseUrl": "https://api.openai.com/v1", "model": "gpt-4o", "apiKey": "sk-...", "timeoutMs": 45000, "customHttpFormat": "json" | "ndjson" | "sse", "headers": {} }, "docsPath": "<optional dir of ${nodeId}.md files>" }