Files
pi-lsp/README.md
Evan Reichard 46e3cc4ccd feat(config): add per-repo .pi-lsp.json server overrides
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.
2026-05-07 22:43:41 -04:00

5.1 KiB

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 symbol
  • lsp_definition - Find the definition of a symbol
  • lsp_references - Find all references to a symbol
  • lsp_completion - Get completion suggestions
  • lsp_documentSymbol - Get the symbol outline of a file
  • lsp_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

  • hover
  • definition
  • references
  • completion
  • documentSymbol
  • diagnostics (waits briefly for the first publishDiagnostics)

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 include match, 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

  1. Add to the LspCommand union in src/types.ts.
  2. 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.