A multi-agent LLM code-review tool. A context agent builds a RAG index over your codebase and feeds a review agent that reasons over your git diff — grounding every comment in the surrounding code. The review streams back token-by-token, on the terminal or over Server-Sent Events.
git diff ─┐
├──► context agent (RAG) ──► review agent ──► streamed review
codebase ─┘ embeds + retrieves grounded prompt (stdout / SSE)
- TypeScript / Node.js, zero global state, fully typed.
- Pluggable providers — Ollama, Gemini, Mistral — swappable via one config key.
- RAG retrieval so the reviewer sees related code, not just the diff.
- Retry + fallback on every remote LLM call.
- Token streaming to the terminal and over SSE.
The review runs as a four-stage pipeline coordinated by an orchestrator. State threads through every stage, and the orchestrator emits a stream of events so the CLI and the SSE server consume the same run.
| Stage | Owner | What happens |
|---|---|---|
1. indexing |
Context agent | Walks the repo, chunks files into overlapping windows, embeds them, stores vectors in an in-memory store. |
2. diffing |
Git module | Extracts the diff (uncommitted changes or a ref range) and splits it per file. |
3. retrieving |
Context agent | For each changed file, embeds the diff and retrieves the top-k most similar chunks (cosine similarity). |
4. reviewing |
Review agent | Builds one grounded prompt (diff + retrieved context), then streams the model's review token-by-token. |
The two agents are intentionally separate: the context agent is the only part that touches the filesystem and the embedding model; the review agent is pure prompt-assembly + generation. They never call each other directly — the orchestrator threads state between them.
src/
├── index.ts CLI entry (commander): `review` and `serve`
├── config/
│ └── config.ts Config schema, defaults, JSON loader, API-key resolution
├── providers/
│ ├── provider.ts AIProvider interface (embed + streaming generate) + ProviderError
│ ├── ollama.ts Local Ollama (newline-delimited JSON stream)
│ ├── gemini.ts Google Generative Language API (SSE stream)
│ ├── mistral.ts Mistral (OpenAI-compatible SSE stream)
│ └── factory.ts Builds a provider from config — the ONLY place that knows concrete classes
├── rag/
│ ├── chunker.ts Line-aligned overlapping chunking
│ └── vectorStore.ts In-memory cosine-similarity top-k store
├── git/
│ └── diff.ts git diff extraction (simple-git) + per-file split
├── agents/
│ ├── contextAgent.ts Stage 1 & 3: index the repo, retrieve context per diff
│ └── reviewAgent.ts Stage 4: build grounded prompt, stream the review
├── pipeline/
│ ├── state.ts PipelineState + PipelineEvent types
│ └── orchestrator.ts Coordinates all stages; async generator of events
├── server/
│ └── sseServer.ts Express SSE endpoint that forwards pipeline events
└── utils/
├── logger.ts Leveled logger (writes to stderr, never pollutes streamed output)
└── retry.ts Exponential backoff + provider fallback
- Node.js >= 18.17 (uses native
fetchand web streams) - A provider:
- Ollama running locally (
ollama serve) with a chat model + an embedding model, or - a Gemini API key, or
- a Mistral API key
- Ollama running locally (
npm install
npm run build # compiles TypeScript to dist/
npm link # optional: makes `codelens` available globallyRun without building during development:
npm run dev -- review --repo /path/to/repocodelens review
# or explicitly
codelens review --repo /path/to/repocodelens review --range "main..HEAD"
codelens review --range "HEAD~3..HEAD"codelens serve
# then, from another terminal — note -N to disable curl buffering so you see tokens live:
curl -N "http://localhost:4000/review?repo=$(pwd)&range=main..HEAD"The server emits named SSE events: stage, token, done, error.
codelens [--log-level debug|info|warn|error] <command>
review -r, --repo <path> repository to review (default: cwd)
-c, --range <range> git range (default: uncommitted changes)
serve start the SSE server on the configured port
Drop a codelens.config.json in the repo you're reviewing (an example is included). The whole pipeline is driven by the single provider key — change it and every stage re-routes to that backend:
{
"provider": "mistral",
"fallbackProvider": "gemini",
"providers": {
"ollama": { "model": "llama3.1", "embedModel": "nomic-embed-text", "baseUrl": "http://localhost:11434" },
"gemini": { "model": "gemini-1.5-flash", "embedModel": "text-embedding-004", "apiKeyEnv": "GEMINI_API_KEY" },
"mistral": { "model": "mistral-small-latest","embedModel": "mistral-embed", "apiKeyEnv": "MISTRAL_API_KEY" }
},
"rag": { "chunkSize": 1200, "chunkOverlap": 200, "topK": 6, "include": [".ts", ".js"], "exclude": ["node_modules", "dist"] },
"retry": { "maxAttempts": 3, "baseDelayMs": 500 },
"server":{ "port": 4000 }
}API keys are read from environment variables named by apiKeyEnv — never stored in the config file:
export GEMINI_API_KEY=...
export MISTRAL_API_KEY=...| Key | Meaning |
|---|---|
provider |
The active backend. This one key swaps the whole pipeline. |
fallbackProvider |
Secondary backend the retry layer escalates to if the primary is down. |
rag.chunkSize / chunkOverlap |
Chunk window and overlap (characters). |
rag.topK |
Chunks retrieved per changed file. |
rag.include / exclude |
File extensions to index / path segments to skip. |
retry.maxAttempts / baseDelayMs |
Backoff schedule for remote calls. |
- One interface, three backends. Everything depends on
AIProvider(embed+ streaminggenerate). The provider factory is the only module that imports concrete provider classes, so adding a backend is a new file plus onecase. - Streaming is uniform. Each provider normalizes its wire format (Ollama's NDJSON, Gemini/Mistral SSE) into the same
AsyncIterable<GenerateChunk>. The orchestrator yields those tokens as events, and both stdout and SSE forward them unchanged. - The orchestrator is an async generator of events. That is what lets the CLI and the HTTP server share one code path — neither owns the pipeline.
- Logs go to stderr. Streamed review text owns stdout, so piping/redirecting the review never mixes in log lines.
- The vector store is swappable. It exposes only
addandsearch, so the in-memory cosine store can be replaced with pgvector / Chroma / Pinecone without touching the agents.
See ARCHITECTURE.md for the data flow in depth and the rationale behind each decision.