Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 125 additions & 32 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
A single-binary terminal coding agent. It operates directly in your working
directory with four tools — **bash**, **read**, **write**, **edit** — built on
the [Pi harness](https://www.npmjs.com/package/@earendil-works/pi-agent-core)
and talking to Claude. Give it a task as one prompt or drop into an interactive
REPL; there's no daemon, no config file, and nothing to install but the binary.
and talking to models from Anthropic, OpenAI, Google, xAI, and other providers.
Give it a task as one prompt or drop into an interactive REPL; there's no daemon,
and nothing to install but the binary.

## Install

Expand Down Expand Up @@ -59,7 +60,7 @@ mv "thunderbolt-cli-$TARGET" ~/.local/bin/thunderbolt

> **What the checksum covers.** `SHA256SUMS` and the binary come from the same
> release over the same TLS connection, so the checksum catches a corrupted or
> truncated download but *not* a compromised release host — whoever could swap the
> truncated download but _not_ a compromised release host — whoever could swap the
> binary could swap its digest too. The binaries are unsigned and the quarantine
> strip bypasses macOS Gatekeeper. Signature verification (minisign) over the
> manifest against a pinned key is the planned follow-up hardening.
Expand All @@ -69,6 +70,40 @@ mv "thunderbolt-cli-$TARGET" ~/.local/bin/thunderbolt
> Windows also has no prebuilt CLI binary. Build from source above on either
> unsupported platform.

## First run

Run `thunderbolt` in a terminal. When no usable API key exists, guided setup
asks for provider, API key, and model, saves defaults, then continues directly
into requested REPL or one-shot task. API key input is not echoed.

Run setup again anytime:

```sh
thunderbolt config
```

Config lives at `~/.thunderbolt/config.json`, or
`$THUNDERBOLT_HOME/config.json` when `THUNDERBOLT_HOME` is set. File mode is
`0600` because config may contain a plaintext API key.

```json
{
"provider": "openai-compat",
"model": "upstream-model",
"apiKey": "sk-...",
"baseUrl": "https://host.example/v1"
}
```

`apiKey` and `baseUrl` are optional. Saved keys apply only when saved provider
and base URL match effective provider and base URL, preventing cross-provider or
cross-endpoint credential forwarding. Missing, malformed, or invalid config is
treated as absent.

Resolution order is explicit flag, supported provider environment variable,
config file, then built-in default. Current environment tier contains credential
variables only; provider, model, and base URL have no environment override.

## Usage

Run a single task and exit:
Expand All @@ -85,14 +120,15 @@ thunderbolt

### Subcommands

| Command | Purpose |
| ------- | ------- |
| `thunderbolt agent [options] [prompt]` | Run coding agent; `agent` is optional/default. |
| `thunderbolt acp serve [options]` | Expose built-in coding agent as stdio ACP server. |
| `thunderbolt acp --transport <wss\|iroh> [--port N] -- <agent-cmd...>` | Bridge stdio ACP agent. |
| `thunderbolt mcp --transport <wss\|iroh> [--port N] -- <server-cmd...>` | Bridge stdio MCP server. |
| `thunderbolt <acp\|mcp> connect <ticket\|nodeid> [-- <local-client-cmd...>]` | Dial iroh bridge. |
| `thunderbolt iroh id` / `pair` / `allow <nodeid>` | Inspect ACP identity, print pairing ticket, or authorize peer. |
| Command | Purpose |
| ---------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `thunderbolt agent [options] [prompt]` | Run coding agent; `agent` is optional/default. |
| `thunderbolt config` | Run guided provider setup and overwrite saved defaults. |
| `thunderbolt acp serve [options]` | Expose built-in coding agent as stdio ACP server. |
| `thunderbolt acp --transport <wss\|iroh> [--port N] -- <agent-cmd...>` | Bridge stdio ACP agent. |
| `thunderbolt mcp --transport <wss\|iroh> [--port N] -- <server-cmd...>` | Bridge stdio MCP server. |
| `thunderbolt <acp\|mcp> connect <ticket\|nodeid> [-- <local-client-cmd...>]` | Dial iroh bridge. |
| `thunderbolt iroh id` / `pair` / `allow <nodeid>` | Inspect ACP identity, print pairing ticket, or authorize peer. |

### Served agent workspace

Expand All @@ -119,36 +155,89 @@ elsewhere on the machine are outside its workspace and unavailable.

### Agent and `acp serve` flags

| Flag | Description |
| ---- | ----------- |
| `-m`, `--model <id>` | Provider model id (default: `claude-opus-4-8`). |
| `--provider <provider>` | `anthropic` or `openai-compat` (default: `anthropic`). |
| `--base-url <url>` | Required OpenAI-compatible endpoint URL. |
| `--api-key <key>` | OpenAI-compatible bearer key; overrides `THUNDERBOLT_OPENAI_COMPAT_KEY`. |
| Flag | Description |
| -------------------- | -------------------------------------------------------------------------- |
| `-m`, `--model <id>` | Provider model id (provider-specific default). |
| `--provider <name>` | Built-in provider or `openai-compat` (default: `anthropic`). |
| `--base-url <url>` | Custom endpoint URL (required for `openai-compat`). |
| `--api-key <key>` | Explicit key for any provider; overrides provider environment. |
| `--thinking <level>` | `off`, `minimal`, `low`, `medium`, `high`, or `xhigh` (default: `medium`). |
| `-y`, `--yolo` | Auto-approve tool calls (alias: `--dangerously-skip-permissions`). |
| `--no-tui` | Force plain readline REPL. |
| `-h`, `--help` | Show help and exit. |
| `-v`, `--version` | Print version and exit. |
| `-y`, `--yolo` | Auto-approve tool calls (alias: `--dangerously-skip-permissions`). |
| `--no-tui` | Force plain readline REPL. |
| `-h`, `--help` | Show help and exit. |
| `-v`, `--version` | Print version and exit. |

ACP/MCP bridge commands accept `--transport wss|iroh` (default `wss`) and
`--port <0-65535>` for WSS (defaults: ACP `8839`, MCP `8840`). Arguments after
`--` form spawned stdio command.

Anthropic provider requires **`ANTHROPIC_API_KEY`**. OpenAI-compatible provider
requires `--base-url` plus `--api-key` or `THUNDERBOLT_OPENAI_COMPAT_KEY`.
Supported built-in providers:

`anthropic`, `openai`, `google`, `xai`, `deepseek`, `zai`, `mistral`, `groq`,
`openrouter`, `moonshotai`, `minimax`, `cerebras`, `together`, `fireworks`.

Each built-in provider uses Pi's generated model catalog and standard API-key
environment variable. `--api-key` overrides that environment key; matching
saved config supplies fallback credentials. Unknown model errors list valid
catalog ids for selected provider.

`openai-compat` remains custom-endpoint escape hatch:

```sh
THUNDERBOLT_OPENAI_COMPAT_KEY=sk-... thunderbolt \
--provider openai-compat \
--base-url http://localhost:11434/v1 \
--model llama3.3 \
"review this repository"
```

For security, `openai-compat` never reads `OPENAI_API_KEY` or another generic
provider key. Use `--api-key` or `THUNDERBOLT_OPENAI_COMPAT_KEY` explicitly so a
credential cannot be forwarded automatically to an arbitrary custom URL.

### Provider defaults

| Provider | Default model |
| ------------ | ------------------------------------------ |
| `anthropic` | `claude-opus-4-8` |
| `openai` | `gpt-5.6-sol` |
| `google` | `gemini-3.1-pro-preview` |
| `xai` | `grok-build-0.1` |
| `deepseek` | `deepseek-v4-pro` |
| `zai` | `glm-5.2` |
| `mistral` | `devstral-medium-latest` |
| `groq` | `openai/gpt-oss-120b` |
| `openrouter` | `anthropic/claude-opus-4.8` |
| `moonshotai` | `kimi-k2.7-code` |
| `minimax` | `MiniMax-M3` |
| `cerebras` | `gpt-oss-120b` |
| `together` | `moonshotai/Kimi-K2.7-Code` |
| `fireworks` | `accounts/fireworks/models/kimi-k2p7-code` |

### Environment

| Variable | Description |
| -------- | ----------- |
| `ANTHROPIC_API_KEY` | Anthropic API key for default provider. |
| `THUNDERBOLT_OPENAI_COMPAT_KEY` | Bearer key for OpenAI-compatible provider. |
| `THUNDERBOLT_HOME` | CLI state root (default `~/.thunderbolt`): iroh identity/allowlist and ACP sessions. |
| `THUNDERBOLT_IROH_RELAY_URL` | Self-hosted iroh-relay WSS URL; unset uses n0 public relays. |
| `THUNDERBOLT_APP_ORIGIN` | Extra comma-separated allowed browser origins for WSS bridges. |
| `THUNDERBOLT_NO_TUI` | Force plain readline REPL when set. |
| `NO_COLOR` | Disable terminal color when set. |
| Variable | Description |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `ANTHROPIC_OAUTH_TOKEN`, `ANTHROPIC_API_KEY` | Anthropic credentials, checked in that order. |
| `OPENAI_API_KEY` | OpenAI API key. |
| `GEMINI_API_KEY` | Google Gemini API key. |
| `XAI_API_KEY` | xAI API key. |
| `DEEPSEEK_API_KEY` | DeepSeek API key. |
| `ZAI_API_KEY` | Z.AI API key. |
| `MISTRAL_API_KEY` | Mistral API key. |
| `GROQ_API_KEY` | Groq API key. |
| `OPENROUTER_API_KEY` | OpenRouter API key. |
| `MOONSHOT_API_KEY` | Moonshot AI API key. |
| `MINIMAX_API_KEY` | MiniMax API key. |
| `CEREBRAS_API_KEY` | Cerebras API key. |
| `TOGETHER_API_KEY` | Together API key. |
| `FIREWORKS_API_KEY` | Fireworks API key. |
| `THUNDERBOLT_OPENAI_COMPAT_KEY` | Dedicated fallback key for arbitrary `openai-compat` URLs. |
| `THUNDERBOLT_HOME` | CLI state root containing `config.json`, iroh identity/allowlist, and ACP sessions (default: `~/.thunderbolt`). |
| `THUNDERBOLT_IROH_RELAY_URL` | Self-hosted iroh-relay WSS URL; unset uses n0 public relays. |
| `THUNDERBOLT_APP_ORIGIN` | Extra comma-separated allowed browser origins for WSS bridges. |
| `THUNDERBOLT_NO_TUI` | Force plain readline REPL when set. |
| `NO_COLOR` | Disable terminal color when set. |

## Demo

Expand All @@ -158,4 +247,8 @@ export ANTHROPIC_API_KEY=sk-ant-...
thunderbolt "summarize what this repo does in three bullets"
thunderbolt --thinking high "find and fix the off-by-one bug in src/range.ts"
thunderbolt --yolo "run the test suite and fix whatever breaks"

# Or select another built-in provider; omitted --model uses provider default.
OPENAI_API_KEY=sk-... thunderbolt --provider openai "fix the failing tests"
thunderbolt --provider google --api-key AIza... "review this repository"
```
2 changes: 1 addition & 1 deletion cli/src/acp/session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { join } from 'node:path'
import { JsonlSessionRepo } from '@earendil-works/pi-agent-core'
import type { Session } from '@earendil-works/pi-agent-core'
import { NodeExecutionEnv } from '@earendil-works/pi-agent-core/node'
import { thunderboltHomeDir } from '../iroh/paths.ts'
import { thunderboltHomeDir } from '../paths.ts'

/** Creates and resumes disk-backed Pi sessions for ACP session ids. */
export type SessionStore = {
Expand Down
49 changes: 49 additions & 0 deletions cli/src/agent/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/** Provider defaults and credential environment metadata shared by CLI setup. */

import type { BuiltinProvider } from './types.ts'

/** Default model backend when provider is omitted. */
export const DEFAULT_PROVIDER: BuiltinProvider = 'anthropic'

/** Default Anthropic model. */
export const DEFAULT_MODEL = 'claude-opus-4-8'

/** Default catalog model for each built-in provider. */
export const DEFAULT_MODELS: Readonly<Record<BuiltinProvider, string>> = {
anthropic: DEFAULT_MODEL,
openai: 'gpt-5.6-sol',
google: 'gemini-3.1-pro-preview',
xai: 'grok-build-0.1',
deepseek: 'deepseek-v4-pro',
zai: 'glm-5.2',
mistral: 'devstral-medium-latest',
groq: 'openai/gpt-oss-120b',
openrouter: 'anthropic/claude-opus-4.8',
moonshotai: 'kimi-k2.7-code',
minimax: 'MiniMax-M3',
cerebras: 'gpt-oss-120b',
together: 'moonshotai/Kimi-K2.7-Code',
fireworks: 'accounts/fireworks/models/kimi-k2p7-code',
}

/** Environment variables Pi checks for each exposed built-in provider. */
export const BUILTIN_PROVIDER_ENV_VARS: Readonly<Record<BuiltinProvider, readonly string[]>> = {
anthropic: ['ANTHROPIC_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'],
openai: ['OPENAI_API_KEY'],
google: ['GEMINI_API_KEY'],
xai: ['XAI_API_KEY'],
deepseek: ['DEEPSEEK_API_KEY'],
zai: ['ZAI_API_KEY'],
mistral: ['MISTRAL_API_KEY'],
groq: ['GROQ_API_KEY'],
openrouter: ['OPENROUTER_API_KEY'],
moonshotai: ['MOONSHOT_API_KEY'],
minimax: ['MINIMAX_API_KEY'],
cerebras: ['CEREBRAS_API_KEY'],
together: ['TOGETHER_API_KEY'],
fireworks: ['FIREWORKS_API_KEY'],
}
10 changes: 5 additions & 5 deletions cli/src/agent/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
* Assembles the Pi `AgentHarness` — the spine that lets the CLI actually talk
* to Claude. It binds a Node execution environment (real bash + filesystem) to
* the working directory, opens an in-memory session, resolves the model, and
* registers coding tools. Workspace-root harnesses omit bash because arbitrary
* shell commands cannot be confined to that workspace.
* Assembles the Pi `AgentHarness` — the spine that lets the CLI talk to the
* selected model provider. It binds a Node execution environment (real bash +
* filesystem) to the working directory, opens an in-memory session, resolves
* the model, and registers coding tools. Workspace-root harnesses omit bash
* because arbitrary shell commands cannot be confined to that workspace.
*/

import { AgentHarness, InMemorySessionRepo } from '@earendil-works/pi-agent-core'
Expand Down
Loading
Loading