kimandClaude Sonnet 5 e572fc5d76 refactor: table-drive the built-in slash-command dispatch
handleSubmit's ~200-line if/return chain becomes a declarative
{test, run}[] list plus one dispatch loop. Same matching semantics
(exact vs. loose startsWith), same order (/permissions before /perm,
/model before /mode), same handler bodies — just uniform structure and
a single place the command set is registered. Handlers still close over
component state; extracting them into an independently testable module
is a larger follow-up.

Also drops a stale comment claiming Ollama can't report context length
for ":cloud" models — /api/show now returns it for every cloud model
tested (glm-5.2, qwen3.5, kimi-k2, minimax, gemma4).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 16:05:45 +09:00
2026-07-06 12:41:15 +09:00

locode

An agentic coding CLI, in the spirit of Claude Code, for models running locally via Ollama or LM Studio. It talks to either backend's OpenAI-compatible /v1/chat/completions endpoint, so any model you can serve from either one works here.

Install

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:

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:

locode config set backend ollama
locode config set model qwen3-coder:30b
locode

List models available from the configured backend:

locode models

Every conversation is auto-saved as you go. Resume later:

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 servers:

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:

{
  "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):

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:

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 (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 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.

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.
S
Description
No description provided
Readme
1.1 MiB
Languages
TypeScript 100%