letscode
A minimal, OpenAI-compatible coding agent for the terminal — written in Python. Point it at any OpenAI-API-compatible endpoint (Ollama, Fireworks, OpenRouter, vLLM, llama.cpp's llama-server, …) and get a streaming agent loop with the four tools that cover 95% of coding sessions — read, write, edit, bash — plus skills, slash commands, and a plugin system you can extend.
Status: v0.7.1, alpha.
The full docs live in docs/ and build via Zensical — run make docs-serve for a live preview, or make docs for a static build.
Or go to https://letscode.hop3.abilian.com for the online doc.
What it does
- Streaming agent loop with parallel tool execution.
- Any provider, picked for you. Name a model and letscode routes to the right endpoint and API key —
letscode --model deepseek-chatusesDEEPSEEK_API_KEY, no per-sessionOPENAI_BASE_URLjuggling./modelopens an interactive picker (with your full OpenRouter catalog whenOPENROUTER_API_KEYis set). Built-ins for OpenAI, Fireworks, DeepSeek, Z.AI, OpenRouter, and Gemini; add more in~/.letscode/catalog.toml. - Four built-in tools:
read,write,edit,bash. Restrictable with--tools/--no-tools. @pathand!cmdin your prompt:@fileinlines a file's contents,!cmdsplices a shell command's output. Plus reusable prompt templates in~/.letscode/prompts/.- Skills in the agentskills.io format (
SKILL.md+ YAML frontmatter). Drop a file in~/.letscode/skills/<name>/and the agent picks it up — the model invokes it as a tool, or you invoke it as/skill:<name>. - Sessions persisted as JSONL under
~/.letscode/sessions/.-ccontinues the most recent for the current directory;/tree,/fork,/clonebranch and duplicate them. - Type while it's working. Enter stops the run; type then Enter stops it and redirects to the new instruction. Ctrl+C also stops; press it twice to exit.
- Slash commands:
/help,/quit,/reset,/model,/tools,/skills,/plugins,/reload,/tree,/fork,/clone,/new,/resume,/name,/copy,/export,/share,/compact,/steer,/follow-up,/queue,/verbose,/footer, plus/skill:<name>. Plugins can add more. Type/and press Tab to complete them — plugin commands and their:subcommandsincluded. - TOML config in
~/.letscode/config.toml(user) +.letscode/config.toml(project, overrides). Runletscode --initto scaffold a commented starter. API keys come from env vars only. - First-run nice:
letscode --version,letscode --init, and an actionable message if you forget the API key — never a traceback. - Plugin system based on
pluggy: tools, commands, skills, frontends, plus lifecycle hooks.letscode install <name-or-path-or-git-url>drops a plugin into a shared per-user location everyletscodeon the machine can see,letscode pluginsshows what's installed and where, andletscode run <tool>runs any command-line tool a plugin ships, from anywhere. Plugins load in isolation — one that fails to import is skipped with a warning instead of taking down the CLI. Seedocs/plugins/. - RPC mode (
letscode --mode rpc) — NDJSON over stdio, for IDE / host-app integration. - Rich terminal — Markdown-rendered assistant text, syntax-highlighted
readpanels, per-turn footer with model / tokens / cost / context-pct (and cache-hit count when the provider reports one).
The deliberate non-goals — native Anthropic / Bedrock / Vertex provider modules, MCP in core, sub-agents in core, plan mode, built-in todos — are kept outside core on purpose; each can be a plugin. (MCP already ships as one — letscode-mcp.) Full list in the roadmap.
Install
uv tool install letscode # or: pipx install letscode
Then letscode --version and letscode --init to scaffold a config.
To hack on it instead:
git clone https://github.com/abilian/letscode
cd letscode
uv sync
uv run letscode --version
Quickstart
Option A — local with Ollama (free)
brew install ollama
ollama serve &
ollama pull qwen2.5-coder:7b
uv run letscode \
--base-url http://localhost:11434/v1 \
--api-key ollama \
--model qwen2.5-coder:7b \
"create hello.py that prints 'hi from letscode' and run it"
The first call downloads the model (~4.5 GB for the 7B). After that, everything runs locally; any non-empty string works as the API key.
Option B — a hosted provider (automatic routing)
Set the provider's API key and name a catalog model — letscode routes to the right endpoint for you:
export DEEPSEEK_API_KEY=... # or GLM_API_KEY / GEMINI_API_KEY / OPENROUTER_API_KEY / FIREWORKS_API_KEY
uv run letscode --model deepseek-chat "explain what this directory contains"
Built-in providers: OpenAI, Fireworks, DeepSeek, Z.AI, OpenRouter, Gemini. Run /model for the interactive picker, or add your own models in ~/.letscode/catalog.toml.
Any other OpenAI-API-compatible endpoint works the direct way — vLLM, llama.cpp's llama-server, deepinfra, together.ai, etc. Set OPENAI_BASE_URL, OPENAI_API_KEY, and LETSCODE_MODEL (or pass --base-url, --api-key, --model).
Modes
letscode # interactive — streaming TUI
letscode "what does main.py do?" # one-shot, rich rendering, then exit
letscode -p "summarize this directory" # print mode — plain stdout, pipe-friendly
letscode -c "now do step 2" # continue most recent session for cwd
letscode --version # print the version (no key needed)
letscode --init # scaffold ~/.letscode/config.toml (no key needed)
letscode --mode rpc # NDJSON over stdio, for IDE/host-app integration
Print-mode exit codes: 0 (ok), 1 (provider error), 130 (Ctrl+C), 2 (usage error).
Tools
The four built-ins, enabled by default:
| Tool | What it does |
|---|---|
read |
Read a file (with optional line range). |
write |
Create or overwrite a file atomically. |
edit |
Apply a search-and-replace edit. |
bash |
Run a shell command with a timeout. |
Restrict with --tools=read,bash (comma-separated), or disable all with --no-tools.
Skills
Drop a SKILL.md file in any of these paths:
~/.letscode/skills/<name>/SKILL.md # user-global
~/.agents/skills/<name>/SKILL.md # agentskills.io standard
~/.pi/agent/skills/<name>/SKILL.md # additional path
~/.claude/skills/<name>/SKILL.md # additional path
.letscode/skills/<name>/SKILL.md # project-local (walked up from cwd)
.agents/skills/<name>/SKILL.md
.pi/skills/<name>/SKILL.md
.claude/skills/<name>/SKILL.md
Format:
---
name: code-review
description: Use when reviewing code for correctness, style, or security.
---
# Code Review
When invoked:
1. Read the changed files.
2. Look for off-by-one errors, race conditions, missing input validation.
3. Suggest improvements with concrete examples.
Skills written in this format for other agentskills.io-format tools load unchanged. Two ways to invoke:
- The model calls it as a tool. Every skill is auto-wrapped as
skill_<normalised-name>(e.g.write-a-prd→skill_write_a_prd). The model invokes via native tool-calling. - The user types
/skill:<name> [message]. Same effect; the slash form keeps the original skill name with hyphens preserved.
Full reference in docs/user-guide/skills.md.
Slash commands
Type at the prompt during an interactive session. Full reference in docs/user-guide/slash-commands.md. The most common:
/help— list every registered command./skill:<name> [message]— prepend a skill's body to the next user message./tree·/fork [n]·/clone— navigate / branch the session./compact [bias]— collapse older history into a summary; optional text biases the summarisation prompt./reload— re-read TOML config + reload skills mid-session./verbose— toggle full vs. truncated tool-result panels./plugins— list loaded plugins (built-in + entry-point) with version.
Configuration
User-global at ~/.letscode/config.toml, project-local at .letscode/config.toml:
model = "deepseek-chat" # a catalog model; letscode routes it to DeepSeek automatically
frontend = "basic"
timeout = 300
[memory] # plugin-owned section, passed through to letscode-memory
relevance = false
letscode --init writes a commented starter file (it never overwrites an existing one).
Resolution order (lowest → highest priority):
- Built-in defaults
- User config (
~/.letscode/config.toml) - Project config (
./.letscode/config.toml) - Environment variables (
LETSCODE_MODEL,OPENAI_BASE_URL,OPENAI_API_KEY) - CLI flags
API keys are deliberately not loaded from the file. Full reference in docs/user-guide/configuration.md.
AGENTS.md and CLAUDE.md files in cwd and its ancestors, plus ~/.letscode/AGENTS.md, are loaded automatically and appended to the system prompt. Use --no-context-files to skip.
Writing a plugin
A plugin is a Python package with at least one @hookimpl declaration and a letscode entry-point. Minimal example:
import pluggy
from pydantic import BaseModel
from letscode.agent.tools import ToolContext, ToolResult, tool
from letscode.llm.types import TextPart
hookimpl = pluggy.HookimplMarker("letscode")
class _GreetParams(BaseModel):
name: str
@tool(name="greet", description="Say hello to someone.")
async def _greet(params: _GreetParams, ctx: ToolContext) -> ToolResult:
del ctx
return ToolResult(content=[TextPart(text=f"Hello, {params.name}!")])
@hookimpl
def letscode_register_tools(registry):
registry.add(_greet)
[project.entry-points.letscode]
my_plugin = "mypkg.plugin"
pip install -e . and the LLM can now call the greet tool.
For the full surface — every hook, the stable-import contract, the versioning rule, packaging notes, ExecutionEnv overrides, the trust model — see docs/plugins/authoring.md. For the design rationale behind the extension model itself, see docs/architecture/extension-model.md.
A worked reference plugin lives in plugins/letscode-memory/ — cross-session memory via /remember, /memories, and /forget, implemented in a handful of hooks against the same stable surface external plugins target.
Project layout
src/letscode/ # the app: agent loop, CLI, frontends, tools, skills, plugin system
plugins/ # in-repo reference plugins (letscode-memory, letscode-goal, letscode-mcp, letscode-textual)
docs/ # public documentation (Zensical)
tests/ # a_unit / b_integration / c_e2e (the latter gated by LETSCODE_E2E=1)
A monorepo split is on the roadmap once external plugin contracts are exercised. For now everything ships as a single package.
Development
uv sync # install
make check # lint + type-check
make test # pytest + coverage gate
make # check + test
make docs # build docs (Zensical, --strict)
make docs-serve # live-reload docs preview
make verify-dist # build the wheel and drive it from a clean venv
make verify-plugin # discover letscode-memory from a built install
For the end-to-end smoke suite (real LLM, costs money or runs locally):
LETSCODE_E2E=1 \
LETSCODE_E2E_BASE_URL=https://api.deepseek.com \
LETSCODE_E2E_API_KEY=$DEEPSEEK_API_KEY \
LETSCODE_E2E_MODEL=deepseek-chat \
uv run pytest tests/c_e2e/
Documentation
The full documentation lives in docs/ and is built with Zensical. Top-level sections:
- Getting started — install, first run, switching providers.
- User guide — configuration, slash commands, skills, sessions.
- Plugins — authoring guide, reference plugin.
- Architecture — extension model, agent loop.
- About — changelog, roadmap, lessons learned, related projects.
License
MIT.
Metadata
Release files for letscode 0.7.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| letscode-0.7.1.tar.gz | 283.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| letscode-0.7.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 431.9 kB
Release files / letscode-0.7.1.tar.gz
| Download URL | letscode-0.7.1.tar.gz |
|---|---|
| Size | 283.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
43234df86158061d3e98765b9ecdd8cc656cea7f6b1c34d9c1e6afb1af2261b0
|
|
BLAKE2b-256 checksum How to use checksums |
5c08efed9ced61ee62ffe463281eede09242625f58371e741c2c0cce56f4f25f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.33 {"installer":{"name":"uv","version":"0.11.33","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / letscode-0.7.1-py3-none-any.whl
| Download URL | letscode-0.7.1-py3-none-any.whl |
|---|---|
| Size | 148.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
58dad807bf7605be7c6f1dc1b4c0c00fd301d10bac2ee18d6c435935292ac0e5
|
|
BLAKE2b-256 checksum How to use checksums |
eaaa65169189f5571ca20644c78ab2e24f629c085c5a0f56aaa831cc0c4987f6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.33 {"installer":{"name":"uv","version":"0.11.33","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|