A minimal coding agent for the terminal.
Project description
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.6, 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.
- Four built-in tools:
read,write,edit,bash. Restrictable with--tools/--no-tools. - 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,/compact,/steer,/follow-up,/queue,/verbose,/footer, plus/skill:<name>. Plugins can add more. - 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. 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. 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 — Fireworks (hosted)
The reference test endpoint is Fireworks running MiniMax M2.5:
export OPENAI_BASE_URL=https://api.fireworks.ai/inference/v1
export OPENAI_API_KEY=$FIREWORKS_API_KEY
export LETSCODE_MODEL=accounts/fireworks/models/minimax-m2p5
uv run letscode "explain what this directory contains"
Any OpenAI-API-compatible endpoint works the same way — OpenRouter, 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 = "accounts/fireworks/models/minimax-m2p5"
base_url = "https://api.fireworks.ai/inference/v1"
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-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.fireworks.ai/inference/v1 \
LETSCODE_E2E_API_KEY=$FIREWORKS_API_KEY \
LETSCODE_E2E_MODEL=accounts/fireworks/models/minimax-m2p5 \
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.
Project details
Release history Release notifications | RSS feed
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 letscode-0.6.1.tar.gz.
File metadata
- Download URL: letscode-0.6.1.tar.gz
- Upload date:
- Size: 247.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30bc4fe09df08b9d1f4194d46a3ccad5a3eb998fa4c2cb87c034c96c386b8d91
|
|
| MD5 |
04212cbefc05c7319e5bd744189177d6
|
|
| BLAKE2b-256 |
6d1200de33b7b790db9f2d062cfa684fcf96ad6d96739dc8f5f4314d5cdc6ac8
|
File details
Details for the file letscode-0.6.1-py3-none-any.whl.
File metadata
- Download URL: letscode-0.6.1-py3-none-any.whl
- Upload date:
- Size: 103.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3db2612e3eb063daa449eae974cf73930d88608da11d0a5a2a9f94c8b51c950d
|
|
| MD5 |
97a90cdfa73eb05aa8997f22096d8243
|
|
| BLAKE2b-256 |
a33c7a682fba1304aa089f7b7171272351ff86fc5be6f33bb36a065a41f31ada
|