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.
# Install (interactive — will add `lain` to PATH)
curl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | bash
# Or non-interactive
curl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | \
bash /dev/stdin --yes
# Configure your project
mkdir -p ~/projects/biller && cd ~/projects/biller
lain repos add auth-svc https://github.com/acme/auth-svc.git
lain repos add billing-svc https://github.com/acme/billing-svc.git
lain workspaces create biller-core --members auth-svc,billing-svc
# Run the server
lain server --config ./repos.yaml --transport http --port 9999
# Open http://localhost:9999 — that's the Command Center.Lain is a persistent code-intelligence MCP server. The headline is
lain server: a long-running process that reads a repos.yaml config,
indexes every registered repository (locally, by clone, or by shallow
fetch), and answers structural questions across them through MCP
tools. The server also serves a Command Center dashboard at GET / for
humans who want to inspect the federation, edit the config, run
queries, and exercise the MCP tool surface directly.
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, and hot-reloads
its repos.yaml / workspaces.yaml config without a restart.
After install, lain exposes exactly five subcommands:
| Command | Purpose |
|---|---|
lain server |
Start the MCP server (the headline). Reads repos.yaml, serves MCP tools + the Command Center dashboard. Hot-reloads the config when it changes. |
lain mcp |
Single-repo MCP server on stdio. Walks up from cwd for .git — the stable "drop in a clone and run" entrypoint. No repos.yaml required. |
lain workspaces |
Manage workspaces.yaml. Create, list, show, activate (use), forget named groups of repos. |
lain repos |
Manage repos.yaml. Add, list, remove a repo entry. |
lain query |
Run a query_graph ops-array against the project's persisted graph. |
lain ask |
Single-user LLM-assisted query (uses semantic_search + explain_symbol heuristics). |
lain hooks |
Agent pre-edit hook entry point: claim / release files, overlap-check for commit-time symbol overlap, lock / unlock for the zero-daemon filesystem-fallback layer. |
lain doctor |
"One version of truth" diagnostic. Checks binary version + git SHA, hook script presence, config/hooks dirs, presence registry, and (if LAIN_URL set) server reachability. |
The cut surface (init, agents, hook, projects, top-level
use) is gone — those concerns are reached through the five commands
above. server plus the two config CLIs (workspaces, repos)
cover everything the prior surface did, scoped to a single project
directory that owns a repos.yaml.
curl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | bashThe installer downloads the lain binary to ~/.local/lain. After that,
configure your agent's MCP config to launch lain server --config ./repos.yaml --transport stdio — see Wire your agent.
Non-interactive install (with options):
# Skip all prompts
curl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | \
bash /dev/stdin --yes
# Download ONNX model for semantic search (all-MiniLM-L6-v2, ~120MB)
curl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | \
bash /dev/stdin --download-model --yesAfter installation:
# Reload your shell (the installer adds to ~/.zshrc or ~/.bashrc)
source ~/.zshrc # or ~/.bashrc
# Verify
lain --version
# Show the five commands
lain --helpbrew tap spuentesp/lain https://github.com/spuentesp/lain
brew install lain
# Run the server for your project
lain server --config ./repos.yamlgit clone https://github.com/spuentesp/lain.git
cd lain
cargo build --release # requires Rust 1.75+
# Binary at ./target/release/laincurl -fsSL https://raw.githubusercontent.com/spuentesp/lain/main/install.sh | bashA project is a directory containing repos.yaml (and optionally
workspaces.yaml).
mkdir -p ~/projects/biller && cd ~/projects/biller
lain repos add auth-svc https://github.com/acme/auth-svc.git
lain repos add billing-svc https://github.com/acme/billing-svc.git
lain workspaces create biller-core --members auth-svc,billing-svclain server --config ./repos.yaml --transport http --port 9999
# Open http://localhost:9999 in your browser for the Command Center.Add the following to your agent's MCP config (URL/format depends on the
agent). The single-repo form is the recommended default — it
walks up from the working directory for .git, no repos.yaml needed:
{
"mcpServers": {
"lain": {
"command": "lain",
"args": ["mcp"]
}
}
}If you want the federation tool surface (list_repos, search_org,
get_cross_repo_blast_radius) too, point at a repos.yaml instead:
{
"mcpServers": {
"lain": {
"command": "lain",
"args": ["server", "--config", "./repos.yaml", "--transport", "stdio"]
}
}
}That's it. The next time your agent starts, it sees the workspace (single-repo form) or the federation, the active workspace, and the full MCP tool surface.
When lain server runs with --transport http, it serves the Command
Center dashboard at GET /. It's a self-contained vanilla-JS SPA that
talks back to the running server over the same JSON-RPC endpoint the
MCP tools use. No separate API, no auth portal.
Tabs:
- Overview —
get_health+get_federation_healthin one view. - Graph — D3 force-directed graph of the active workspace.
- Repos — per-repo table (id, path, health, node/edge counts).
- Query — runs
query_graphagainst the federation. - Tools — auto-generated MCP tool tester. Calls
tools/list, then renders a form per tool by introspecting itsinputSchema. Copy as cURL copies acurl -X POST http://localhost:9999/mcp ...snippet to the clipboard.
The status bar in the footer polls every 2 s for get_server_status
and get_reload_status so hand-edits to repos.yaml /
workspaces.yaml show up live.
See docs/command-center.md for the full
walkthrough.
lain server watches repos.yaml and workspaces.yaml and rebuilds
its federation state when they change — no restart needed. Both the
notify watcher (for hand-edits) and the CLI (via lain repos add
or lain workspaces create) trigger the same ReloadBus.
When you run lain repos add my-repo …, the CLI writes the YAML
atomically (write to temp file, then rename), then signals the
running server over a Unix socket at
~/.local/lain/run/<repos-stem>.sock. The server's rebuild task
diffs the new file against the live federation and applies add / remove
operations against FederatedIndex. get_reload_status reports the
state (idle / rebuilding / failed); the Command Center status
bar shows it live.
See docs/hot-reload.md for the full picture
(internals, observability, failure modes, caveats).
A project is a directory containing repos.yaml (and optionally
workspaces.yaml). Each project has its own server: change to the
project directory and run lain server --config ./repos.yaml, or
keep multiple servers running on different ports. The Command Center
shows recently-used projects in the sidebar with a Copy restart cmd
button that copies the right lain server --config <path> --workspace <name> line to the clipboard.
Workspaces are scoped to a single project. Pick one with
lain workspaces use <name>; the active name is written to
~/.config/lain/active_workspace and is honored at server start via
--workspace auto.
For org-wide structural questions — "who else uses this function?",
"what depends on this service?" — run lain server --config ./repos.yaml. Federation mode exposes six MCP tools (list_repos,
get_repo_info, get_federation_health, search_org,
get_cross_repo_blast_radius,
get_cross_repo_blast_radius_for_repo) that answer questions
spanning repos. See docs/FEDERATION.md for
the full guide and docs/REPOS_YAML.md for the
config schema.
- Federation mode — index N repos and answer org-wide structural questions across them.
- Command Center — vanilla-JS SPA at
GET /for human inspection, config editing, query running, and MCP tool testing. - Hot reload —
repos.yaml/workspaces.yamlchanges apply without restarting the server.
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 functions.get_blast_radius— Everything affected by a change.trace_dependency— What a symbol depends on.get_coupling_radar— Files that change together.
find_anchors— Most-called, most-stable symbols (architectural pillars).list_entry_points— Findmain(), route handlers, app initialization.get_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.
A project is a directory containing repos.yaml (and optionally
workspaces.yaml). Manage it directly with the CLI:
lain repos add <name> <url>— register a repo inrepos.yaml.lain repos list— show registered repos.lain repos remove <name>— unregister a repo.lain workspaces create <name> --members a,b,c— declare a named workspace.lain workspaces list— show all workspaces.lain workspaces use <name>— activate a workspace (writes~/.config/lain/active_workspace).lain workspaces current— print the active workspace.lain workspaces forget <name>— remove a workspace.
| Requirement | Details |
|---|---|
| Rust (build only) | 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 setup uses the provided install script with --download-model.
Otherwise, drop a model into .lain/models/:
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.jsonExport the model path so the server picks it up:
export LAIN_EMBEDDING_MODEL=$PWD/.lain/models/model.onnxFor 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: "Without 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 |
Command Center dashboard + curl-driven MCP |
The HTTP transport is no longer combined with stdio in a single
both mode — start two lain server processes (or use the HTTP
transport and exercise tools via curl against /mcp).
Hand-edit not picked up?
The hot-reload watcher is non-recursive and uses atomic rename.
Editing the file in place (vim repos.yaml) triggers a notify event
within ~1 s. If you've moved the file across directories, save it
back into the same directory.
Repo stuck in indexing / degraded / unavailable / missing?
# Check federation health
curl -s -X POST http://localhost:9999/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_federation_health","arguments":{}},"id":1}'The Command Center's Overview tab shows the same numbers in a single view.
Force a reload:
curl -s -X POST http://localhost:9999/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"request_reload","arguments":{}},"id":1}'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":1}'MIT — Copyright (c) 2026 spuentesp