Users can now drop a .pi-lsp.json at any ancestor of their working files to add new LSP servers, override built-in ones, or disable servers entirely. The nearest config (walking upward) wins. - New src/config.ts: walks upward for .pi-lsp.json, parses, and merges with the built-in registry. Cached per config-file path with mtime invalidation. Falls back to built-ins on parse error. - Merge rules: matching id shallow-merges (user wins); new id appends (must include match/command/args/rootMarkers); `disable` filters at the end. - src/root.ts: pickServer() now resolves servers via the per-repo registry. Adds findServerById(filePath, id) and re-exports getServersForPath() for callers. - src/daemon.ts: getOrCreateEntry() resolves serverId against the filePath's config so spawned servers reflect repo overrides. - index.ts and cli.ts: replace direct `servers` imports with path-aware getServersForPath() lookups. - Tests: 9 new unit tests covering merge semantics, walk-up discovery, mtime invalidation, and graceful fallback. - Docs: README "Per-Repo Config" section + AGENTS.md updates.
evan/pi-lsp
LSP extension for pi coding agent. Provides LSP tools that the LLM can use to query language servers, plus automatic diagnostics after edit/write operations.
Features
LSP Tools (callable by LLM)
lsp_hover- Get hover documentation for a symbollsp_definition- Find the definition of a symbollsp_references- Find all references to a symbollsp_completion- Get completion suggestionslsp_documentSymbol- Get the symbol outline of a filelsp_diagnostics- Get lint/type-check diagnostics
Auto-Check
Automatically runs LSP diagnostics after edit or write tool calls. If issues are found, sends a message with the diagnostics to the LLM.
Enable/disable:
pi --lsp-auto-check=false # Disable auto-check
pi --lsp-auto-check=true # Enable (default)
Manual Check Command
Run diagnostics manually on specific files:
/lsp-check main.go utils.go
Server Control Commands
Disable a server so this pi instance won't use it (the shared daemon and other instances are unaffected). When all servers are disabled, LSP tools are removed from the active tool set.
| Command | Args | Behavior |
|---|---|---|
/lsp-servers |
none | List running servers and disabled state |
/lsp-disable |
[<id>] |
Disable all (no arg) or specific server. Bare command disables all. |
/lsp-enable |
[<id>] |
Enable all (no arg) or specific server. Restores tools when any is enabled. |
/lsp-destroy |
[<id>] |
Kill running daemon entries for all (no arg) or specific server. Explicitly destructive. |
/lsp-disable gopls # Disable just gopls; other LSP tools still work
/lsp-disable # Disable all — removes LSP tools from active set
/lsp-enable gopls # Re-enable gopls; restores tools
/lsp-enable # Re-enable all
/lsp-destroy gopls # Kill running gopls process(es) in the daemon
/lsp-destroy # Kill all running server processes
Install
cd ~/.pi/extensions/lsp
npm install
CLI Usage (for development/testing)
tsx ./cli.ts <file> <lsp_command> <req_data_json> [--no-daemon]
tsx ./cli.ts daemon <status|stop>
Requests use a long-lived background daemon by default. The daemon is
autospawned on first use, keeps one language server alive per
(server.id, project root), and evicts idle servers after
ServerConfig.idleTtlMs (default: 5 minutes). Pass --no-daemon to use the
legacy one-shot path for debugging.
req_data_json is the raw LSP params for the command, minus
textDocument.uri (we inject that from <file>).
Commands
hoverdefinitionreferencescompletiondocumentSymboldiagnostics(waits briefly for the firstpublishDiagnostics)
Examples
# Hover at line 224, col 23 (LSP is 0-indexed, so subtract 1)
npm run lsp -- backend/api/server.go hover \
'{"position":{"line":223,"character":22}}'
# Go to definition
npm run lsp -- backend/api/server.go definition \
'{"position":{"line":223,"character":22}}'
# Document symbols (no params needed)
npm run lsp -- backend/api/server.go documentSymbol '{}'
# Diagnostics
npm run lsp -- backend/api/server.go diagnostics '{}'
# Inspect/stop the background daemon
npm run lsp -- daemon status
npm run lsp -- daemon stop
Set LSP_DEBUG=1 to forward server stderr to the daemon log. The daemon
socket is $XDG_RUNTIME_DIR/pi-lsp-$UID.sock (tmpdir fallback); logs are in
/tmp/pi-lsp-daemon.log.
Adding A Server
Edit server.ts:
{
id: "rust-analyzer",
match: ["rs"],
command: "rust-analyzer",
args: [],
rootMarkers: ["Cargo.toml"],
languageId: "rust",
}
Per-Repo Config (.pi-lsp.json)
Drop a .pi-lsp.json at any ancestor of your working files to add or override
servers for that repo. The nearest config (walking upward) wins.
{
"servers": [
{
"id": "rust-analyzer",
"match": ["rs"],
"command": "rust-analyzer",
"args": [],
"rootMarkers": ["Cargo.toml"],
"languageId": "rust"
},
{ "id": "gopls", "args": ["-remote=auto", "-vv"] }
],
"disable": ["oxlint"]
}
Merge rules:
- Entry with a built-in
id→ fields shallow-merge over the built-in (user wins). - Entry with a new
id→ appended; must includematch,command,args,rootMarkers. disable→ filters out matching ids (built-in or user-defined).
Reloading after edits: Config is re-read on mtime change, so new lookups
pick up changes automatically. However, already-running language servers in
the daemon keep their original spawn args. If you change command, args, or
rootMarkers, run /lsp-destroy (or pi-lsp daemon stop) so they respawn
with the new config.
Security note: .pi-lsp.json controls what binary pi-lsp spawns. Treat it
like .vscode/settings.json — don't accept untrusted configs from arbitrary
repos.
Adding A Command
- Add to the
LspCommandunion insrc/types.ts. - Add a handler in
src/commands.ts.
Future
- Daemon hardening - persistent metrics, health checks, and richer status output.
- Build output - ship compiled JS entrypoints instead of relying on tsx for development.