Files
kim 60c6021390 feat: split system prompt by local/cloud, improve mouse tracking, increase retries & iterations
- Separate system prompt for local vs cloud models via isLocal flag
  (isLocalBackendURL, buildSystemPrompt(..., isLocal), propagate through
  createSession/createSessionFromRecord/compactSession/spawnSubAgent)
- Increase MAX_EMPTY_RESPONSE_RETRIES from 1 to 3 for cloud model resilience
- Upgrade mouse input: full SGR-1006 parsing with col/row/pressed/modifiers,
  logicalButton() helper, and copyToClipboard() via OSC 52
- Add temporary mouse debug logging in App.tsx
- Increase DEFAULT_MAX_ITERATIONS from 100 to 300
- Update README mouse/scrollback docs, tweak diff-remove color
2026-08-21 18:01:29 +09:00

172 lines
21 KiB
Markdown

# locode
An agentic coding CLI, in the spirit of Claude Code, for models running locally via [Ollama](https://ollama.com) or [LM Studio](https://lmstudio.ai). It talks to either backend's OpenAI-compatible `/v1/chat/completions` endpoint, so any model you can serve from either one works here.
## Install
```sh
npm install
npm run build
npm link # makes the `locode` command available globally
```
## Quick start
Make sure Ollama (`ollama serve`, default `http://localhost:11434`) or LM Studio (with a model loaded, default `http://localhost:1234`) is running, then:
```sh
locode --model qwen3-coder:30b
```
If you omit `--model`, locode lists the models available from the backend and lets you pick one with the arrow keys (Enter to confirm). Your saved default (via config or `$LOCODE_MODEL`) is pre-highlighted.
Pass `--model` explicitly to skip the picker entirely. Or persist your defaults so you don't need flags every time:
```sh
locode config set backend ollama
locode config set model qwen3-coder:30b
locode
```
List models available from the configured backend:
```sh
locode models
```
Every conversation is auto-saved as you go. Resume later:
```sh
locode --continue # resume the most recent conversation
locode --resume # pick from a list of saved conversations
locode --resume <id> # resume a specific one
locode sessions list # see saved conversations (id, model, title) without starting the UI
locode sessions rm <id> # delete a saved conversation
```
locode can also use tools from external [MCP](https://modelcontextprotocol.io) servers:
```sh
locode mcp add my-server --command npx --arg -y --arg @some/mcp-server # stdio server
locode mcp add my-remote --url https://example.com/mcp # remote (streamable HTTP) server
locode mcp list # see configured servers (user-level + project .mcp.json)
locode mcp remove <name>
```
Project-level servers can also be checked into a repo via a `.mcp.json` file in its root:
```json
{
"mcpServers": {
"my-server": { "command": "npx", "args": ["-y", "@some/mcp-server"] }
}
}
```
locode connects to every configured server on startup; their tools show up alongside the built-in ones, namespaced as `mcp__<server>__<tool>`.
locode can also install **Claude Code plugins directly** — the parts that map onto locode's own architecture, anyway (see below for what doesn't):
```sh
locode plugin add ./path/to/plugin # a local plugin directory
locode plugin add https://github.com/someone/some-plugin.git # cloned via git
locode plugin list
locode plugin remove <name>
```
Hooks (see below) are hand-edited JSON rather than added via a CLI subcommand:
```sh
locode hooks path # print the user-level hooks.json path to edit
locode hooks list # see every configured hook (plugin + user + project), merged
```
## How it works
locode is a full-screen terminal app built with [Ink](https://github.com/vadimdemedes/ink) (the same React-for-CLI framework Claude Code itself is built with) — it needs a real interactive terminal (piped/redirected input isn't supported). It takes over the terminal's alternate screen buffer (like `vim`/`htop`) — your prior scrollback is restored when you exit. The input box is always pinned to the last row of the window; the conversation fills the space above it and old messages scroll off the top as new ones arrive. Press Ctrl+C or type `/exit` to quit.
- **Ctrl+O**: print the full text of the last `/compact` (or auto-compact) summary. The collapsed notice you see right after compacting only shows a one-line hint — press Ctrl+O any time afterward to print the whole thing.
- **Ctrl+B**: while a `bash` command is running, detaches it into the background and returns control to you immediately — the turn continues with a `bash_output`-checkable job id instead of waiting for the command to finish. A notice appears in the transcript once the backgrounded command actually completes. Only `bash` supports this today. The model can kill a still-running backgrounded job with `bash_kill`; any jobs still running when locode itself exits are killed too, so they don't outlive the process as orphans.
- **Ctrl+F**: open and focus a file panel docked to the right of the chat (hidden by default); press again to close it. It has two tabs — **Files**, the project's collapsible file tree (directories in cyan, same `node_modules`/`.git`/`dist` exclusions as `@` mentions), and **Activity**, the files `read_file`/`write_file`/`edit_file` have touched so far this session, most recent first, with a status glyph (`·` read, `+` written, `~` edited) and a repeat count. **Ctrl+G** switches between the two tabs. While the panel is focused, `↑`/`↓` move the selection (auto-scrolling to keep it in view), `↵`/`←`/`→` expand or collapse the selected folder, and **Esc** hands keyboard focus back to the chat input without closing the panel — typing is disabled while the panel has focus, so the same arrow key doesn't simultaneously recall chat history.
- **Backends**: `--backend ollama` (default) or `--backend lmstudio`, or `--base-url <url>` for anything else that speaks the same API.
- **Tools**: `read_file`, `list_files`, `grep`, `definition`, `references`, `diagnostics`, `web_search`, `web_fetch`, `git_status`, `bash_output`, `todo_write`, `task_create`, `task_list`, `task_get`, `task_update` run automatically. `write_file`, `edit_file`, `multi_edit`, `bash`, `bash_kill`, and `git_commit` show a diff/preview in a bordered box and ask you to pick Yes / Yes-always-this-session / No with the arrow keys before running.
- **Permission modes**: `default` (ask before every mutating tool), `plan` (research only — every mutating tool is blocked outright, no prompt; the model is expected to describe what it would do in its final answer instead), `auto-edit` (file edits auto-approved, `bash`/`git_commit` still ask), `auto-accept` (everything auto-approved — use with care). Cycle with `Shift+Tab` or set directly with `/perm <mode>`.
- **Structured tasks**: for multi-step work the model can call `task_create`/`task_list`/`task_get`/`task_update` to track units of work with a dependency graph (`blocks`/`blockedBy`), ownership (`owner`), status, and free-form metadata — created incrementally rather than replaced wholesale. The older flat `todo_write` live checklist (`☐`/`◐`/`☑`) remains for simpler cases.
- **Project instructions**: a `CLAUDE.md` (or `AGENTS.md`) file in the project root is automatically read at session start and folded into the system prompt — put repo-specific conventions there and every session picks them up without being told.
- **Images**: `read_file` returns image files (png, jpg, jpeg, gif, webp, bmp — up to 5MB) as actual image content instead of trying to decode them as text, so vision-capable models can see them when the model itself calls the tool. To attach a file or image to your own message directly, use `/import <path> [caption]`.
- **`@` file mentions**: type `@` in the chat input to open a fuzzy file picker (searches the whole project, skipping `node_modules`/`.git`/`dist`) — keep typing to filter, `↑`/`↓` to navigate, `Tab` (or `Enter`) to insert the highlighted path. Any `@path` left in your message when you hit `Enter` for real is resolved against disk and attached to that message (text inlined, images attached as image content) — a stray `@` that isn't an actual file (e.g. an email address) is left as plain text.
- **Git**: `git_status` covers read-only inspection (`status`, `diff`, `log`, `show`, `branches`) and runs automatically. `git_commit` covers `add`, `commit`, `create_branch`, `checkout`, and `push` — each shows the actual diff/status/commits it's about to affect before you confirm (e.g. a commit's preview is the staged diff plus the message, a push's preview is the list of commits it would send).
- **Sub-agents**: the `agent` tool lets the model delegate a self-contained task to a fresh, isolated tool loop (same tools, minus `agent` itself — no nested sub-agents) and get back only the final answer, keeping the main conversation's context focused. It shows up as a single `⏺ Agent(description)` / `⎿ Sub-agent finished (...)` line — the sub-agent's own intermediate steps aren't displayed. Any mutating tool calls it makes still go through the same permission prompts as the main conversation.
- **MCP servers**: locode connects to any [MCP](https://modelcontextprotocol.io) servers configured via `locode mcp add` or a project's `.mcp.json` (stdio and remote/streamable-HTTP transports), and adds their tools to every session, namespaced as `mcp__<server>__<tool>`. Every MCP tool is treated as mutating (confirmation required on every call) regardless of what it reports — the MCP `readOnlyHint` annotation is advisory and could be wrong (or set by a malicious server specifically to skip confirmation), so locode never trusts it. One misconfigured server doesn't block the others — check `/mcp` for per-server connection status.
- **Claude Code plugins**: `locode plugin add <path-or-git-url>` installs a Claude Code-compatible plugin — locode reads its `.claude-plugin/plugin.json`, then loads it all directly: MCP servers (its `.mcp.json` or manifest `mcpServers`, merged in like any other MCP server), slash commands (`commands/*.md` — frontmatter `description`/`argument-hint`, body is a template expanded with `$ARGUMENTS`/`$1..$9` and submitted as your message), agents (`agents/*.md` — the body becomes a sub-agent's system prompt, exposed as a callable tool named `agent__<plugin>__<agent>`; a `tools:` frontmatter list restricts what it can use, with Claude Code's built-in tool names — Read, Grep, Edit, etc. — automatically mapped to locode's equivalents), hooks (`hooks/hooks.json`, see below), and skills (`skills/*/SKILL.md`, see below). Check `/plugins` for what's loaded.
- **Skills**: named instructions the model loads on demand rather than a hook or a sub-agent — every installed skill (`skills/<name>/SKILL.md`) is exposed through one shared `skill` tool, whose own description lists every skill's name and "use this when..." blurb so the model knows when to call it. You can also invoke one directly with `/<skill-name> [request]`, which skips the model's own judgment and submits the skill's instructions (plus your request, if any) as the turn. Check `/skills` for what's installed.
- **Hooks**: shell commands that fire on session lifecycle events — `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PermissionRequest`, `SubagentStart`, `SubagentStop`, `CwdChanged`, `FileChanged`, `ConfigChange`, `Stop`, `SessionEnd`. Configured the same way MCP servers are — plugin-bundled (`hooks/hooks.json`), user-level (`locode hooks path`, hand-edited), and project-level (`.locode/hooks.json`) all merge together, every hook from every source runs. A hook receives a JSON payload on stdin (`session_id`, `cwd`, `hook_event_name`, plus event-specific fields like `prompt` or `tool_name`/`tool_input`); exit 0 allows (stdout becomes injected context for `SessionStart`/`UserPromptSubmit`), exit 2 blocks (stderr is the reason shown), anything else is a non-blocking warning. `PreToolUse`, `UserPromptSubmit`, and `PermissionRequest` can block; the rest are fire-and-forget. `PreToolUse` fires before permission modes apply, so a hook's block can't be bypassed by auto-accept. `command` and `http` hook types run an external process/request; a `prompt` hook just injects its static `message` as additional context instead. Command/http hooks can opt into structured JSON output via `outputSchema: "json"`. `SessionEnd` fires on every exit path (including Ctrl+C and external `SIGTERM`/`SIGHUP`). Check `/hooks` for what's configured.
- **Tool-calling mode**: on connect, locode probes whether the model reliably uses native OpenAI-style function calling. If not, it switches to a prompt-based fallback mode where the model is instructed to emit tool calls as fenced ` ```tool_call ``` ` JSON blocks, which locode parses itself. The result is cached per backend+model so future sessions skip the probe. Override with `--tool-mode native|fallback|auto` or the in-session `/mode` command.
- **Context tracking & compaction**: the status bar shows `ctx NN%` — context window usage, from real `usage.prompt_tokens` when the backend reports it (requested via `stream_options.include_usage`), or a `~`-prefixed char-based estimate otherwise. The window size itself is auto-detected (Ollama's `/api/show`, then LM Studio's `/api/v0/models`) and cached per backend+model; falls back to a configurable default (`locode config set contextWindow <n>`, or `$LOCODE_CONTEXT_WINDOW`) if neither responds. At 85% usage, locode automatically asks the model to summarize the conversation and replaces the history with that summary (a notice tells you when this happens) — or trigger it yourself anytime with `/compact`.
- **Code intelligence (LSP)**: `definition`, `references`, and `diagnostics` use a real language server (LSP) for go-to-definition, find-all-references, and type/syntax error checks — the same engine an editor's Problems panel uses, more precise than `grep`. A server is lazily started per language on first use and reused for the whole session: `typescript-language-server` (TypeScript/JavaScript), `pyright-langserver` (Python), `gopls` (Go), `rust-analyzer` (Rust), and `clangd` (C/C++ — one clangd covers both). The relevant server binary must be on your PATH; if it isn't, the tool returns a clear "install X" error. After any edit, locode syncs the file to the live server so a subsequent `diagnostics` call reflects the change (it waits for the server to publish fresh diagnostics rather than reading a stale snapshot). Add or override servers with `locode config set lspServers` (see Config).
Note: even models with genuine native tool-calling support occasionally emit a tool call as plain text instead of a real structured call — this is model sampling variance, not a bug. If a turn seems to "describe" a tool call instead of running it, just ask again or try `/mode fallback`.
## Slash commands
```
/model <name> switch the model used for the current backend
/backend <name> switch backend (ollama | lmstudio), keeps current model
/mode <name> view or force tool-call mode (native | fallback)
/mouse [on|off] toggle mouse tracking (on by default; hold Shift+click/drag for native text selection)
/perm [mode] cycle or set permission mode (default | plan | auto-edit | auto-accept)
/status show current model, backend, tool-call mode, and cwd
/dashboard show session stats: token I/O, elapsed/model time, turns, tool calls
/tools list available tools
/permissions list mutating tools allowed for the rest of this session
/sessions list saved conversations you can resume with --resume
/mcp show connected MCP servers and their tool counts
/plugins show installed Claude Code-compatible plugins (commands, agents, skills, MCP servers)
/hooks show configured hooks per lifecycle event
/skills show installed skills; /<skill-name> [request] invokes one directly
/compact summarize the conversation now to free up context
/export [file] save the conversation as markdown (or /export json [file] for a full JSON dump incl. tool calls/results)
/import <path> [caption] attach a local file or image to your next message
/clear clear conversation history
/help show this help
/exit, /quit exit
```
## Config
Config precedence: CLI flags > env vars (`LOCODE_BACKEND`, `LOCODE_MODEL`, `LOCODE_BASE_URL`, `LOCODE_CONTEXT_WINDOW`, `LOCODE_MAX_ITERATIONS`, `LOCODE_AUTO_COMPACT_THRESHOLD`, `LOCODE_REQUEST_TIMEOUT_MS`) > persisted config file > defaults.
```sh
locode config set backend ollama
locode config set model qwen3-coder:30b
locode config set contextWindow 32768 # fallback size when auto-detection fails
locode config set maxIterations 40 # max tool calls per turn before locode gives up (default 50)
locode config set autoCompactThreshold 0.85 # fraction of context window at which auto-compact triggers
locode config set requestTimeoutMs 300000 # per-request timeout in ms (default 180000); raise this if
# your backend queues requests behind a concurrency limit
# (e.g. Ollama's OLLAMA_NUM_PARALLEL) under multi-session load
locode config set lspServers '{"java":{"command":"jdtls","extensions":[".java"]}}' # add a language server
# (JSON object keyed by language id; built-in ids
# override command/args, new ids add support and
# require extensions). Built-ins: typescript,
# python, go, rust, c (C/C++ share clangd).
locode config get
locode config path
```
## Known limitations
- Requires a real interactive terminal (TTY) — you can't pipe input into it or run it from a non-interactive script.
- Native tool-calling reliability varies by model and is non-deterministic even for capable models (see above).
- No OS-level sandboxing (no container/VM isolation) — mutating tools operate on the real filesystem/shell with the permissions of the user running `locode`. Only approve commands you understand. Two lightweight guardrails run unconditionally regardless of permission mode (including `auto-accept`), as a safety floor rather than a full sandbox: `write_file`/`edit_file`/`bash`'s `cwd` override can't target a path outside the working directory (`../` traversal, an absolute path elsewhere, or — on Windows — a different drive all refuse), and `bash` refuses a short list of unambiguously catastrophic commands (wiping the filesystem root or home directory, a fork bomb, formatting/wiping a whole drive, writing raw data to a block device) before they'd ever run. Neither guard stops a model from doing damage confined to *within* the project directory, or running something merely inadvisable — see `src/tools/pathGuard.ts` and `src/tools/bashGuard.ts`.
- In-app scrollback: mouse wheel scrolls the conversation view when mouse tracking is on (the default). PageUp/PageDown also scroll a page at a time. Hold Shift+click/drag for native terminal text selection and copy (when mouse tracking is on). Scrolling back up unpins the view from the latest message; scrolling back to the bottom (or sending a new message) re-pins it so new messages auto-scroll into view. Toggle mouse tracking with `/mouse on|off`.
- Windows shell quoting for the `bash` tool has only had light testing; behavior may differ from Unix shells for complex quoting.
- `git_commit` covers add/commit/create_branch/checkout/push/reset/stash/merge/rebase/delete_branch. Use `bash` for anything beyond that.
- MCP tool results support text, image, audio, and resource content blocks. Images are returned in the same shape as `read_file` so vision-capable models can see them; audio and binary resources are summarized. Remote (HTTP) MCP servers support static headers (e.g. a bearer token) but not OAuth flows.
- Compaction (`/compact` or automatic) replaces history with a model-generated prose summary — it costs one extra model call and loses tool-call/tool-result detail (the model's own account of what happened survives; the raw record doesn't). The auto-compact threshold defaults to 85% and is configurable via `locode config set autoCompactThreshold` or `LOCODE_AUTO_COMPACT_THRESHOLD`.
- Plugin support (`locode plugin add`) now covers every part of a plugin: MCP servers, slash commands, agents, hooks, and skills. A slash command's `allowed-tools` frontmatter restricts that one invocation's toolset (same tool-name translation as an agent's `tools:` — see `/plugins`); a skill invoked directly via `/<skill-name>` isn't restricted this way, since skills have no `allowed-tools` field of their own. Duplicate MCP server names across sources are now detected and surfaced in `/mcp` — project-level wins over user-level wins over plugin-level. Duplicate slash-command and skill names are also surfaced in `/plugins` and `/skills`.
- Skills are exposed as one shared `skill` tool rather than one tool per skill — if two plugins install a skill with the same name, the first plugin in load order wins and the collision is shown in `/skills`. Skills can bundle sibling `references/*.md` files that are included when the skill is invoked.
- Hooks cover the most useful subset of Claude Code's lifecycle events: `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PermissionRequest`, `SubagentStart`, `SubagentStop`, `CwdChanged`, `FileChanged`, `ConfigChange`, `Stop`, `SessionEnd`. `CwdChanged` is declared in the config format but not yet fired anywhere — a cwd never changes mid-session in locode today, so configuring it is a no-op for now. All three hook types are supported: `command` and `http` run an external process/request, and `prompt` just injects its static `message` as additional context (the same way a command/http hook's stdout does) — it has no process to fail, so it can't block an event the way a command hook's exit code 2 can. Command/http hooks can opt into structured JSON output via `outputSchema: "json"`. `PreToolUse`, `UserPromptSubmit`, and `PermissionRequest` can block; the rest are fire-and-forget. `SessionEnd` fires on every exit path (including Ctrl+C and external `SIGTERM`/`SIGHUP`), so it doesn't always have a real session id to report. All hooks for an event run in parallel with no defined ordering, and every configured hook always runs — there's no way to disable one without editing the file it came from.