Builds out locode's Claude Code plugin parity: MCP servers, slash commands, sub-agents, hooks (12 lifecycle events), and skills, all loadable from a local path or git URL. Also adds image support (read_file, /import), @-mention file autocomplete, a /dashboard stats view, /export with an editable filename prompt, an expanded git tool (reset/stash/merge/rebase/delete_branch), and a redesigned status bar. On top of that, a review pass found and fixed 10 correctness/stability bugs: argument-injection in git reset/merge/rebase (a ref like "--hard" was parsed as a flag), tool-call image results interleaving with native tool_call_id messages and breaking OpenAI-compatible message ordering, backgrounded bash jobs still being silently killed by their original timeout with the kill masked as a clean exit, a SubagentStart hook's block being ignored, the sub-agent timeout clock starting before the hook it should exclude, a template-expansion bug that could re-substitute $1..$9 placeholders, unbounded background-job output buffers, a missing directory-target fallback in /export, and image size caps checked after reading the whole file instead of before. Session saves and exports now go through a shared atomic write-then-rename helper so a crash can't leave a truncated file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
157 lines
16 KiB
Markdown
157 lines
16 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.
|
|
|
|
- **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`, `web_search`, `web_fetch`, `git_status`, `bash_output` run automatically. `write_file`, `edit_file`, `bash`, 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.
|
|
- **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>`. A server's tool is treated as mutating (confirmation required) unless it declares itself read-only via the MCP `readOnlyHint` annotation. 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 are supported (`prompt` is declared in the config format but not yet executed); command 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`.
|
|
|
|
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)
|
|
/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 — opens an editable filename prompt (default: locode-export-<timestamp>.md)
|
|
/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`) > 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 25)
|
|
locode config set autoCompactThreshold 0.85 # fraction of context window at which auto-compact triggers
|
|
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 sandboxing beyond the confirmation prompts — mutating tools operate on the real filesystem/shell with the permissions of the user running `locode`. Only approve commands you understand.
|
|
- Session resume replays prior user/assistant text so you can see it, but it doesn't re-display prior tool-call/tool-result lines from before the resume (the model still has that history — it's just not re-rendered).
|
|
- No in-app scrollback — once a message scrolls off the top of the window it's gone until you resize the terminal taller (the conversation itself is still intact and sent to the model; this only affects what you can visually re-read).
|
|
- 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 plugin's `allowed-tools` restriction on a command isn't enforced (the expanded prompt just runs as a normal turn with the full toolset). 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. `command` hooks and `http` hooks are supported; `prompt` hooks are declared in the config format but not yet executed. Command 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.
|