@mako10k/lsp-cli
v0.1.3
Published
Lightweight CLI for driving arbitrary LSP servers (MVP: rust-analyzer)
Readme
lsp-cli
A lightweight CLI client to drive arbitrary LSP servers for structural analysis and refactoring (MVP: rust-analyzer).
Table of contents
- Quickstart
- Sample (for testing)
- Command reference
- Use case samples
- Architecture / config / details
- Protocol support
- Changelog
Quickstart
# Smoke test (rust-analyzer must be in PATH)
# If you use the rustup proxy, run this first:
# rustup component add rust-analyzer
npx @mako10k/lsp-cli --root . ping
# documentSymbol (line/col are 0-based)
npx @mako10k/lsp-cli --root . --format pretty symbols path/to/file.rsDevelopment (from source)
npm install
npm run build
# Install the repo locally as a CLI (provides the lsp-cli command)
npm link
lsp-cli --helpSample (for testing)
A small Rust sample project is included:
samples/rust-basic
Examples:
# initialize smoke test
npx @mako10k/lsp-cli --root samples/rust-basic ping
# documentSymbol
npx @mako10k/lsp-cli --root samples/rust-basic --format pretty symbols samples/rust-basic/src/math.rs
# references: the call site of add inside main.rs (0-based)
# Example: the "a" position of "add" on line 9 (" let x = add(1, 2);")
npx @mako10k/lsp-cli --root samples/rust-basic --format json references samples/rust-basic/src/main.rs 8 12
# definition (right after rust-analyzer initialization, results may be empty; --wait is recommended)
npx @mako10k/lsp-cli --root samples/rust-basic --format pretty --wait-ms 500 definition samples/rust-basic/src/main.rs 8 12
# hover
npx @mako10k/lsp-cli --root samples/rust-basic --format pretty --wait-ms 500 hover samples/rust-basic/src/main.rs 8 12
# signature help (somewhere inside add()
npx @mako10k/lsp-cli --root samples/rust-basic --format pretty --wait-ms 500 signature-help samples/rust-basic/src/main.rs 8 16
# workspace symbols
npx @mako10k/lsp-cli --root samples/rust-basic --format pretty --wait-ms 500 ws-symbols add --limit 20
# implementation
npx @mako10k/lsp-cli --root samples/rust-basic --format pretty --wait-ms 500 implementation samples/rust-basic/src/main.rs 8 12
# type definition
npx @mako10k/lsp-cli --root samples/rust-basic --format pretty --wait-ms 500 type-definition samples/rust-basic/src/main.rs 8 12Notes
Positions (
line,col) are 0-based (LSP-compliant).With
--server typescript-language-server, it starts TypeScript Language Server via npx (TypeScript is required in the target workspace):npx -y typescript-language-server --stdioApplying changes is dry-run by default; files are modified only when
--applyis specified.
Command reference
Global options
Global options must appear before the command for reliable parsing.
--root <path>: workspace root (default: cwd)--server <name>: server profile name (default:rust-analyzer)--server-cmd <cmd>: override the server command string--config <path>: config file path (default:<root>/.lsp-cli.jsonor<root>/lsp-cli.config.json)--format <json|pretty>: output format (default:json)--stdin: read command params from stdin as JSON (command-specific)--jq <filter>: pipe JSON output throughjq(requiresjqin PATH)--wait-ms <n>: wait before some requests (ms; helps rust-analyzer warm-up)--daemon-log <path>: daemon log sink (auto-start);discard|default|<path>
Command index
Core:
ping
Read-only navigation:
symbols,references,definition,type-definition,implementation,hover,signature-help,ws-symbols
Edits and refactoring (mutating; dry-run by default):
rename,code-actions,apply-edits,delete-symbol
Formatting and tokens:
format,format-range,completion,document-highlight,inlay-hints,semantic-tokens-full,semantic-tokens-range,semantic-tokens-delta,prepare-rename
Daemon and operations:
daemon-status,daemon-stop,daemon-log,events,server-status,server-stop,server-restart,did-change-configuration
Batch / advanced:
batch,daemon-request
Experimental (daemon-only; normally use non-suffixed commands):
symbols-daemon,references-daemon,definition-daemon,hover-daemon,signature-help-daemon,ws-symbols-daemon
Read-only navigation
Typical arguments are:
symbols <file>references <file> <line> <col>definition <file> <line> <col>type-definition <file> <line> <col>implementation <file> <line> <col>hover <file> <line> <col>signature-help <file> <line> <col>ws-symbols <query> [--limit <n>]
Edits and refactoring (mutating)
rename <file> <line> <col> <newName>: defaults to dry-run; add--applyto modify files.code-actions <file> ...: defaults to dry-run; add--applyto apply selected action.apply-edits: readsWorkspaceEditJSON from stdin; add--applyto modify files.delete-symbol <file> <symbolName>: dry-run by default; add--applyto modify files.
Formatting and tokens
format <file> [--apply]format-range <file> <startLine> <startCol> <endLine> <endCol> [--apply]completion <file> <line> <col>document-highlight <file> <line> <col>inlay-hints <file> <startLine> <startCol> <endLine> <endCol>semantic-tokens-full <file>semantic-tokens-range <file> <startLine> <startCol> <endLine> <endCol>semantic-tokens-delta <file>prepare-rename <file> <line> <col>did-change-configuration --settings '<json>'(or--stdin)
Daemon and operations
daemon-status: daemon metadata (pid/startedAt/socketPath)server-status: whether in-daemon LSP is runningserver-stop/server-restart: stop/restart only the in-daemon LSP sessiondaemon-stop: stop the daemon process itselfevents --kind diagnostics --since <cursor> --limit <n>: pull-based notificationsdaemon-log [discard|default|<path>]: get/set daemon log sink
Batch mode
batch reads JSON Lines (one line = one request) from stdin and executes them sequentially within the same LSP session.
Advanced / debug
daemon-request --method <name> [--params <json>]: send an arbitrary LSP request via the daemon.
Daemon mode (persistent)
To reduce repeated execution costs (initialize, etc.) for the same --root, the CLI tries to connect to a daemon by default. If it cannot connect, it implicitly starts the daemon and retries; if it still fails, it falls back to the legacy behavior: start LSP as a one-shot process.
There is no explicit daemon start command (auto-start only).
Use case samples
1) Pull diagnostics (after edits)
# run something that triggers diagnostics (e.g. open/format/rename)
npx @mako10k/lsp-cli --root samples/rust-basic symbols samples/rust-basic/src/math.rs > /dev/null
# then pull diagnostics
npx @mako10k/lsp-cli --root samples/rust-basic events --kind diagnostics --since 02) "Navigate" (definition → references)
npx @mako10k/lsp-cli --root samples/rust-basic --format pretty --wait-ms 500 definition samples/rust-basic/src/main.rs 8 12
npx @mako10k/lsp-cli --root samples/rust-basic --format pretty --wait-ms 500 references samples/rust-basic/src/main.rs 8 123) Safe refactor (dry-run first)
# preview rename
npx @mako10k/lsp-cli --root samples/rust-basic rename samples/rust-basic/src/main.rs 8 12 new_name
# apply rename
npx @mako10k/lsp-cli --root samples/rust-basic rename --apply samples/rust-basic/src/main.rs 8 12 new_nameArchitecture / config / details
Daemon-first + fallback
By default, many commands try: daemon → auto-start daemon → fallback to direct stdio.
Events (pull-based notifications)
The daemon stores notifications like textDocument/publishDiagnostics and exposes them via events.
Dry-run vs --apply
Commands that can modify files are dry-run by default. To actually write files, pass --apply.
Daemon events (pull-based)
The daemon accumulates notifications such as textDocument/publishDiagnostics, and you can fetch them via events.
# Fetch diagnostics (raw JSON)
npx @mako10k/lsp-cli --root samples/rust-basic events --kind diagnostics
# Fetch deltas using a cursor (pass the previous cursor via --since)
npx @mako10k/lsp-cli --root samples/rust-basic events --kind diagnostics --since 0Daemon server control (stop/restart LSP only)
You can stop or restart only the LSP session inside the daemon, without killing the daemon process itself.
# Check whether the LSP inside the daemon is running
npx @mako10k/lsp-cli --root samples/rust-basic server-status
# Stop only the LSP (daemon stays alive)
npx @mako10k/lsp-cli --root samples/rust-basic server-stop
# Restart LSP (re-run initialize)
npx @mako10k/lsp-cli --root samples/rust-basic server-restartDaemon stop (stop the daemon process)
npx @mako10k/lsp-cli --root samples/rust-basic daemon-stopapply-edits (apply/dry-run WorkspaceEdit)
You can supply a WorkspaceEdit from stdin and preview/apply it (reuses the existing WorkspaceEdit application logic).
# dry-run (preview)
cat workspaceEdit.json | npx @mako10k/lsp-cli apply-edits
# apply (modifies files)
cat workspaceEdit.json | npx @mako10k/lsp-cli apply-edits --applyBatch mode (JSONL)
Reads JSON Lines from stdin (one line = one request) and executes them sequentially within the same LSP session.
cat <<'JSONL' | npx @mako10k/lsp-cli --root . --server typescript-language-server --format json batch
{"cmd":"references","file":"src/index.ts","line":0,"col":0}
{"cmd":"definition","file":"src/index.ts","line":0,"col":0}
JSONLTo apply edits/refactors, add batch --apply (explicit opt-in for safety):
cat <<'JSONL' | npx @mako10k/lsp-cli --root . --server typescript-language-server --format json batch --apply
{"cmd":"rename","file":"src/index.ts","line":0,"col":0,"newName":"renamed","apply":true}
JSONLStructured edit (delete symbol)
Deletes a symbol block by name using documentSymbol (dry-run example):
npx @mako10k/lsp-cli --server typescript-language-server --root . --format pretty \
delete-symbol src/servers/typescriptLanguageServer.ts typescriptLanguageServerProfile- With
--jq '<filter>', JSON output is piped throughjqfor formatting/extraction (jqmust be in PATH). - For
<file>, pass-to read the file path from stdin. - With
--stdin, command input is read as JSON from stdin.
Protocol support
- See
PROTOCOL_SUPPORT.mdfor a feature-by-feature comparison of LSP capabilities vs whatlsp-cliimplements.
Changelog
- See
CHANGELOG.md.
Config file (server profiles)
- By default it searches:
<root>/.lsp-cli.json<root>/lsp-cli.config.json
- You can specify explicitly with
--config <path>(relative paths are treated as relative to<root>).
The config supports:
presets: reusable server config snippets (referenced bypreset).servers: custom server profiles and overrides.commandis required for custom servers unless provided via apreset.augment: extra overrides merged into both built-in and custom profiles. When augmenting built-ins,commandis optional.- Per-server fields include
args,initializationOptions,languageIdByExt/defaultLanguageId,cwd,env,waitMs, andwarmup.
Example: .lsp-cli.json
{
"presets": {
"ra-default": {
"command": "rust-analyzer",
"args": [],
"defaultLanguageId": "rust",
"languageIdByExt": {
".rs": "rust"
}
}
},
"augment": {
"rust-analyzer": {
"initializationOptions": {
"cargo": {
"buildScripts": { "enable": true }
}
}
},
"typescript-language-server": {
"env": {
"TSS_LOG": "-level verbose"
}
}
},
"servers": {
"my-ra": {
"preset": "ra-default",
"cwd": "samples/rust-basic"
}
}
}Usage:
npx @mako10k/lsp-cli --root samples/rust-basic --config .lsp-cli.json pingExamples (stdin / jq)
# Pass a file path via stdin
printf '%s\n' samples/rust-basic/src/math.rs \
| npx @mako10k/lsp-cli --root samples/rust-basic --jq 'length' symbols -
# Pass references input via JSON stdin
printf '{"file":"samples/rust-basic/src/main.rs","line":8,"col":12}' \
| npx @mako10k/lsp-cli --root samples/rust-basic --stdin --jq '.[0]' references