Skip to content
 
 

Repository files navigation

pi-lsp-client

Language Server Protocol integration for the pi coding agent. This fork is maintained by Close for its development environment. It provides six LSP tools, per-session server management, crash retry, and a /lsp inspector.

Fork history

This repository is Close's maintained fork of code-yeongyu/pi-lsp-client. Close updates it for the pi versions used internally and publishes releases from closeio/pi-lsp-client.

The upstream package ports LSP tools written for oh-my-openagent (omo) by Yeongyu Kim (@code-yeongyu). The original implementation lives under src/tools/lsp/ in that repository.

Yeongyu re-licensed the ported source under MIT for the pi-coding-agent ecosystem. omo remains under SUL-1.0. The MIT license applies only to the code shipped in this repository. See LICENSE and NOTICE.

Quick Demo

> Show me the type errors in src/foo.ts.

[lsp_diagnostics] src/foo.ts
E:2 W:1 • 1 file
  E 14:5  Type 'string' is not assignable to type 'number'.
  E 27:1  Cannot find name 'unknownVar'.
  W  9:3  'helper' is declared but its value is never read.
> Rename `oldFoo` to `newFoo` everywhere.

[lsp_prepare_rename] src/lib.ts:42:7
Rename available at 42:7-42:13 (current: "oldFoo")

[lsp_rename] src/lib.ts:42:7 → "newFoo"
✓ Applied 7 edits to 4 files
  - src/lib.ts
  - src/cli.ts
  - src/main.ts
  - test/lib.test.ts

Installation

Install an exact release from git:

pi install https://github.com/closeio/pi-lsp-client@v0.1.0

Full version tags never move. To track the latest release within a major version, install its floating major tag instead:

pi install https://github.com/closeio/pi-lsp-client@v0
pi update --extensions

Each release moves v<major> to the release commit. For example, publishing v1.2.3 moves v1. pi update --extensions fetches that moved tag and resets the installed package to its commit. It does not change an exact v1.2.3 installation.

Use pi install -l with either form to record the package in the current project's .pi/settings.json. For development, load the checkout directly with pi -e /path/to/pi-lsp-client/src/index.ts.

After installation, restart pi (or run /reload inside an interactive session). All six tools register automatically and become callable by the LLM.

Tools

lsp_diagnostics

Errors, warnings, and hints from the language server BEFORE running build. Works for both single files and directories (extension auto-detected).

Parameter Type Description
filePath string (required) File or directory path.
severity "error" | "warning" | "information" | "hint" | "all" (optional) Filter by severity. Default all.

lsp_goto_definition

Jump to the definition of the symbol at a given position.

Parameter Type Description
filePath string (required) Source file.
line number (required) 1-based line number.
character number (required) 0-based column.

lsp_find_references

Find all usages of the symbol at a given position across the entire workspace.

Parameter Type Description
filePath string (required) Source file.
line number (required) 1-based line number.
character number (required) 0-based column.
includeDeclaration boolean (optional) Include the declaration itself. Default true.

lsp_symbols

Document outline (scope: "document") or workspace-wide symbol search (scope: "workspace", requires query).

Parameter Type Description
filePath string (required) Source file (used as LSP context).
scope "document" | "workspace" (required) Outline vs search.
query string (optional) Symbol name (required for workspace scope).
limit number (optional) Max results. Default 200.

lsp_prepare_rename

Validate that a rename is possible at a given position. Always run this before lsp_rename.

Parameter Type Description
filePath string (required) Source file.
line number (required) 1-based line number.
character number (required) 0-based column.

lsp_rename

Apply a rename across the workspace. Mutates files. Runs executionMode: "sequential" so it never races other mutating tools.

Parameter Type Description
filePath string (required) Source file.
line number (required) 1-based line number.
character number (required) 0-based column.
newName string (required) New symbol name.

Commands

/lsp

Interactive inspector for the current session's server pool. Shows server id, root, refCount, pendingWaiters, lastUsedAt, isInitializing, alive, and command. Press Escape or Ctrl-C to close. In non-interactive mode (no TUI), prints a one-line summary instead.

/lsp status

One-line summary of which servers are installed (no full inspector).

/lsp install <serverId>

Run the documented install recipe for <serverId> (whitelisted in AUTO_INSTALLABLE_SERVERS). Status footer shows Installing <id>... while the install runs. On success, notifies; on failure, shows the install command's stderr tail.

If <serverId> is not in the auto-installable whitelist, the command surfaces the manual install hint from LSP_INSTALL_HINTS instead of running anything.

/lsp warmup <serverId>

Spawn and initialize an installed server in the current working directory without waiting for any tool call. Useful before a long batch of LSP-driven work to amortize first-call latency.

Built-in Servers

40+ language servers from omo's BUILTIN_SERVERS, including TypeScript (typescript-language-server), Python (pyright, basedpyright, ruff, ty, pyrefly), Go (gopls), Rust (rust-analyzer), C/C++ (clangd), Ruby (ruby-lsp), Bash (bash-language-server), YAML (yaml-language-server), Lua, Java, PHP, Dart, Swift, Kotlin, Zig, Nix, Haskell, Elixir, OCaml, Terraform, and more.

Each server has an installed check (PATH probe + extension probing) and an install hint (LSP_INSTALL_HINTS). A subset is auto-installable via /lsp install <id> (AUTO_INSTALLABLE_SERVERS).

Rust is manual-only: /lsp install rust prints the rust-analyzer install hint instead of running rustup. If rust-analyzer exits while loading rust-src, repair the active toolchain with rustup component remove rust-src and rustup component add rust-src, then warm up Rust again.

Custom Servers / Configuration

Add custom servers by creating either:

  • .pi/lsp-client.json in the directory you launch pi from (project-local, takes priority)
  • <pi agent dir>/lsp-client.json (user-global)

The user-global path follows pi's own convention. By default that's ~/.pi/agent/lsp-client.json, but it honors the PI_CODING_AGENT_DIR environment variable (the same one pi reads for its agent directory). If PI_CODING_AGENT_DIR=/path/to/agent, the user config is read from /path/to/agent/lsp-client.json.

For backward compatibility, an old ~/.pi/lsp-client.json is still read if the primary agent-dir path does not exist.

{
  "lsp": {
    "my-server": {
      "command": ["my-lsp", "--stdio"],
      "extensions": [".myext"],
      "priority": 100,
      "env": { "MY_LSP_LOG": "1" }
    },
    "biome": {
      "disabled": true
    }
  }
}

disabled: true removes a builtin server from resolution. Project config wins over user config. Builtins are the lowest priority (only used when no project/user override exists).

Lifecycle

  • Per-session managers. Each pi session owns its own LspManager, resolved lazily on first use via getManagerForSession(ctx.sessionManager) from manager-registry.js. Concurrent sub-agent sessions in one Node process do not share clients - one session's session_shutdown cannot dispose servers another session is still using. Managers are keyed by reference identity of ctx.sessionManager in a WeakMap, so a forgotten session is GC-safe.
  • Lazy spawn. Servers spawn on first tool call for a matching extension. No eager warmup of the entire registry.
  • Refcount. Each withLspClient(...) call increments refCount on entry and decrements in finally. Idle reaping fires only when refCount hits zero AND lastUsedAt is older than the idle timeout.
  • Idle timeout: 5 minutes. Idle clients are stopped and removed from the pool.
  • Init timeout: 60 seconds. A pending init older than 60s is reaped, even if other callers are waiting on it.
  • Abort-aware acquisition. getClient(root, server, signal?) participates in tool cancellation. If the signal aborts before init resolves, the caller is removed from the waiter list; if no callers remain, the initializing client is stopped and removed.
  • Crash retry. When the JSON-RPC transport throws LspConnectionClosedError or LspProcessExitedError mid-call, the wrapper evicts the dead client and retries exactly once for idempotent read tools (diagnostics, goto_definition, find_references, symbols, prepare_rename). Mutating tools (rename) are never retried.
  • Session shutdown is the primary cleanup boundary. pi.on("session_shutdown", ...) calls disposeManagerForSession(ctx.sessionManager) - stops that session's clients, clears its reaper interval, unregisters its process exit fallback, and clears pi-lsp status/widget keys. Other sessions' managers are untouched.
  • No raw signal handlers. No SIGINT/SIGTERM listeners. They would conflict with pi's TUI shutdown. Each manager registers a synchronous process.on("exit", ...) killSync handler as a fallback for unexpected exits. The disposer removes that handler so listener count doesn't grow across /reload.

Cross-Platform Notes

  • Subprocess spawning uses node:child_process.spawn everywhere. On Windows the spawn helper enables shell: true and includes platform-specific path probing (PATHEXT, Path casing).
  • The transport uses vscode-jsonrpc/node directly with the spawned process's stdin/stdout streams. No Bun-specific Web stream adapter.

Troubleshooting

Symptom Fix
"LSP server '' is configured but NOT INSTALLED." Run /lsp install <id> if the server is in the auto-installable whitelist, otherwise install it manually using the displayed hint.
"No LSP server configured for extension: .ext" Add a custom entry in .pi/lsp-client.json (see Custom Servers).
rust-analyzer exited while loading rust-src Run rustup component remove rust-src and rustup component add rust-src for the active toolchain, then retry the LSP tool or /lsp warmup rust.
lsp_rename did not retry after a server crash This is by design. Mutating tools never auto-retry to avoid double-applying edits. Re-issue the rename manually.
Footer status stuck after /reload File a bug. The session_shutdown handler clears pi-lsp status/widget keys. If they persist, the cleanup boundary was bypassed.
Stale LSP child after /reload Run /lsp to inspect the current snapshot. If getSnapshot() is empty but a child process is still alive, file a bug. stopAll() should have killed it.

Development

git clone https://github.com/closeio/pi-lsp-client
cd pi-lsp-client
npm install            # install dev + peer dependencies
npm test               # run vitest
npm run typecheck      # strict tsc --noEmit
npm run check          # tsc + biome
pi -e ./src/index.ts   # smoke-test inside a real pi session

The test suite uses vitest. Test descriptions follow #given .. #when .. #then style; bodies use plain // given / // when / // then comments. No any, no enums.

Releasing

Releases use the version in package.json and a matching v<version> git tag. For each release:

  1. Run npm version patch --no-git-tag-version (or minor / major).
  2. Move the release notes from [Unreleased] to ## [<version>] - YYYY-MM-DD in CHANGELOG.md.
  3. Merge the version change to main and wait for CI to pass.
  4. Run the release workflow from main in GitHub Actions.

The workflow rejects an existing full version tag or a missing changelog section, reruns the checks and tests, creates the immutable version tag and GitHub release, then force-moves the matching v<major> tag. The first v0.1.0 release is already prepared, so it only needs steps 3 and 4.

License

MIT. See NOTICE for re-license disclosure relative to omo.

Acknowledgements

  • Yeongyu Kim (@code-yeongyu) is the author of the original omo LSP tools and the upstream pi-lsp-client package this fork is based on.
  • Mario Zechner (@badlogic) is the author of pi-mono and the pi-coding-agent extension API this package targets.
  • Microsoft maintains the vscode-jsonrpc transport library used here.

About

Close fork of an LSP client for the pi coding agent.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages