Skip to main content

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

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

letscode-0.6.1.tar.gz (247.4 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.1-py3-none-any.whl (103.3 kB view details)

Uploaded Python 3

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

Hashes for letscode-0.6.1.tar.gz
Algorithm Hash digest
SHA256 30bc4fe09df08b9d1f4194d46a3ccad5a3eb998fa4c2cb87c034c96c386b8d91
MD5 04212cbefc05c7319e5bd744189177d6
BLAKE2b-256 6d1200de33b7b790db9f2d062cfa684fcf96ad6d96739dc8f5f4314d5cdc6ac8

See more details on using hashes here.

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

Hashes for letscode-0.6.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3db2612e3eb063daa449eae974cf73930d88608da11d0a5a2a9f94c8b51c950d
MD5 97a90cdfa73eb05aa8997f22096d8243
BLAKE2b-256 a33c7a682fba1304aa089f7b7171272351ff86fc5be6f33bb36a065a41f31ada

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 Pingdom Monitoring Sentry Error logging StatusPage Status page