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

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 Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

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

letscode-0.5.0-py3-none-any.whl (103.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: letscode-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 103.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","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.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 499bf606953bd91addb15912be1e47a1502ec5fc4aa690b48aa73da1e0620beb
MD5 f3585f8f3d09286028652416826f55a8
BLAKE2b-256 958000f0f57aee6baed8464f69b0f26b395b67e0bb7eba0464054171915efd40

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