Skip to main content

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-chat uses DEEPSEEK_API_KEY, no per-session OPENAI_BASE_URL juggling. /model opens an interactive picker (with your full OpenRouter catalog when OPENROUTER_API_KEY is 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.
  • @path and !cmd in your prompt: @file inlines a file's contents, !cmd splices 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/. -c continues the most recent for the current directory; /tree, /fork, /clone branch 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 :subcommands included.
  • TOML config in ~/.letscode/config.toml (user) + .letscode/config.toml (project, overrides). Run letscode --init to 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 every letscode on the machine can see, letscode plugins shows what's installed and where, and letscode 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. See docs/plugins/.
  • RPC mode (letscode --mode rpc) — NDJSON over stdio, for IDE / host-app integration.
  • Rich terminal — Markdown-rendered assistant text, syntax-highlighted read panels, 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-prdskill_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):

  1. Built-in defaults
  2. User config (~/.letscode/config.toml)
  3. Project config (./.letscode/config.toml)
  4. Environment variables (LETSCODE_MODEL, OPENAI_BASE_URL, OPENAI_API_KEY)
  5. 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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

letscode-0.7.1.tar.gz (283.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

letscode-0.7.1-py3-none-any.whl (148.2 kB view details)

Uploaded Python 3

File details

Details for the file letscode-0.7.1.tar.gz.

File metadata

  • Download URL: letscode-0.7.1.tar.gz
  • Upload date:
  • Size: 283.7 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for letscode-0.7.1.tar.gz
Algorithm Hash digest
SHA256 43234df86158061d3e98765b9ecdd8cc656cea7f6b1c34d9c1e6afb1af2261b0
MD5 6fc3c950200ac7885a9d91ab1af95a24
BLAKE2b-256 5c08efed9ced61ee62ffe463281eede09242625f58371e741c2c0cce56f4f25f

See more details on using hashes here.

File details

Details for the file letscode-0.7.1-py3-none-any.whl.

File metadata

  • Download URL: letscode-0.7.1-py3-none-any.whl
  • Upload date:
  • Size: 148.2 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for letscode-0.7.1-py3-none-any.whl
Algorithm Hash digest
SHA256 58dad807bf7605be7c6f1dc1b4c0c00fd301d10bac2ee18d6c435935292ac0e5
MD5 05c0456c680768f1999ef80bf647c57f
BLAKE2b-256 eaaa65169189f5571ca20644c78ab2e24f629c085c5a0f56aaa831cc0c4987f6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page