LAIN builds a map of how all the code in your project connects — what calls what, what depends on what, which files tend to change together. Then it lets your AI coding assistant ask questions about that map. So instead of the AI just looking at one file and guessing, it can ask "if I change this function, what else breaks?" and get a real answer. It plugs into any AI agent that supports MCP and runs in the background while you work.
# One-line install (interactive - will ask you to configure and add to PATH)
curl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | bash
# After install: reload your shell (or open a new terminal)
source ~/.zshrc # or ~/.bashrc
# Or non-interactive (skips prompts, auto-adds to PATH)
curl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | \
bash /dev/stdin --workspace . --transport both --yesLain is a persistent code-intelligence MCP server. It builds a queryable knowledge graph of your codebase — symbols and their relationships extracted via LSP and tree-sitter, augmented with git co-change history and optional semantic embeddings — and exposes that graph through MCP tools. The value over LSP-only or RAG-based approaches is cross-file structural reasoning: blast radius for proposed changes, transitive dependency traces, anchor identification, co-change correlation, and contextual build failure decoration so agents can reason about callers rather than just the failing line. Written in Rust, persists across sessions, stays fresh during editing via a file watcher that updates a volatile overlay layered on top of the static graph.
curl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | bashThe installer will ask you to configure:
- Workspace path
- MCP transport mode (stdio, http, or both)
- HTTP port (if using http/both)
- Target agent (auto-detects Claude Code, Cursor, Windsurf, Cline)
- Whether to download the ONNX model for semantic search
After you confirm your settings, it will:
- Download and install LAIN to
~/.local/lain - Optionally download the ONNX model (~120MB)
- Run
lain initwith your configuration - Add LAIN to your agent's settings
Non-interactive install (with options):
# Install with specific workspace and download ONNX model for semantic search
curl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | \
bash /dev/stdin --workspace . --transport both --download-model --yes
# Install for specific agent
curl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | \
bash /dev/stdin --agent cursor --yes
# See all options
curl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | \
bash /dev/stdin --helpInstall options:
| Option | Description | Default |
|---|---|---|
--workspace PATH |
Workspace path for LAIN | . |
--transport MODE |
MCP transport: stdio, http, both | stdio |
--port PORT |
HTTP port for MCP server | 9999 |
--agent AGENT |
Target agent: auto, claude, cursor, windsurf, cline | auto |
--embedding-model PATH |
Path to ONNX embedding model | - |
--download-model |
Download default ONNX model (all-MiniLM-L6-v2.onnx, ~120MB) | - |
-y, --yes |
Skip all confirmation prompts | - |
After installation:
# Reload your shell (the installer adds to ~/.zshrc or ~/.bashrc automatically)
source ~/.zshrc # or ~/.bashrc, then open a new terminal
# Verify installation
lain --version
# Query the graph
lain query "find Function | limit 5"brew tap spuentesp/lain https://github.com/spuentesp/lain
brew install lain
# Initialize
lain initDownload the latest release for your platform from GitHub releases, then:
# Make executable
chmod +x lain
# Run directly
./lain --workspace /path/to/your/project --transport stdio# Clone the repo
git clone https://github.com/spuentesp/lain.git
cd lain
# Build (requires Rust 1.75+)
cargo build --release
# Binary will be at ./target/release/laincurl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | bash# Auto-detect agent (Claude Code, Cursor, Windsurf, Cline)
lain init
# Or specify agent explicitly
lain init --agent claude# Standard mode (for Claude Code)
lain --workspace /path/to/project --transport stdio
# With HTTP diagnostics (web UI at http://localhost:9999)
lain --workspace /path/to/project --transport both --port 9999
# With semantic search (requires ONNX model)
lain --workspace /path/to/project --embedding-model ~/.local/lain/models/all-MiniLM-L6-v2.onnxIf you work on several repos, register them so lain works without --workspace:
lain projects add lain ~/code/lain # registers under basename
lain projects add other ~/code/other-thing # arbitrary name
lain projects list # see registered projects
lain use lain # mark as active
# Now `lain query "..."` and `lain init` use the active project
# without typing the path each time.lain init auto-registers the project under its directory basename, so
first-time use is frictionless.
# Check health and LSP status
curl -s -X POST http://localhost:9999/mcp -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_health","arguments":{}},"id":1}'
# Query the graph directly
lain query "find Function | limit 5"JSON-based ops array for flexible graph traversals:
{
"ops": [
{ "op": "find", "type": "Function" },
{ "op": "connect", "edge": "Calls", "depth": { "min": 1, "max": 3 } },
{ "op": "filter", "label": "test" },
{ "op": "semantic_filter", "like": "error handling", "threshold": 0.35 },
{ "op": "limit", "count": 10 }
]
}Available ops: find, connect, filter, semantic_filter, group, sort, limit
get_call_chain— Shortest path between two functionsget_blast_radius— Everything affected by a changetrace_dependency— What a symbol depends onget_coupling_radar— Files that change together
find_anchors— Most-called, most-stable symbols (architectural pillars)list_entry_points— Findmain(), route handlers, app initializationget_context_depth— How far from an entry point (abstraction layers)explore_architecture— High-level tree of modules and files
semantic_search— Find code by meaning, not just names. Uses local ONNX embeddings with hybrid scoring (cosine similarity + stemmed token-overlap) and shows body excerpts in the response. BGE-small-en-v1.5 is the recommended model (better than MiniLM for technical corpora); use a query prefix to enable BGE-style asymmetric retrieval.
find_dead_code— Potentially unreachable code (filters trait defaults, common names)suggest_refactor_targets— High-coupling, low-stability nodes
Lain enriches build failures with architectural context:
run_build— Build with Rust/Go/JS/Python toolchain error parsingrun_tests— Tests with error enrichmentrun_clippy— cargo clippy with context
lain projects add <name> <path>— register a projectlain projects list— show registered projectslain projects forget <name>— remove a projectlain projects current— show the active projectlain use <name>— set the active project (solainwithout--workspaceuses it)
find_dead_code— Potentially unreachable code (filters trait defaults, common names)suggest_refactor_targets— High-coupling, low-stability nodes
Lain enriches build failures with architectural context:
run_build— Build with Rust/Go/JS/Python toolchain error parsingrun_tests— Tests with error enrichmentrun_clippy— cargo clippy with context
| Requirement | Details |
|---|---|
| Rust | 1.75 or newer |
| Git | Required for co-change analysis |
| ONNX Model | Optional — for semantic search |
For semantic_search to work, you need an ONNX embedding model. The easiest way to set this up is using the provided install script:
./scripts/install.shAlternatively, you can set it up manually:
# Create model directory
mkdir -p .lain/models
# Option A: bge-small-en-v1.5 (recommended — better MTEB scores, 384d, ~120MB)
curl -L https://huggingface.co/BAAI/bge-small-en-v1.5/resolve/main/onnx/model.onnx \
-o .lain/models/model.onnx
curl -L https://huggingface.co/BAAI/bge-small-en-v1.5/resolve/main/tokenizer.json \
-o .lain/models/tokenizer.json
# Option B: all-MiniLM-L6-v2 (smaller, 384d, ~80MB)
curl -L https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx \
-o .lain/models/model.onnx
curl -L https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/tokenizer.json \
-o .lain/models/tokenizer.jsonSet the model path:
export LAIN_EMBEDDING_MODEL=$PWD/.lain/models/model.onnx
# or
./lain --embedding-model ./.lain/models/model.onnx ...For BGE-style asymmetric retrieval (better for short queries), set the
query prefix in .lain/tuning.toml:
query_prefix = "Represent this sentence for searching relevant passages: "Tune the CPU thread usage (default auto-detects, min(cores, 4)):
[ingestion]
nlp_max_threads = 0 # 0 = auto, or set to a numberWithout the model, semantic_search returns "unavailable" but all other features work.
| Mode | Command | Use Case |
|---|---|---|
stdio |
--transport stdio |
Claude Code, MCP clients |
http |
--transport http --port 9999 |
Web diagnostics dashboard |
both |
--transport both --port 9999 |
Both stdio + diagnostics |
LSP servers not ready?
# Install missing language servers
curl -X POST http://localhost:9999/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"install_language_server","arguments":{"language":"rust"}},"id":2}'Graph stale?
# Sync to current git HEAD
curl -X POST http://localhost:9999/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"sync_state","arguments":{}},"id":3}'View all available tools:
curl -s -X POST http://localhost:9999/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_agent_strategy","arguments":{}},"id":4}'lain init --agent kimiinstalls~/.kimi-code/plugins/managed/lain/withkimi.plugin.json+skills/lain/SKILL.md+ registers ininstalled.json(source=local-path).lain init --agent gemininow writesGEMINI.md(canonical filename per gemini-cli docs). The previousLAIN.mdwas silently ignored.
Beyond the headline features above, recent versions added:
- Hybrid semantic scoring:
semantic_searchnow combines cosine similarity with stemmed token-overlap (query "running" matches symbols namedindex,indexed,indexes, etc.) - Body excerpts in responses: both
semantic_searchandexplain_symbolnow show the actual code, not just metadata. A developer asking "what is this?" sees the implementation. - Call Graph section:
explain_symbolshows callers and callees alongside the source excerpt. - Anchor percentile normalization: anchor scores are now bounded to [0, 100] via min-max within the candidate set, so the search ranking formula is consistent across reindexes and corpus growth.
- Batched inference API:
NlpEmbedder::embed_batch()is available for larger models / GPU where batching helps (not used on CPU for bge-small since the per-call overhead dominates). - Configurable ONNX thread count:
.lain/tuning.tomlhasnlp_max_threads(0 = auto-detect, or set explicitly). Bumped from 1 to 4-8 threads gives 4-5× faster cold queries. - Cross-encoder reranker (opt-in):
cross-encoder/ms-marco-MiniLM-L6-v2can rerank the top-K bi-encoder candidates. Off by default; enable withcross_encoder_top_k = 20. - Volatile embedding persistence: cold-query embeddings are written back to
graph.binso subsequent process starts don't re-embed the same nodes. Cold-query latency on a 1500-node corpus drops from 29 s to ~5–10 s. - Project registry:
lain projects add/list/forget/current/usemanages multiple repos so you don't have to type--workspaceevery time.
A simple A/B test was run on the asciinema_fix_pty_bug (a small fork i made from https://github.com/asciinema/asciinema.git ) across 5 passes, 4 times using a script. Median numbers are reported.
| Metric | with_lain | without_lain |
|---|---|---|
| Pass rate | 5/5 (100%) | 5/5 (100%) |
| Median duration | 39.3s | 54.1s |
| Median tokens in | 35,488 | 41,731 |
Key observations:
- Both conditions passed 100% — the bug fix worked in both conditions, with variation per run.
with_lainused fewer input tokens (~35k vs ~42k median), a difference of ~7k tokens per run.
About the bug: The failing test (pty::tests::spawn_extra_env on macOS) stems from handle_child() setting env vars via env::set_var() before execvp(). The shell's interpretation of echo -n $VAR varies across platforms — sometimes -n is treated as a literal argument. The fix: use printf "%s" "$ASCIINEMA_TEST_FOO" instead, portable across all Unix-like systems.
This was a test I did for A/B comparison — not a rigorous evaluation.
MIT — Copyright (c) 2026 spuentesp