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.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/. -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, /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). 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. 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. 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-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 = "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):

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

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.6.3.tar.gz (275.0 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.6.3-py3-none-any.whl (137.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: letscode-0.6.3.tar.gz
  • Upload date:
  • Size: 275.0 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

Hashes for letscode-0.6.3.tar.gz
Algorithm Hash digest
SHA256 014ff5f086cdbdb63d0db49d3a6840910c38c324c4916b1c651c5dbec2b022da
MD5 64eaddc59df205e0c3cbb257de5c2e5c
BLAKE2b-256 67e789b7534894019a482cec5cfde3f6944217b6121938a0430409f6cc09c6c8

See more details on using hashes here.

File details

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

File metadata

  • Download URL: letscode-0.6.3-py3-none-any.whl
  • Upload date:
  • Size: 137.2 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

Hashes for letscode-0.6.3-py3-none-any.whl
Algorithm Hash digest
SHA256 1cb3c69092b6c77e31baf3a2e09b72b6fe575951a3c52cd8f9b1d7de471175b4
MD5 14c6473f82bfa90e7faf5fe487e56c21
BLAKE2b-256 c9d21a661c31a6da24757b8f231706c17a550e5cac9c7f0163018f672fdee273

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