A lightweight, personal Claude Code-style agentic CLI for LOCAL LLMs (LM Studio).
Project description
llmc-code
A lightweight, personal Claude Code-style agentic CLI in pure Python — for LOCAL LLMs only.
It gives you a streaming terminal chat with an agentic tool-use loop, multi-agent orchestration (role-scoped sub-agents), codebase intelligence (explore-before-act), and a build → validate → fix workflow. There is no hosted/cloud provider — it drives a local model served by LM Studio's OpenAI-compatible server. A deterministic mock provider exists purely for offline testing.
Providers:
- local — your LM Studio model (native OpenAI-style tool calls, with a fenced-JSON text fallback for models that lack native tool support).
- mock — deterministic and offline, for testing with no keys/network.
Install
Install it as a global command with pipx (or uv tool).
This puts the llmc-code command on your PATH inside an isolated environment, so it
runs from any directory with no venv to activate:
pipx install llmc-code
# or: uv tool install llmc-code
Then run llmc-code anywhere. Upgrade later with pipx upgrade llmc-code.
To install the latest unreleased build straight from source:
pipx install git+https://github.com/Adhithya-Karthikeyan/llmc-code.git
Requires Python ≥ 3.10.
Development install
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
This installs the llmc-code console script. You can also run it as a module with
python -m llmcode.
Usage
Interactive REPL (uses your saved provider/model, defaults to local):
llmc-code # or: python -m llmcode
llmc-code --provider mock # offline, scripted demo
llmc-code --provider local --model <your-lm-studio-model-id>
One-shot, non-interactive (--yes auto-confirms dangerous tools so it runs
unattended):
llmc-code --provider mock --yes -p "create hello.py that prints hi and run it"
llmc-code -p "explain what this repo does"
Flags:
| Flag | Meaning |
|---|---|
--provider {local,mock} |
Pick the backend (default local) |
--model <name> |
Model id for the active provider |
--base-url <url> |
LM Studio base URL (default http://localhost:1234/v1) |
-p, --prompt "<text>" |
Run one prompt and exit |
-c, --continue |
Resume this project's saved session at startup (see Session memory) |
--yes, -y |
Auto-confirm write/edit/bash tools (untrusts model + fetched content) |
--max-iterations <n> |
Cap provider turns per message (use >=2 for tool tasks) |
--effort <level> |
Reasoning effort: off|low|medium|high (best-effort) |
--theme <name> |
Color theme: amber (default, warm polished look) | auto (truecolor) | ansi (Dark mode, ANSI colors only) | orange. Session-only unless --save |
--mcp {on,off} |
Enable/disable MCP servers (~/.llmcode/mcp.json). off starts no servers and sends no MCP tools — smaller prompt, faster tok/s. Session-only unless --save |
--context N|auto|fixed|off |
Working-context budget (~tokens). llmc-code auto-trims history to it after each turn so decode stays fast; adaptive (flexes per request) by default. Session-only unless --save |
--private |
Opt into the offline lockdown for this session: no external egress (see below). Add --save to persist |
--allow-network |
Accepted no-op alias for back-compat (network is already the default). Explicitly keeps network on. Add --save to persist |
--save |
Persist the given flags as the new default in ~/.llmcode/config.json |
Slash commands (in the REPL)
/help Show help
/provider <name> Switch provider: local | mock
/models List models available on the server
/model <name> Set the model (verified against the server's list)
/effort <level> Reasoning effort: off | low | medium | high (best-effort)
/theme <name> Color theme: amber (default) | auto | ansi | orange
/compact Aggressively summarize ALL history into one tight note,
keeping only the last exchange (frees the most context)
/context [N|auto|fixed|off]
Working-context budget — llmc-code auto-trims history to this
after each turn so decode stays fast. auto = flex per
request (default); fixed = flat N; off = trim only near
the model's window. Persisted.
/audit [path] Map-reduce audit: review the repo in small isolated chunks
(fast; small context) → merged report. Default path '.'
/speed Tips to raise tok/s (LM Studio settings + context size)
/mcp [on|off] No arg: list MCP servers + status. on/off: enable/disable
MCP (off = fewer tools per prompt → faster). Persisted.
/clear Clear the conversation history
/resume Reload this project's saved session (local-only memory)
/forget Delete this project's saved session
/exit, /quit Leave
Ctrl+O Reveal full detail (args+results) of the last turn
Session memory
llmc-code remembers the conversation per project directory (Claude-Code-style: auto-save, opt-in resume), so you can pick up where you left off.
-
Auto-save. After every completed turn (and on a clean exit) the running conversation is saved for the current working directory. A crash never loses it. A history of just the system prompt is skipped — nothing to remember.
-
Opt-in resume. A fresh launch stays light and does not reload the history automatically. When a saved session exists, the REPL prints a dim hint:
↩ last session (5m ago, 12 msgs): fix the config parser — /resume to continue (or relaunch with -c)Reload it with
/resumeinside the REPL, or launch with-c/--continueto load it before the first turn./forgetdeletes this project's saved session. -
Where + privacy. Sessions are stored locally only, as JSON under
~/.llmcode/sessions/<dir>-<hash>.json, keyed by the absolute path of the directory. They are never sent anywhere. The saved conversation may include file contents the model read while working — fine for a local tool, but the file stays on your machine. When you resume, the model-aware auto-compaction already bounds how much of that history re-enters the context window.
One-shot (-p) runs also save their conversation, and -p -c prepends the saved
history first — so an unattended one-shot can build on itself across runs.
Networking & safety (default: network enabled)
llmc-code runs network-enabled by default: web_fetch is available, run_bash
can reach the network, a non-loopback base_url is allowed, all configured MCP
servers start, and proxy env vars are honored. The startup banner and the REPL
status line show the active state: private mode: OFF — network enabled (or
private mode: ON — offline lockdown under --private).
The on-device local model on loopback (LM Studio on 127.0.0.1) is still the
default backend; enabling network does not change where your model runs.
Always-on safety (active in BOTH default and --private modes)
These guards are never disabled — they hold even with network enabled, so the tool only reaches safe URLs/endpoints:
web_fetchSSRF guard. Onlyhttp/httpsURLs are allowed. The resolved IP is validated and the tool refuses loopback, private (RFC 1918), link-local, reserved, CGNAT (RFC 6598), multicast, and unspecified ranges, plus the cloud-metadata endpoint169.254.169.254. The host is resolved and the connection is pinned to the validated IP (DNS-rebinding defense), and the host is re-validated on every HTTP redirect hop. Download size and timeout are capped. This is fully active wheneverweb_fetchruns (i.e. by default).run_bashis confirmation-gated (unless--yes), its writes are confined to the workspace root, and its child env is built from a secret-scrubbing allowlist (provider keys /AWS_*/GITHUB_TOKENetc. are not inherited).write_file/edit_fileare workspace-confined — they refuse to touch paths resolving outside the directory the CLI was launched from.- No telemetry. There is zero analytics / usage / crash reporting.
--private (opt-in offline lockdown)
Pass --private to opt into a strict, no-egress mode (add --save to persist).
This is enforced, not advisory, and is on top of the always-on guards
above:
- Provider base_url must be loopback. A non-loopback
base_url(anything other than127.0.0.0/8,::1, orlocalhost) is REFUSED with a clear error — never silently used, and the loopback host is IP-pinned. Validated at all three entry points: the--base-urlflag, the persisted~/.llmcode/config.json, and every in-session rebuild (/provider,/model,/effort). web_fetchis removed from the tool set. The model can't even call it (and a direct call is refused).run_bashnetwork egress is blocked at the OS level. Commands run under a macOSsandbox-execno-network profile (deny network-outbound, re-allowing loopback only); the child and all its descendants are kernel-blocked from opening external sockets. Ifsandbox-execis unavailable,run_bashfails closed (refuses) rather than running unsandboxed.- No proxy tunneling. The OpenAI client is built with
httpx.Client(trust_env=False)soHTTP_PROXY/HTTPS_PROXY/ALL_PROXYcannot tunnel even loopback traffic to an external proxy. - MCP servers are gated. Only servers explicitly marked
"private_ok": trueinmcp.jsonare started; others are skipped with a warning. (In the default network-on mode, all configured servers start.)
Honest residual notes:
- In the default (network-on) mode
run_bashis not network-sandboxed, so a command you approve can reach the network — the confirmation gate is the boundary there. - Under
--private,sandbox-execis deprecated (its man page is dated 2017) but remains functional on current macOS; Apple could remove it in a future release. - An MCP server you allow (
private_ok, or any server in the default mode) can egress — the allow decision is yours.
MCP (Model Context Protocol)
llmc-code can connect to local MCP servers over the stdio transport and
expose their tools to the agent — a stdlib-only, synchronous JSON-RPC 2.0 client
(no mcp SDK, no new dependencies).
It is opt-in: with no config file, MCP is simply off and nothing changes.
To enable it, create ~/.llmcode/mcp.json in the Claude Desktop format:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/some/dir"],
"env": {},
"disabled": false,
"trustReadOnlyHint": false,
"private_ok": false,
"timeout": 30
}
}
}
- On REPL start (and on every one-shot
-prun) each enabled server is spawned, handshaken (initialize->notifications/initialized->tools/list), and its tools are merged into the orchestrator's tool set. A connect line prints, e.g.mcp: filesystem connected (11 tools). A server that fails to start logs a dimmcp: <name> failed: ...and is skipped — it never crashes the CLI. - MCP tools are named
mcp__<server>__<tool>and render in the collapsed line asserver:tool <arg>. /mcplists each configured server, whether it connected, and its tool names.- Servers are cleanly shut down on exit (stdin closed, terminate, then kill).
Safety notes / limitations
--privategating. By default (network on) all configured servers start. Under--private, an MCP server is an egress surface (it is not network-sandboxed), so it is skipped unless you explicitly mark it"private_ok": true. A local vault writer likekyp-memshould be marked"private_ok": trueso it still works under--private.- stdio transport only. HTTP / SSE / Streamable-HTTP MCP servers are not supported.
- Tool results are untrusted. MCP output flows into the model like any other
tool result (same as
web_fetch). - Confirmation is on by default. Every MCP tool is confirmation-gated. A
tool's
readOnlyHintis self-asserted by the (untrusted) server, so it is NOT trusted on its own — a malicious server could mark a destructive tool read-only to auto-run it. The hint only relaxes the gate when you opt in per-server with"trustReadOnlyHint": true. The trust decision is the operator's, never the server's. - No secret inheritance. Child processes get a minimal allowlisted env
(
PATH,HOME, locale, temp) plus the server's configuredenv. Provider and cloud secrets (OPENAI_API_KEY,LMSTUDIO_API_KEY,AWS_*,GITHUB_TOKEN, ...) are not forwarded — give a server a secret only explicitly via itsenvblock. - Per-call timeout. Each request is bounded by
"timeout"seconds (default 30). On atools/calltimeout the connection is treated as poisoned: the child is killed (no orphan) and its tools are dropped, so a stale reply can never corrupt a later call. Raisetimeoutfor legitimately long operations. - Name hardening. Server-controlled tool names are validated (no
__namespace forging) and de-collided — a duplicate full name is skipped, never silently overwriting another tool. - Tool flooding. A server advertising many tools enlarges the model's tool list; keep the configured set focused.
Concise output, collapsed tool lines, tok/s
The CLI presents like a senior engineer: terse and skimmable.
- Concise answers — the orchestrator + sub-agents follow an output contract: up to 5 markdown headers (and none for short answers), tight one-idea-per-line bullets, the answer first, no preamble or process narration. Competence is unchanged; only presentation is constrained.
- Collapsed tool output — by default each tool call is ONE line, e.g.
⏺ read_file README.mdfor reads or⏺ run_bash pytest -q (exit 0)for bash (onlyrun_bashshows an exit-code hint). A failure shows a dim✗ <error>. Full args and results are not dumped; they are stored for the current turn. Ctrl+Oto expand — press it at the>prompt to immediately print the full args + results of the turn that just finished. Limitation: the REPL is line-based, soCtrl+Ois handled only while the prompt is active — it cannot interrupt a running turn; press it at the next prompt. Only the orchestrator's detail is revealed; sub-agent tool calls still stream as nested collapsed lines.- tok/s footer — after each visible assistant message, a dim footer reports
the generation rate, e.g.
38.4 tok/s. The local provider reads the server'scompletion_tokens(viastream_options.include_usage); when absent it approximates from the text. - Duck working-indicator — while the CLI is busy (waiting on the model, or
running tools) a small ASCII duck waddles to-and-fro in place. It is purely
decorative (no words) and terminal-only: it is fully disabled when output
is piped or not a TTY (one-shot in a pipe, captured tests), so it never leaks
stray characters into redirected output. It always erases its own line before
any real output (the answer, the collapsed tool line, the footer, or a
y/Nconfirm prompt). SetLLMCODE_NO_SPINNER=1to turn the duck off on a TTY too. /compact— summarizes the conversation so far via the active provider into a compact context note (preserving decisions, files touched, open tasks, key facts), replaces the long history with[system summary + the last 1–2 turns], and prints a rough before→after token estimate. If the provider call fails it no-ops and leaves history untouched.
Pointing at LM Studio
-
Open LM Studio → load a model (one that supports tool/function calling gives the best results).
-
Start its local server (Developer tab → Start Server). It exposes an OpenAI-compatible API at
http://localhost:1234/v1by default. -
Point llmc-code at it:
llmc-code --provider local --model <the-model-id> --base-url http://localhost:1234/v1
The API key is read from OPENAI_API_KEY or LMSTUDIO_API_KEY, defaulting to the
harmless string lm-studio (LM Studio ignores it, but the OpenAI SDK requires a
non-empty value).
Fenced-JSON tool-call fallback
If the loaded model can't do native tool calls, the local provider falls back to
parsing a fenced ```json tool-call block from the model's text output:
```json
{"tool": "read_file", "input": {"path": "main.py"}}
```
tool maps to the tool name and input to its arguments.
Mock (offline)
llmc-code --provider mock --yes -p "create hello.py that prints hi and run it"
The mock provider is deterministic and needs no keys or network. Its hello
scenario scripts a write_file then a run_bash tool call so you can validate
the full agent loop offline. It's also what the test suite uses.
How it works
The "intelligence" is emergent from three things working together:
- Good exploration tools —
repo_map(compact structural index),grep, andread_file(withoffset/limitfor slices) so the model learns the code without dumping whole files into context. - A strong orchestrator prompt that enforces explore → plan → act → validate and delegates multi-file reading to sub-agents to keep its context small.
- Delegation —
spawn_agentspins up focused, role-scoped sub-agents; the/auditcommand map-reduces a whole-repo review across small isolated chunks.
Architecture
llmcode/
config.py Settings, LM Studio defaults, ~/.llmcode/config.json persistence
prompts.py System prompts: ORCHESTRATOR, EXPLORER, CODER, REVIEWER
tools.py Tool registry: read_file (offset/limit), write_file, edit_file, run_bash, glob, grep, repo_map
audit.py Map-reduce /audit: chunk the repo, review each chunk in an isolated context, merge
providers.py Provider ABC + LocalProvider (LM Studio) + MockProvider
(normalized to {type: text|tool_call|done} events)
agent.py The agentic loop: stream → run tools → feed results → repeat
orchestration.py spawn_agent delegation tool + role toolsets
mcp.py stdio MCP client + manager (sync JSON-RPC 2.0, stdlib only)
repl.py prompt_toolkit input + rich streamed output; provider factory
__main__.py CLI: flags, one-shot -p mode, `llmc-code` entry point
tests/ pytest suite (MockProvider only; no network)
Normalized provider events
Every provider's stream_chat(messages, tools) yields plain dicts with a type:
{"type": "text", "text": <str>}— incremental assistant text (deltas).{"type": "tool_call", "id", "name", "arguments": <dict>}— args alreadyjson.loads'd into a dict.{"type": "done", "finish_reason": <str>, "output_tokens": <int|None>}— exactly one, always last.output_tokensis the completion token count for the message (the local provider'scompletion_tokens, orNonewhen the server did not report it so the agent approximates for the tok/s footer).
All tool calls are emitted before the single terminal done.
Agent loop
- Send the conversation + tool schemas to the provider; stream the reply.
- If the model returns tool calls, execute each (with confirmation for
write_file/edit_file/run_bashunless--yes), append the structured results asrole:"tool"messages. - Loop until the model returns a plain-text final answer (or the iteration guard trips).
Roles
| Role | Tools | Purpose |
|---|---|---|
| orchestrator | all + spawn_agent |
Plans, delegates, validates |
| explorer | read-only (repo_map, read_file, glob, grep) |
Investigates the codebase |
| coder | full | Implements a scoped change, then validates |
| reviewer | read-only | Critiques a change before finalizing |
Development / tests
source .venv/bin/activate
pip install -e ".[dev]"
pytest -q
All tests run offline against the mock provider — no API keys, no network. The
openai import is lazy (only inside LocalProvider), so the agent/tool/mock
stack imports and runs without openai installed.
Notes / status
- The local (LM Studio) provider is fully implemented but needs a running LM Studio server with a model loaded to exercise live; it is not network-tested here. The mock provider and the full agent/tool/orchestration stack are covered by the test suite.
- Config (provider, model, base URL) persists to
~/.llmcode/config.json. API keys are read from the environment only and never written to disk.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file llmc_code-0.2.0.tar.gz.
File metadata
- Download URL: llmc_code-0.2.0.tar.gz
- Upload date:
- Size: 446.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f6078323d47ea3b8743ab69b71af1a487d3cdb2d3052c025f712febfd6253211
|
|
| MD5 |
332558bfbf6ec1a7e2727974ba34f1ae
|
|
| BLAKE2b-256 |
0a4170ec321012b4c47d26861ec7a4f0c6777e0f5f1fa15e6c00f9a8319d19f1
|
File details
Details for the file llmc_code-0.2.0-py3-none-any.whl.
File metadata
- Download URL: llmc_code-0.2.0-py3-none-any.whl
- Upload date:
- Size: 278.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9376f231140d3ec6e136a40eaefd14af88d331f83a90e8f1a6e41b36aea48d3d
|
|
| MD5 |
6f443683f7ff830c4a133b0f998e5408
|
|
| BLAKE2b-256 |
9126485b3814088fe23950d9acdad20dad5d9f50db3413bfcaf70da085493ed7
|