diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 76c5b31..d1bad4c 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -89,6 +89,7 @@ jobs:
needs: [setup]
if: ${{ !inputs.reuse_engine_version }}
strategy:
+ fail-fast: false
matrix:
include:
- platform: linux
@@ -96,6 +97,11 @@ jobs:
target: x86_64-unknown-linux-musl
binary_ext: ""
arch: x86_64
+ - platform: linux
+ runner: depot-ubuntu-24.04-arm-8
+ target: aarch64-unknown-linux-musl
+ binary_ext: ""
+ arch: aarch64
- platform: windows
runner: depot-ubuntu-24.04-8
target: x86_64-pc-windows-gnu
@@ -161,6 +167,7 @@ jobs:
needs: [setup]
if: ${{ !inputs.reuse_engine_version }}
strategy:
+ fail-fast: false
matrix:
include:
- platform: linux/arm64
diff --git a/CLAUDE.md b/CLAUDE.md
index 6c5ca0a..9f6a874 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -64,10 +64,22 @@ Universal schema guidance:
- `sandbox-agent api sessions reject-question` ↔ `POST /v1/sessions/{sessionId}/questions/{questionId}/reject`
- `sandbox-agent api sessions reply-permission` ↔ `POST /v1/sessions/{sessionId}/permissions/{permissionId}/reply`
+## OpenCode CLI (Experimental)
+
+`sandbox-agent opencode` starts a sandbox-agent server and attaches an OpenCode session (uses `/opencode`).
+
## Post-Release Testing
After cutting a release, verify the release works correctly. Run `/project:post-release-testing` to execute the testing agent.
+## OpenCode Compatibility Tests
+
+The OpenCode compatibility suite lives at `server/packages/sandbox-agent/tests/opencode-compat` and validates the `@opencode-ai/sdk` against the `/opencode` API. Run it with:
+
+```bash
+SANDBOX_AGENT_SKIP_INSPECTOR=1 pnpm --filter @sandbox-agent/opencode-compat-tests test
+```
+
## Git Commits
- Do not include any co-authors in commit messages (no `Co-Authored-By` lines)
diff --git a/Cargo.toml b/Cargo.toml
index f7b9861..e192959 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,7 +3,7 @@ resolver = "2"
members = ["server/packages/*"]
[workspace.package]
-version = "0.1.6-rc.1"
+version = "0.1.6"
edition = "2021"
authors = [ "Rivet Gaming, LLC " ]
license = "Apache-2.0"
@@ -12,12 +12,12 @@ description = "Universal API for automatic coding agents in sandboxes. Supports
[workspace.dependencies]
# Internal crates
-sandbox-agent = { version = "0.1.6-rc.1", path = "server/packages/sandbox-agent" }
-sandbox-agent-error = { version = "0.1.6-rc.1", path = "server/packages/error" }
-sandbox-agent-agent-management = { version = "0.1.6-rc.1", path = "server/packages/agent-management" }
-sandbox-agent-agent-credentials = { version = "0.1.6-rc.1", path = "server/packages/agent-credentials" }
-sandbox-agent-universal-agent-schema = { version = "0.1.6-rc.1", path = "server/packages/universal-agent-schema" }
-sandbox-agent-extracted-agent-schemas = { version = "0.1.6-rc.1", path = "server/packages/extracted-agent-schemas" }
+sandbox-agent = { version = "0.1.6", path = "server/packages/sandbox-agent" }
+sandbox-agent-error = { version = "0.1.6", path = "server/packages/error" }
+sandbox-agent-agent-management = { version = "0.1.6", path = "server/packages/agent-management" }
+sandbox-agent-agent-credentials = { version = "0.1.6", path = "server/packages/agent-credentials" }
+sandbox-agent-universal-agent-schema = { version = "0.1.6", path = "server/packages/universal-agent-schema" }
+sandbox-agent-extracted-agent-schemas = { version = "0.1.6", path = "server/packages/extracted-agent-schemas" }
# Serialization
serde = { version = "1.0", features = ["derive"] }
@@ -68,6 +68,7 @@ zip = { version = "0.6", default-features = false, features = ["deflate"] }
url = "2.5"
regress = "0.10"
include_dir = "0.7"
+base64 = "0.22"
# Code generation (build deps)
typify = "0.4"
diff --git a/README.md b/README.md
index ffc0728..a1af778 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ Sandbox Agent solves three problems:
- **Runs Inside Any Sandbox**: Lightweight static Rust binary. One curl command to install inside E2B, Daytona, Vercel Sandboxes, or Docker
- **Server or SDK Mode**: Run as an HTTP server or embed with the TypeScript SDK
- **OpenAPI Spec**: [Well documented](https://sandboxagent.dev/docs/api-reference) and easy to integrate from any language
+- **OpenCode SDK & UI Support** *(Experimental)*: [Connect OpenCode CLI, SDK, or web UI](https://sandboxagent.dev/docs/opencode-compatibility) to control agents through familiar OpenCode tooling
## Architecture
diff --git a/docker/release/build.sh b/docker/release/build.sh
index a9e42e4..6e7d66f 100755
--- a/docker/release/build.sh
+++ b/docker/release/build.sh
@@ -18,6 +18,12 @@ case $TARGET in
TARGET_STAGE="x86_64-builder"
BINARY="sandbox-agent-$TARGET"
;;
+ aarch64-unknown-linux-musl)
+ echo "Building for Linux aarch64 musl"
+ DOCKERFILE="linux-aarch64.Dockerfile"
+ TARGET_STAGE="aarch64-builder"
+ BINARY="sandbox-agent-$TARGET"
+ ;;
x86_64-pc-windows-gnu)
echo "Building for Windows x86_64"
DOCKERFILE="windows.Dockerfile"
diff --git a/docker/release/linux-aarch64.Dockerfile b/docker/release/linux-aarch64.Dockerfile
new file mode 100644
index 0000000..e1f3acd
--- /dev/null
+++ b/docker/release/linux-aarch64.Dockerfile
@@ -0,0 +1,74 @@
+# syntax=docker/dockerfile:1.10.0
+
+# Build inspector frontend
+FROM node:22-alpine AS inspector-build
+WORKDIR /app
+RUN npm install -g pnpm
+
+# Copy package files for workspaces
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
+COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
+COPY sdks/cli-shared/package.json ./sdks/cli-shared/
+COPY sdks/typescript/package.json ./sdks/typescript/
+
+# Install dependencies
+RUN pnpm install --filter @sandbox-agent/inspector...
+
+# Copy SDK source (with pre-generated types from docs/openapi.json)
+COPY docs/openapi.json ./docs/
+COPY sdks/cli-shared ./sdks/cli-shared
+COPY sdks/typescript ./sdks/typescript
+
+# Build cli-shared and SDK (just tsup, skip generate since types are pre-generated)
+RUN cd sdks/cli-shared && pnpm exec tsup
+RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
+
+# Copy inspector source and build
+COPY frontend/packages/inspector ./frontend/packages/inspector
+RUN cd frontend/packages/inspector && pnpm exec vite build
+
+# Use Alpine with native musl for ARM64 builds (runs natively on ARM64 runner)
+FROM rust:1.88-alpine AS aarch64-builder
+
+# Accept version as build arg
+ARG SANDBOX_AGENT_VERSION
+ENV SANDBOX_AGENT_VERSION=${SANDBOX_AGENT_VERSION}
+
+# Install dependencies
+RUN apk add --no-cache \
+ musl-dev \
+ clang \
+ llvm-dev \
+ openssl-dev \
+ openssl-libs-static \
+ pkgconfig \
+ git \
+ curl \
+ build-base
+
+# Add musl target
+RUN rustup target add aarch64-unknown-linux-musl
+
+# Set environment variables for native musl build
+ENV CARGO_INCREMENTAL=0 \
+ CARGO_NET_GIT_FETCH_WITH_CLI=true \
+ RUSTFLAGS="-C target-feature=+crt-static"
+
+WORKDIR /build
+
+# Copy the source code
+COPY . .
+
+# Copy pre-built inspector frontend
+COPY --from=inspector-build /app/frontend/packages/inspector/dist ./frontend/packages/inspector/dist
+
+# Build for Linux with musl (static binary) - aarch64
+RUN --mount=type=cache,target=/usr/local/cargo/registry \
+ --mount=type=cache,target=/usr/local/cargo/git \
+ --mount=type=cache,target=/build/target \
+ cargo build -p sandbox-agent --release --target aarch64-unknown-linux-musl && \
+ mkdir -p /artifacts && \
+ cp target/aarch64-unknown-linux-musl/release/sandbox-agent /artifacts/sandbox-agent-aarch64-unknown-linux-musl
+
+# Default command to show help
+CMD ["ls", "-la", "/artifacts"]
diff --git a/docs/cli.mdx b/docs/cli.mdx
index 4a6e118..b761234 100644
--- a/docs/cli.mdx
+++ b/docs/cli.mdx
@@ -29,6 +29,12 @@ sandbox-agent server [OPTIONS]
sandbox-agent server --token "$TOKEN" --port 3000
```
+Server logs are redirected to a daily log file under the sandbox-agent data directory (for example, `~/.local/share/sandbox-agent/logs`). Override with `SANDBOX_AGENT_LOG_DIR`, or set `SANDBOX_AGENT_LOG_STDOUT=1` to keep logs on stdout/stderr.
+
+HTTP request logging is enabled by default. Control it with:
+- `SANDBOX_AGENT_LOG_HTTP=0` to disable request logs
+- `SANDBOX_AGENT_LOG_HTTP_HEADERS=1` to include request headers (Authorization is redacted)
+
---
## Install Agent (Local)
@@ -49,6 +55,31 @@ sandbox-agent install-agent claude --reinstall
---
+## OpenCode (Experimental)
+
+Start a sandbox-agent server and attach an OpenCode session (uses `opencode attach`):
+
+```bash
+sandbox-agent opencode [OPTIONS]
+```
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `-t, --token ` | - | Authentication token for all requests |
+| `-n, --no-token` | - | Disable authentication (local dev only) |
+| `-H, --host ` | `127.0.0.1` | Host to bind to |
+| `-p, --port ` | `2468` | Port to bind to |
+| `--session-title ` | - | Title for the OpenCode session |
+| `--opencode-bin ` | - | Override `opencode` binary path |
+
+```bash
+sandbox-agent opencode --token "$TOKEN"
+```
+
+Requires the `opencode` binary to be installed (or set `OPENCODE_BIN` / `--opencode-bin`).
+
+---
+
## Credentials
### Extract
diff --git a/docs/docs.json b/docs/docs.json
index 43eb67b..45a3fa5 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -41,7 +41,7 @@
"pages": [
{
"group": "Getting started",
- "pages": ["quickstart", "building-chat-ui", "manage-sessions"]
+ "pages": ["quickstart", "building-chat-ui", "manage-sessions", "opencode-compatibility"]
},
{
"group": "Deploy",
@@ -61,11 +61,11 @@
},
{
"group": "Reference",
- "pages": [
- "cli",
- "inspector",
- "session-transcript-schema",
- "cors",
+ "pages": [
+ "cli",
+ "inspector",
+ "session-transcript-schema",
+ "cors",
{
"group": "AI",
"pages": ["ai/skill", "ai/llms-txt"]
diff --git a/docs/opencode-compatibility.mdx b/docs/opencode-compatibility.mdx
new file mode 100644
index 0000000..be7da6f
--- /dev/null
+++ b/docs/opencode-compatibility.mdx
@@ -0,0 +1,142 @@
+---
+title: "OpenCode SDK & UI Support"
+description: "Connect OpenCode clients, SDKs, and web UI to Sandbox Agent."
+icon: "rectangle-terminal"
+---
+
+
+ **Experimental**: OpenCode SDK & UI support is experimental and may change without notice.
+
+
+Sandbox Agent exposes an OpenCode-compatible API, allowing you to connect any OpenCode client, SDK, or web UI to control coding agents running inside sandboxes.
+
+## Why Use OpenCode Clients with Sandbox Agent?
+
+OpenCode provides a rich ecosystem of clients:
+
+- **OpenCode CLI** (`opencode attach`): Terminal-based interface
+- **OpenCode Web UI**: Browser-based chat interface
+- **OpenCode SDK** (`@opencode-ai/sdk`): Rich TypeScript SDK
+
+## Quick Start
+
+### Using OpenCode CLI & TUI
+
+Sandbox Agent provides an all-in-one command to setup Sandbox Agent and connect an OpenCode session, great for local development:
+
+```bash
+sandbox-agent opencode --port 2468 --no-token
+```
+
+Or, start the server and attach separately:
+
+```bash
+# Start sandbox-agent
+sandbox-agent server --no-token --host 127.0.0.1 --port 2468
+
+# Attach OpenCode CLI
+opencode attach http://localhost:2468/opencode
+```
+
+With authentication enabled:
+
+```bash
+# Start with token
+sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
+
+# Attach with password
+opencode attach http://localhost:2468/opencode --password "$SANDBOX_TOKEN"
+```
+
+### Using the OpenCode Web UI
+
+The OpenCode web UI can connect to Sandbox Agent for a full browser-based experience.
+
+
+
+ ```bash
+ sandbox-agent server --no-token --host 127.0.0.1 --port 2468 --cors-allow-origin http://127.0.0.1:5173
+ ```
+
+
+ ```bash
+ git clone https://github.com/opencode-ai/opencode
+ cd opencode/packages/app
+ export VITE_OPENCODE_SERVER_HOST=127.0.0.1
+ export VITE_OPENCODE_SERVER_PORT=2468
+ bun run dev -- --host 127.0.0.1 --port 5173
+ ```
+
+
+ Navigate to `http://127.0.0.1:5173/` in your browser.
+
+
+
+
+ If you see `Error: Could not connect to server`, check that:
+ - The sandbox-agent server is running
+ - `--cors-allow-origin` matches the **exact** browser origin (`localhost` and `127.0.0.1` are different origins)
+
+
+### Using OpenCode SDK
+
+```typescript
+import { createOpencodeClient } from "@opencode-ai/sdk";
+
+const client = createOpencodeClient({
+ baseUrl: "http://localhost:2468/opencode",
+ headers: { Authorization: "Bearer YOUR_TOKEN" }, // if using auth
+});
+
+// Create a session
+const session = await client.session.create();
+
+// Send a prompt
+await client.session.promptAsync({
+ path: { id: session.data.id },
+ body: {
+ parts: [{ type: "text", text: "Hello, write a hello world script" }],
+ },
+});
+
+// Subscribe to events
+const events = await client.event.subscribe({});
+for await (const event of events.stream) {
+ console.log(event);
+}
+```
+
+## Notes
+
+- **API Routing**: The OpenCode API is available at the `/opencode` base path
+- **Authentication**: If sandbox-agent is started with `--token`, include `Authorization: Bearer ` header or use `--password` flag with CLI
+- **CORS**: When using the web UI from a different origin, configure `--cors-allow-origin`
+- **Provider Selection**: Use the provider/model selector in the UI to choose which backing agent to use (claude, codex, opencode, amp)
+
+## Endpoint Coverage
+
+See the full endpoint compatibility table below. Most endpoints are functional for session management, messaging, and event streaming. Some endpoints return stub responses for features not yet implemented.
+
+
+
+| Endpoint | Status | Notes |
+|---|---|---|
+| `GET /event` | ✓ | Emits events for session/message updates (SSE) |
+| `GET /global/event` | ✓ | Wraps events in GlobalEvent format (SSE) |
+| `GET /session` | ✓ | In-memory session store |
+| `POST /session` | ✓ | Create new sessions |
+| `GET /session/{id}` | ✓ | Get session details |
+| `POST /session/{id}/message` | ✓ | Send messages to session |
+| `GET /session/{id}/message` | ✓ | Get session messages |
+| `GET /permission` | ✓ | List pending permissions |
+| `POST /permission/{id}/reply` | ✓ | Respond to permission requests |
+| `GET /question` | ✓ | List pending questions |
+| `POST /question/{id}/reply` | ✓ | Answer agent questions |
+| `GET /provider` | − | Returns provider metadata |
+| `GET /agent` | − | Returns agent list |
+| `GET /config` | − | Returns config |
+| *other endpoints* | − | Return empty/stub responses |
+
+✓ Functional − Stubbed
+
+
diff --git a/frontend/packages/website/src/components/FeatureGrid.tsx b/frontend/packages/website/src/components/FeatureGrid.tsx
index f2ea1ff..47c2d4a 100644
--- a/frontend/packages/website/src/components/FeatureGrid.tsx
+++ b/frontend/packages/website/src/components/FeatureGrid.tsx
@@ -1,6 +1,6 @@
'use client';
-import { Workflow, Server, Database, Download, Globe } from 'lucide-react';
+import { Workflow, Server, Database, Download, Globe, Plug } from 'lucide-react';
import { FeatureIcon } from './ui/FeatureIcon';
export function FeatureGrid() {
@@ -91,8 +91,32 @@ export function FeatureGrid() {
- {/* Managing Sessions */}
-
+ {/* Runs Inside Any Sandbox */}
+
+ {/* Top Shine Highlight */}
+
+ {/* Top Left Reflection/Glow */}
+
+ {/* Sharp Edge Highlight */}
+
+
+
+
+
Runs Inside Any Sandbox
+
+
+ Lightweight static binary. One curl command to install inside E2B, Daytona, Vercel Sandboxes, or Docker.
+
+
+
+ {/* Automatic Agent Installation */}
+
{/* Top Shine Highlight */}
{/* Top Left Reflection/Glow */}
@@ -115,27 +139,28 @@ export function FeatureGrid() {
- {/* Runs Inside Any Sandbox */}
-
+ {/* OpenCode SDK & UI Support */}
+
{/* Top Shine Highlight */}
{/* Top Left Reflection/Glow */}
-
+
{/* Sharp Edge Highlight */}
-
+
-
Runs Inside Any Sandbox
+ OpenCode SDK & UI Support
+ Experimental
- Lightweight static binary. One curl command to install inside E2B, Daytona, Vercel Sandboxes, or Docker.
+ Connect OpenCode CLI, SDK, or web UI to control agents through familiar OpenCode tooling.
diff --git a/justfile b/justfile
index 2b3dd6b..f9d4103 100644
--- a/justfile
+++ b/justfile
@@ -17,6 +17,7 @@ release-build target="x86_64-unknown-linux-musl":
[group('release')]
release-build-all:
./docker/release/build.sh x86_64-unknown-linux-musl
+ ./docker/release/build.sh aarch64-unknown-linux-musl
./docker/release/build.sh x86_64-pc-windows-gnu
./docker/release/build.sh x86_64-apple-darwin
./docker/release/build.sh aarch64-apple-darwin
diff --git a/package.json b/package.json
index a8566b6..f86cc47 100644
--- a/package.json
+++ b/package.json
@@ -12,5 +12,11 @@
"devDependencies": {
"turbo": "^2.4.0",
"vitest": "^3.0.0"
+ },
+ "pnpm": {
+ "overrides": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0"
+ }
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2a27026..5bfc5da 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,6 +4,10 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+overrides:
+ '@types/react': ^18.3.3
+ '@types/react-dom': ^18.3.0
+
importers:
.:
@@ -37,11 +41,11 @@ importers:
specifier: latest
version: 25.2.0
'@types/react':
- specifier: ^19.1.0
- version: 19.2.10
+ specifier: ^18.3.3
+ version: 18.3.27
'@types/react-dom':
- specifier: ^19.1.0
- version: 19.2.3(@types/react@19.2.10)
+ specifier: ^18.3.0
+ version: 18.3.7(@types/react@18.3.27)
'@vitejs/plugin-react':
specifier: ^4.5.0
version: 4.7.0(vite@6.4.1(@types/node@25.2.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2))
@@ -200,7 +204,7 @@ importers:
dependencies:
'@astrojs/react':
specifier: ^4.2.0
- version: 4.4.2(@types/node@25.2.0)(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(jiti@1.21.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tsx@4.21.0)(yaml@2.8.2)
+ version: 4.4.2(@types/node@25.2.0)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(jiti@1.21.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tsx@4.21.0)(yaml@2.8.2)
'@astrojs/tailwind':
specifier: ^6.0.0
version: 6.0.2(astro@5.16.15(@types/node@25.2.0)(jiti@1.21.7)(rollup@4.56.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))
@@ -224,11 +228,11 @@ importers:
version: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
devDependencies:
'@types/react':
- specifier: ^19.0.0
- version: 19.2.10
+ specifier: ^18.3.3
+ version: 18.3.27
'@types/react-dom':
- specifier: ^19.0.0
- version: 19.2.3(@types/react@19.2.10)
+ specifier: ^18.3.0
+ version: 18.3.7(@types/react@18.3.27)
typescript:
specifier: ^5.7.0
version: 5.9.3
@@ -342,6 +346,9 @@ importers:
'@sandbox-agent/cli-darwin-x64':
specifier: workspace:*
version: link:platforms/darwin-x64
+ '@sandbox-agent/cli-linux-arm64':
+ specifier: workspace:*
+ version: link:platforms/linux-arm64
'@sandbox-agent/cli-linux-x64':
specifier: workspace:*
version: link:platforms/linux-x64
@@ -369,6 +376,8 @@ importers:
sdks/cli/platforms/darwin-x64: {}
+ sdks/cli/platforms/linux-arm64: {}
+
sdks/cli/platforms/linux-x64: {}
sdks/cli/platforms/win32-x64: {}
@@ -423,8 +432,8 @@ packages:
resolution: {integrity: sha512-1tl95bpGfuaDMDn8O3x/5Dxii1HPvzjvpL2YTuqOOrQehs60I2DKiDgh1jrKc7G8lv+LQT5H15V6QONQ+9waeQ==}
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}
peerDependencies:
- '@types/react': ^17.0.50 || ^18.0.21 || ^19.0.0
- '@types/react-dom': ^17.0.17 || ^18.0.6 || ^19.0.0
+ '@types/react': ^18.3.3
+ '@types/react-dom': ^18.3.0
react: ^17.0.2 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.2 || ^18.0.0 || ^19.0.0
@@ -2203,19 +2212,11 @@ packages:
'@types/react-dom@18.3.7':
resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
peerDependencies:
- '@types/react': ^18.0.0
-
- '@types/react-dom@19.2.3':
- resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
- peerDependencies:
- '@types/react': ^19.2.0
+ '@types/react': ^18.3.3
'@types/react@18.3.27':
resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==}
- '@types/react@19.2.10':
- resolution: {integrity: sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==}
-
'@types/semver@7.7.1':
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
@@ -4556,10 +4557,10 @@ snapshots:
dependencies:
prismjs: 1.30.0
- '@astrojs/react@4.4.2(@types/node@25.2.0)(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(jiti@1.21.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tsx@4.21.0)(yaml@2.8.2)':
+ '@astrojs/react@4.4.2(@types/node@25.2.0)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(jiti@1.21.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tsx@4.21.0)(yaml@2.8.2)':
dependencies:
- '@types/react': 19.2.10
- '@types/react-dom': 19.2.3(@types/react@19.2.10)
+ '@types/react': 18.3.27
+ '@types/react-dom': 18.3.7(@types/react@18.3.27)
'@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@25.2.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2))
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
@@ -6475,19 +6476,11 @@ snapshots:
dependencies:
'@types/react': 18.3.27
- '@types/react-dom@19.2.3(@types/react@19.2.10)':
- dependencies:
- '@types/react': 19.2.10
-
'@types/react@18.3.27':
dependencies:
'@types/prop-types': 15.7.15
csstype: 3.2.3
- '@types/react@19.2.10':
- dependencies:
- csstype: 3.2.3
-
'@types/semver@7.7.1': {}
'@types/ssh2@1.15.5':
diff --git a/research/agents/openclaw.md b/research/agents/openclaw.md
new file mode 100644
index 0000000..0a9e5ba
--- /dev/null
+++ b/research/agents/openclaw.md
@@ -0,0 +1,553 @@
+# OpenClaw (formerly Clawdbot) Research
+
+Research notes on OpenClaw's architecture, API, and automation patterns for integration with sandbox-agent.
+
+## Overview
+
+- **Provider**: Multi-provider (Anthropic, OpenAI, etc. via Pi agent)
+- **Execution Method**: WebSocket Gateway + HTTP APIs
+- **Session Persistence**: Session Key (string) + Session ID (UUID)
+- **SDK**: No official SDK - uses WebSocket/HTTP protocol directly
+- **Binary**: `clawdbot` (npm global install or local)
+- **Default Port**: 18789 (WebSocket + HTTP multiplex)
+
+## Architecture
+
+OpenClaw is architected differently from other coding agents (Claude Code, Codex, OpenCode, Amp):
+
+```
+┌─────────────────────────────────────┐
+│ Gateway Service │ ws://127.0.0.1:18789
+│ (long-running daemon) │ http://127.0.0.1:18789
+│ │
+│ ┌─────────────────────────────┐ │
+│ │ Pi Agent (embedded RPC) │ │
+│ │ - Tool execution │ │
+│ │ - Model routing │ │
+│ │ - Session management │ │
+│ └─────────────────────────────┘ │
+└─────────────────────────────────────┘
+ │
+ ├── WebSocket (full control plane)
+ ├── HTTP /v1/chat/completions (OpenAI-compatible)
+ ├── HTTP /v1/responses (OpenResponses-compatible)
+ ├── HTTP /tools/invoke (single tool invocation)
+ └── HTTP /hooks/agent (webhook triggers)
+```
+
+**Key Difference**: OpenClaw runs as a **daemon** that owns the agent runtime. Other agents (Claude, Codex, Amp) spawn a subprocess per turn. OpenClaw is more similar to OpenCode's server model but with a persistent gateway.
+
+## Automation Methods (Priority Order)
+
+### 1. WebSocket Gateway Protocol (Recommended)
+
+Full-featured bidirectional control with streaming events.
+
+#### Connection Handshake
+
+```typescript
+// Connect to Gateway
+const ws = new WebSocket("ws://127.0.0.1:18789");
+
+// First frame MUST be connect request
+ws.send(JSON.stringify({
+ type: "req",
+ id: "1",
+ method: "connect",
+ params: {
+ minProtocol: 3,
+ maxProtocol: 3,
+ client: {
+ id: "gateway-client", // or custom client id
+ version: "1.0.0",
+ platform: "linux",
+ mode: "backend"
+ },
+ role: "operator",
+ scopes: ["operator.admin"],
+ caps: [],
+ auth: { token: "YOUR_GATEWAY_TOKEN" }
+ }
+}));
+
+// Expect hello-ok response
+// { type: "res", id: "1", ok: true, payload: { type: "hello-ok", ... } }
+```
+
+#### Agent Request
+
+```typescript
+// Send agent turn request
+const runId = crypto.randomUUID();
+ws.send(JSON.stringify({
+ type: "req",
+ id: runId,
+ method: "agent",
+ params: {
+ message: "Your prompt here",
+ idempotencyKey: runId,
+ sessionKey: "agent:main:main", // or custom session key
+ thinking: "low", // optional: low|medium|high
+ deliver: false, // don't send to messaging channel
+ timeout: 300000 // 5 minute timeout
+ }
+}));
+```
+
+#### Response Flow (Two-Stage)
+
+```typescript
+// Stage 1: Immediate ack
+// { type: "res", id: "...", ok: true, payload: { runId, status: "accepted", acceptedAt: 1234567890 } }
+
+// Stage 2: Streaming events
+// { type: "event", event: "agent", payload: { runId, seq: 1, stream: "output", data: {...} } }
+// { type: "event", event: "agent", payload: { runId, seq: 2, stream: "tool", data: {...} } }
+// ...
+
+// Stage 3: Final response (same id as request)
+// { type: "res", id: "...", ok: true, payload: { runId, status: "ok", summary: "completed", result: {...} } }
+```
+
+### 2. OpenAI-Compatible HTTP API
+
+For simple integration with tools expecting OpenAI Chat Completions.
+
+**Enable in config:**
+```json5
+{
+ gateway: {
+ http: {
+ endpoints: {
+ chatCompletions: { enabled: true }
+ }
+ }
+ }
+}
+```
+
+**Request:**
+```bash
+curl -X POST http://127.0.0.1:18789/v1/chat/completions \
+ -H "Authorization: Bearer YOUR_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "model": "clawdbot:main",
+ "messages": [{"role": "user", "content": "Hello"}],
+ "stream": true
+ }'
+```
+
+**Model Format:**
+- `model: "clawdbot:
"` (e.g., `"clawdbot:main"`)
+- `model: "agent:"` (alias)
+
+### 3. OpenResponses HTTP API
+
+For clients that speak OpenResponses (item-based input, function tools).
+
+**Enable in config:**
+```json5
+{
+ gateway: {
+ http: {
+ endpoints: {
+ responses: { enabled: true }
+ }
+ }
+ }
+}
+```
+
+**Request:**
+```bash
+curl -X POST http://127.0.0.1:18789/v1/responses \
+ -H "Authorization: Bearer YOUR_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "model": "clawdbot:main",
+ "input": "Hello",
+ "stream": true
+ }'
+```
+
+### 4. Webhooks (Fire-and-Forget)
+
+For event-driven automation without maintaining a connection.
+
+**Enable in config:**
+```json5
+{
+ hooks: {
+ enabled: true,
+ token: "webhook-secret",
+ path: "/hooks"
+ }
+}
+```
+
+**Request:**
+```bash
+curl -X POST http://127.0.0.1:18789/hooks/agent \
+ -H "Authorization: Bearer webhook-secret" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "message": "Run this task",
+ "name": "Automation",
+ "sessionKey": "hook:automation:task-123",
+ "deliver": false,
+ "timeoutSeconds": 120
+ }'
+```
+
+**Response:** `202 Accepted` (async run started)
+
+### 5. CLI Subprocess
+
+For simple one-off automation (similar to Claude Code pattern).
+
+```bash
+clawdbot agent --message "Your prompt" --session-key "automation:task"
+```
+
+## Session Management
+
+### Session Key Format
+
+```
+agent::
+agent:main:main # Main agent, main session
+agent:main:subagent:abc # Subagent session
+agent:beta:main # Beta agent, main session
+hook:email:msg-123 # Webhook-spawned session
+global # Legacy global session
+```
+
+### Session Operations (WebSocket)
+
+```typescript
+// List sessions
+{ type: "req", id: "...", method: "sessions.list", params: { limit: 50, activeMinutes: 120 } }
+
+// Resolve session info
+{ type: "req", id: "...", method: "sessions.resolve", params: { key: "agent:main:main" } }
+
+// Patch session settings
+{ type: "req", id: "...", method: "sessions.patch", params: {
+ key: "agent:main:main",
+ thinkingLevel: "medium",
+ model: "anthropic/claude-sonnet-4-20250514"
+}}
+
+// Reset session (clear history)
+{ type: "req", id: "...", method: "sessions.reset", params: { key: "agent:main:main" } }
+
+// Delete session
+{ type: "req", id: "...", method: "sessions.delete", params: { key: "agent:main:main" } }
+
+// Compact session (summarize history)
+{ type: "req", id: "...", method: "sessions.compact", params: { key: "agent:main:main" } }
+```
+
+### Session CLI
+
+```bash
+clawdbot sessions # List sessions
+clawdbot sessions --active 120 # Active in last 2 hours
+clawdbot sessions --json # JSON output
+```
+
+## Streaming Events
+
+### Event Format
+
+```typescript
+interface AgentEvent {
+ runId: string; // Correlates to request
+ seq: number; // Monotonically increasing per run
+ stream: string; // Event category
+ ts: number; // Unix timestamp (ms)
+ data: Record; // Event-specific payload
+}
+```
+
+### Stream Types
+
+| Stream | Description |
+|--------|-------------|
+| `output` | Text output chunks |
+| `tool` | Tool invocation/result |
+| `thinking` | Extended thinking content |
+| `status` | Run status changes |
+| `error` | Error information |
+
+### Event Categories
+
+| Event Type | Payload |
+|------------|---------|
+| `assistant.delta` | `{ text: "..." }` |
+| `tool.start` | `{ name: "Read", input: {...} }` |
+| `tool.result` | `{ name: "Read", result: "..." }` |
+| `thinking.delta` | `{ text: "..." }` |
+| `run.complete` | `{ summary: "..." }` |
+| `run.error` | `{ error: "..." }` |
+
+## Token Usage / Cost Tracking
+
+OpenClaw tracks tokens per response and supports cost estimation.
+
+### In-Chat Commands
+
+```
+/status # Session model, context usage, last response tokens, estimated cost
+/usage off|tokens|full # Toggle per-response usage footer
+/usage cost # Local cost summary from session logs
+```
+
+### Configuration
+
+Token costs are configured per model:
+```json5
+{
+ models: {
+ providers: {
+ anthropic: {
+ models: [{
+ id: "claude-sonnet-4-20250514",
+ cost: {
+ input: 3.00, // USD per 1M tokens
+ output: 15.00,
+ cacheRead: 0.30,
+ cacheWrite: 3.75
+ }
+ }]
+ }
+ }
+ }
+}
+```
+
+### Programmatic Access
+
+Token usage is included in agent response payloads:
+```typescript
+// In final response or streaming events
+{
+ usage: {
+ inputTokens: 1234,
+ outputTokens: 567,
+ cacheReadTokens: 890,
+ cacheWriteTokens: 123
+ }
+}
+```
+
+## Authentication
+
+### Gateway Token
+
+```bash
+# Environment variable
+CLAWDBOT_GATEWAY_TOKEN=your-secret-token
+
+# Or config file
+{
+ gateway: {
+ auth: {
+ mode: "token",
+ token: "your-secret-token"
+ }
+ }
+}
+```
+
+### HTTP Requests
+
+```
+Authorization: Bearer YOUR_TOKEN
+```
+
+### WebSocket Connect
+
+```typescript
+{
+ params: {
+ auth: { token: "YOUR_TOKEN" }
+ }
+}
+```
+
+## Status Sync
+
+### Health Check
+
+```typescript
+// WebSocket
+{ type: "req", id: "...", method: "health", params: {} }
+
+// HTTP
+curl http://127.0.0.1:18789/health # Basic health
+clawdbot health --json # Detailed health
+```
+
+### Status Response
+
+```typescript
+{
+ ok: boolean;
+ linkedChannel?: string;
+ models?: { available: string[] };
+ agents?: { configured: string[] };
+ presence?: PresenceEntry[];
+ uptimeMs?: number;
+}
+```
+
+### Presence Events
+
+The gateway pushes presence updates to all connected clients:
+```typescript
+// Event
+{ type: "event", event: "presence", payload: { entries: [...], stateVersion: {...} } }
+```
+
+## Comparison with Other Agents
+
+| Aspect | OpenClaw | Claude Code | Codex | OpenCode | Amp |
+|--------|----------|-------------|-------|----------|-----|
+| **Process Model** | Daemon | Subprocess | Server | Server | Subprocess |
+| **Protocol** | WebSocket + HTTP | CLI JSONL | JSON-RPC stdio | HTTP + SSE | CLI JSONL |
+| **Session Resume** | Session Key | `--resume` | Thread ID | Session ID | `--continue` |
+| **Multi-Turn** | Same session key | Same session ID | Same thread | Same session | Same session |
+| **Streaming** | WS events + SSE | JSONL | Notifications | SSE | JSONL |
+| **HITL** | No | No (headless) | No (SDK) | Yes (SSE) | No |
+| **SDK** | None (protocol) | None (CLI) | Yes | Yes | Closed |
+
+### Key Differences
+
+1. **Daemon Model**: OpenClaw runs as a persistent gateway service, not a per-turn subprocess
+2. **Multi-Protocol**: Supports WebSocket, OpenAI-compatible HTTP, OpenResponses, and webhooks
+3. **Channel Integration**: Built-in WhatsApp/Telegram/Discord/iMessage support
+4. **Node System**: Mobile/desktop nodes can connect for camera, canvas, location, etc.
+5. **No HITL**: Like Claude/Codex/Amp, permissions are configured upfront, not interactive
+
+## Integration Patterns for sandbox-agent
+
+### Recommended: Persistent WebSocket Connection
+
+```typescript
+class OpenClawDriver {
+ private ws: WebSocket;
+ private pending = new Map();
+
+ async connect(url: string, token: string) {
+ this.ws = new WebSocket(url);
+ await this.handshake(token);
+ this.ws.on("message", (data) => this.handleMessage(JSON.parse(data)));
+ }
+
+ async runAgent(params: {
+ message: string;
+ sessionKey?: string;
+ thinking?: string;
+ }): Promise {
+ const runId = crypto.randomUUID();
+ const events: AgentEvent[] = [];
+
+ return new Promise((resolve, reject) => {
+ this.pending.set(runId, { resolve, reject, events });
+ this.ws.send(JSON.stringify({
+ type: "req",
+ id: runId,
+ method: "agent",
+ params: {
+ message: params.message,
+ sessionKey: params.sessionKey ?? "agent:main:main",
+ thinking: params.thinking,
+ deliver: false,
+ idempotencyKey: runId
+ }
+ }));
+ });
+ }
+
+ private handleMessage(frame: GatewayFrame) {
+ if (frame.type === "event" && frame.event === "agent") {
+ const pending = this.pending.get(frame.payload.runId);
+ if (pending) pending.events.push(frame.payload);
+ } else if (frame.type === "res") {
+ const pending = this.pending.get(frame.id);
+ if (pending && frame.payload?.status === "ok") {
+ pending.resolve({ result: frame.payload, events: pending.events });
+ this.pending.delete(frame.id);
+ } else if (pending && frame.payload?.status === "error") {
+ pending.reject(new Error(frame.payload.summary));
+ this.pending.delete(frame.id);
+ }
+ // Ignore "accepted" acks
+ }
+ }
+}
+```
+
+### Alternative: HTTP API (Simpler)
+
+```typescript
+async function runOpenClawPrompt(prompt: string, sessionKey?: string) {
+ const response = await fetch("http://127.0.0.1:18789/v1/chat/completions", {
+ method: "POST",
+ headers: {
+ "Authorization": `Bearer ${process.env.CLAWDBOT_GATEWAY_TOKEN}`,
+ "Content-Type": "application/json",
+ "x-clawdbot-session-key": sessionKey ?? "automation:sandbox"
+ },
+ body: JSON.stringify({
+ model: "clawdbot:main",
+ messages: [{ role: "user", content: prompt }],
+ stream: false
+ })
+ });
+ return response.json();
+}
+```
+
+## Configuration for sandbox-agent Integration
+
+Recommended config for automated use:
+
+```json5
+{
+ gateway: {
+ port: 18789,
+ auth: {
+ mode: "token",
+ token: "${CLAWDBOT_GATEWAY_TOKEN}"
+ },
+ http: {
+ endpoints: {
+ chatCompletions: { enabled: true },
+ responses: { enabled: true }
+ }
+ }
+ },
+ agents: {
+ defaults: {
+ model: {
+ primary: "anthropic/claude-sonnet-4-20250514"
+ },
+ thinking: { level: "low" },
+ workspace: "${HOME}/workspace"
+ }
+ }
+}
+```
+
+## Notes
+
+- OpenClaw is significantly more complex than other agents due to its gateway architecture
+- The multi-protocol support (WS, OpenAI, OpenResponses, webhooks) provides flexibility
+- Session management is richer (labels, spawn tracking, model/thinking overrides)
+- No SDK means direct protocol implementation is required
+- The daemon model means connection lifecycle management is important (reconnects, etc.)
+- Agent responses are two-stage: immediate ack + final result (handle both)
+- Tool policy filtering is configurable per agent/session/group
diff --git a/research/opencode-tmux-test.md b/research/opencode-tmux-test.md
new file mode 100644
index 0000000..ff553c0
--- /dev/null
+++ b/research/opencode-tmux-test.md
@@ -0,0 +1,54 @@
+# OpenCode TUI Test Plan (Tmux)
+
+This plan captures OpenCode TUI output and sends input via tmux so we can validate `/opencode` end-to-end.
+
+## Prereqs
+- `opencode` installed and on PATH.
+- `tmux` installed (e.g., `/home/linuxbrew/.linuxbrew/bin/tmux`).
+- Local sandbox-agent binary built.
+
+## Environment
+- `SANDBOX_AGENT_LOG_DIR=/path` to set server log dir
+- `SANDBOX_AGENT_LOG_STDOUT=1` to keep logs on stdout/stderr
+- `SANDBOX_AGENT_LOG_HTTP=0` to disable request logs
+- `SANDBOX_AGENT_LOG_HTTP_HEADERS=1` to include request headers (Authorization redacted)
+- `RUST_LOG=...` for trace filtering
+
+## Steps
+1. Build and run the server using the local binary:
+ ```bash
+ SANDBOX_AGENT_SKIP_INSPECTOR=1 cargo build -p sandbox-agent
+ SANDBOX_AGENT_LOG_HTTP_HEADERS=1 ./target/debug/sandbox-agent server \
+ --host 127.0.0.1 --port 2468 --token "$TOKEN"
+ ```
+2. Create a session via the OpenCode API:
+ ```bash
+ SESSION_JSON=$(curl -sS -H "Authorization: Bearer $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{}' \
+ http://127.0.0.1:2468/opencode/session)
+ SESSION_ID=$(node -e "const v=JSON.parse(process.env.SESSION_JSON||'{}');process.stdout.write(v.id||'');")
+ ```
+3. Start the OpenCode TUI in tmux:
+ ```bash
+ tmux new-session -d -s opencode \
+ "opencode attach http://127.0.0.1:2468/opencode --session $SESSION_ID --password $TOKEN"
+ ```
+4. Send a prompt:
+ ```bash
+ tmux send-keys -t opencode:0.0 "hello" C-m
+ ```
+5. Capture output:
+ ```bash
+ tmux capture-pane -pt opencode:0.0 -S -200 > /tmp/opencode-screen.txt
+ ```
+6. Inspect server logs for requests:
+ ```bash
+ tail -n 200 ~/.local/share/sandbox-agent/logs/log-$(date +%m-%d-%y)
+ ```
+7. Repeat after adjusting `/opencode` stubs until the TUI displays responses.
+
+## Notes
+- Tmux captures terminal output only. GUI outputs require screenshots or logs.
+- If OpenCode connects to another host/port, logs will show no requests.
+- If the prompt stays in the input box, use `C-m` to submit (plain `Enter` may not trigger send in tmux).
diff --git a/research/opencode-web-customization.md b/research/opencode-web-customization.md
new file mode 100644
index 0000000..0ba0ed9
--- /dev/null
+++ b/research/opencode-web-customization.md
@@ -0,0 +1,82 @@
+# OpenCode Web Customization & Local Run Notes
+
+## Local Web UI (pointing at `/opencode`)
+
+This uses the OpenCode web app from `~/misc/opencode/packages/app` and points it at the
+Sandbox Agent OpenCode-compatible API. The OpenCode JS SDK emits **absolute** paths
+(`"/global/event"`, `"/session/:id/message"`, etc.), so any base URL path is discarded.
+To keep the UI working, sandbox-agent now exposes the OpenCode router at both
+`/opencode/*` and the root (`/global/*`, `/session/*`, ...).
+
+### 1) Start sandbox-agent (OpenCode compat)
+
+```bash
+cd /home/nathan/sandbox-agent.feat-opencode-compat
+SANDBOX_AGENT_SKIP_INSPECTOR=1 SANDBOX_AGENT_LOG_STDOUT=1 \
+ ./target/debug/sandbox-agent server --no-token --host 127.0.0.1 --port 2468 \
+ --cors-allow-origin http://127.0.0.1:5173 \
+ > /tmp/sandbox-agent-opencode.log 2>&1 &
+```
+
+Logs:
+
+```bash
+tail -f /tmp/sandbox-agent-opencode.log
+```
+
+### 2) Start OpenCode web app (dev)
+
+```bash
+cd /home/nathan/misc/opencode/packages/app
+VITE_OPENCODE_SERVER_HOST=127.0.0.1 VITE_OPENCODE_SERVER_PORT=2468 \
+ /home/nathan/.bun/bin/bun run dev -- --host 127.0.0.1 --port 5173 \
+ > /tmp/opencode-web.log 2>&1 &
+```
+
+Logs:
+
+```bash
+tail -f /tmp/opencode-web.log
+```
+
+### 3) Open the UI
+
+```
+http://127.0.0.1:5173/
+```
+
+The app should connect to `http://127.0.0.1:2468` by default in dev (via
+`VITE_OPENCODE_SERVER_HOST/PORT`). If you see a “Could not connect to server”
+error, verify the sandbox-agent process is running and reachable on port 2468.
+
+### Notes
+
+- The web UI uses `VITE_OPENCODE_SERVER_HOST` and `VITE_OPENCODE_SERVER_PORT` to
+ pick the OpenCode server in dev mode (see `packages/app/src/app.tsx`).
+- When running in production, the app defaults to `window.location.origin` for
+ the server URL. If you need a different target, you must configure it via the
+ in-app “Switch server” dialog or change the build config.
+- If you see a connect error in the web app, check CORS. By default, sandbox-agent
+ allows no origins. You must pass `--cors-allow-origin` for the dev server URL.
+- The OpenCode provider list now exposes a `sandbox-agent` provider with models
+ for each agent (defaulting to `mock`). Use the provider/model selector in the UI
+ to pick the backing agent instead of environment variables.
+
+## Dev Server Learnings (Feb 4, 2026)
+
+- The browser **cannot** reach `http://127.0.0.1:2468` unless the web UI is on the
+ same machine. If the UI is loaded from `http://100.94.102.49:5173`, the server
+ must be reachable at `http://100.94.102.49:2468`.
+- The OpenCode JS SDK uses absolute paths, so a base URL path (like
+ `http://host:port/opencode`) is ignored. This means the server must expose
+ OpenCode routes at the **root** (`/global/*`, `/session/*`, ...), even if it
+ also exposes them under `/opencode/*`.
+- CORS must allow the UI origin. Example:
+ ```bash
+ ./target/debug/sandbox-agent server --no-token --host 0.0.0.0 --port 2468 \
+ --cors-allow-origin http://100.94.102.49:5173
+ ```
+- Binding the dev servers to `0.0.0.0` is required for remote access. Verify
+ `ss -ltnp | grep ':2468'` and `ss -ltnp | grep ':5173'`.
+- If the UI throws “No default model found”, it usually means the `/provider`
+ response lacks a providerID → modelID default mapping for a connected provider.
diff --git a/research/specs/command-shell-exec.md b/research/specs/command-shell-exec.md
new file mode 100644
index 0000000..6980123
--- /dev/null
+++ b/research/specs/command-shell-exec.md
@@ -0,0 +1,23 @@
+# Spec: Command + Shell Execution
+
+**Proposed API Changes**
+- Add command execution APIs to the core session manager (non-PTY, single-shot).
+- Define output capture and error handling semantics in the session event stream.
+
+**Summary**
+OpenCode routes for command/shell execution should run real commands in the session context and stream outputs to OpenCode message parts and events.
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/command`
+- `POST /opencode/session/{sessionID}/command`
+- `POST /opencode/session/{sessionID}/shell`
+
+**Core Functionality Required**
+- Execute commands with cwd/env + timeout support.
+- Capture stdout/stderr, exit code, and duration.
+- Optional streaming output to session events.
+- Map command output into OpenCode `message.part.updated` events.
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubs for `/command`, `/session/{sessionID}/command`, `/session/{sessionID}/shell`.
+- Add E2E tests to run a simple command and validate output is returned and events are emitted.
diff --git a/research/specs/event-stream-parity.md b/research/specs/event-stream-parity.md
new file mode 100644
index 0000000..3532e02
--- /dev/null
+++ b/research/specs/event-stream-parity.md
@@ -0,0 +1,23 @@
+# Spec: Event Stream Parity
+
+**Proposed API Changes**
+- Ensure all core session manager events map to OpenCode’s expected event types and sequencing.
+- Provide structured, ordered event delivery with backpressure handling.
+
+**Summary**
+OpenCode relies on SSE event streams for UI state. We need full parity in event ordering, types, and payloads, not just message/part events.
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/event`
+- `GET /opencode/global/event`
+
+**Core Functionality Required**
+- Deterministic event ordering and replay by offset.
+- Explicit `session.status` transitions (busy/idle/error).
+- `message.updated` and `message.part.updated` with full payloads.
+- Permission and question events with full metadata.
+- Error events with structured details.
+
+**OpenCode Compat Wiring + Tests**
+- Replace partial event emission with full parity for all supported events.
+- Add E2E tests that validate event ordering and type coverage in SSE streams.
diff --git a/research/specs/filesystem-integration.md b/research/specs/filesystem-integration.md
new file mode 100644
index 0000000..a9e3b38
--- /dev/null
+++ b/research/specs/filesystem-integration.md
@@ -0,0 +1,25 @@
+# Spec: Filesystem Integration
+
+**Proposed API Changes**
+- Add a workspace filesystem service to the core session manager with path scoping and traversal protection.
+- Expose file list/content/status APIs via the core service for reuse in OpenCode compat.
+
+**Summary**
+Provide safe, read-oriented filesystem access needed by OpenCode for file listing, content retrieval, and status details within a session directory.
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/file`
+- `GET /opencode/file/content`
+- `GET /opencode/file/status`
+- `GET /opencode/path`
+
+**Core Functionality Required**
+- Path normalization and sandboxed root enforcement per session/project.
+- File listing with filters (directory, glob, depth, hidden).
+- File content retrieval with mime detection and optional range.
+- File status (exists, type, size, last modified; optionally VCS status).
+- Optional file tree caching for performance.
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubs for `/file`, `/file/content`, `/file/status`, and `/path`.
+- Add E2E tests for reading content, listing directories, and invalid path handling.
diff --git a/research/specs/formatter-lsp.md b/research/specs/formatter-lsp.md
new file mode 100644
index 0000000..4ac3572
--- /dev/null
+++ b/research/specs/formatter-lsp.md
@@ -0,0 +1,21 @@
+# Spec: Formatter + LSP Integration
+
+**Proposed API Changes**
+- Add a formatter service and LSP status registry to the core session manager.
+- Provide per-language formatter availability and LSP server status.
+
+**Summary**
+OpenCode surfaces formatter and LSP availability via dedicated endpoints. We need real integration (or at minimum, real status introspection).
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/formatter`
+- `GET /opencode/lsp`
+
+**Core Functionality Required**
+- Discover available formatters by language in the workspace.
+- Track LSP server status (running, capabilities).
+- Optional API to trigger formatting for a file (future extension).
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubs for `/formatter` and `/lsp`.
+- Add E2E tests that validate formatter/LSP presence for fixture languages.
diff --git a/research/specs/mcp-integration.md b/research/specs/mcp-integration.md
new file mode 100644
index 0000000..fd45241
--- /dev/null
+++ b/research/specs/mcp-integration.md
@@ -0,0 +1,31 @@
+# Spec: MCP Integration
+
+**Proposed API Changes**
+- Add an MCP server registry to the core session manager.
+- Support MCP auth lifecycle and connect/disconnect operations.
+- Expose tool discovery metadata for OpenCode tooling.
+
+**Summary**
+OpenCode expects MCP server registration, authentication, and connectivity endpoints, plus tool discovery for those servers.
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/mcp`
+- `POST /opencode/mcp`
+- `POST /opencode/mcp/{name}/auth`
+- `DELETE /opencode/mcp/{name}/auth`
+- `POST /opencode/mcp/{name}/auth/callback`
+- `POST /opencode/mcp/{name}/auth/authenticate`
+- `POST /opencode/mcp/{name}/connect`
+- `POST /opencode/mcp/{name}/disconnect`
+- `GET /opencode/experimental/tool`
+- `GET /opencode/experimental/tool/ids`
+
+**Core Functionality Required**
+- Register MCP servers with config (url, transport, auth type).
+- Auth flows (token exchange, callback handling).
+- Connect/disconnect lifecycle and health.
+- Tool listing and tool ID exposure for connected servers.
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubs for `/mcp*` and `/experimental/tool*`.
+- Add E2E tests using a real MCP test server to validate auth + tool list flows.
diff --git a/research/specs/project-worktree.md b/research/specs/project-worktree.md
new file mode 100644
index 0000000..87d2d84
--- /dev/null
+++ b/research/specs/project-worktree.md
@@ -0,0 +1,27 @@
+# Spec: Project + Worktree Model
+
+**Proposed API Changes**
+- Add a project/worktree manager to the core session manager.
+- Expose project metadata and active worktree operations (create/list/reset/delete).
+
+**Summary**
+OpenCode relies on project and worktree endpoints for context and repo operations. We need a real project model backed by the workspace and VCS.
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/project`
+- `GET /opencode/project/current`
+- `PATCH /opencode/project/{projectID}`
+- `GET /opencode/experimental/worktree`
+- `POST /opencode/experimental/worktree`
+- `DELETE /opencode/experimental/worktree`
+- `POST /opencode/experimental/worktree/reset`
+
+**Core Functionality Required**
+- Project identity and metadata (id, title, directory, branch).
+- Current project derivation by directory/session.
+- Worktree creation/reset/delete tied to VCS.
+- Return consistent IDs and location data.
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubs for `/project`, `/project/current`, `/project/{projectID}`, and `/experimental/worktree*` endpoints.
+- Add E2E tests for worktree lifecycle and project metadata correctness.
diff --git a/research/specs/provider-auth.md b/research/specs/provider-auth.md
new file mode 100644
index 0000000..1ab638d
--- /dev/null
+++ b/research/specs/provider-auth.md
@@ -0,0 +1,26 @@
+# Spec: Provider Auth Lifecycle
+
+**Proposed API Changes**
+- Add provider credential management to the core session manager (integrated with existing credentials store).
+- Expose OAuth and direct credential set/remove operations.
+
+**Summary**
+OpenCode expects provider auth and OAuth endpoints. We need a real provider registry and credential storage that ties to agent configuration.
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/provider`
+- `GET /opencode/provider/auth`
+- `POST /opencode/provider/{providerID}/oauth/authorize`
+- `POST /opencode/provider/{providerID}/oauth/callback`
+- `PUT /opencode/auth/{providerID}`
+- `DELETE /opencode/auth/{providerID}`
+
+**Core Functionality Required**
+- Provider registry with models, capabilities, and connection state.
+- OAuth initiation/callback handling with credential storage.
+- Direct credential setting/removal.
+- Mapping to agent manager credentials and environment.
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubs for `/provider`, `/provider/auth`, `/provider/{providerID}/oauth/*`, `/auth/{providerID}`.
+- Add E2E tests for credential set/remove and provider list reflecting auth state.
diff --git a/research/specs/pty-management.md b/research/specs/pty-management.md
new file mode 100644
index 0000000..06aa62a
--- /dev/null
+++ b/research/specs/pty-management.md
@@ -0,0 +1,28 @@
+# Spec: PTY Management
+
+**Proposed API Changes**
+- Add a PTY manager to the core session manager to spawn, track, and stream terminal processes.
+- Define a PTY IO channel for OpenCode connect operations (SSE or websocket).
+
+**Summary**
+OpenCode expects a PTY lifecycle API with live IO. We need real PTY creation and streaming output/input handling.
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/pty`
+- `POST /opencode/pty`
+- `GET /opencode/pty/{ptyID}`
+- `PUT /opencode/pty/{ptyID}`
+- `DELETE /opencode/pty/{ptyID}`
+- `GET /opencode/pty/{ptyID}/connect`
+
+**Core Functionality Required**
+- Spawn PTY processes with configurable cwd/args/title/env.
+- Track PTY state (running/exited), pid, exit code.
+- Streaming output channel with backpressure handling.
+- Input write support with safe buffering.
+- Cleanup on session termination.
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubs for `/pty` and `/pty/{ptyID}` endpoints.
+- Implement `/pty/{ptyID}/connect` streaming with real PTY IO.
+- Add E2E tests for PTY spawn, output capture, and input echo.
diff --git a/research/specs/search-symbol-indexing.md b/research/specs/search-symbol-indexing.md
new file mode 100644
index 0000000..715e61d
--- /dev/null
+++ b/research/specs/search-symbol-indexing.md
@@ -0,0 +1,23 @@
+# Spec: Search + Symbol Indexing
+
+**Proposed API Changes**
+- Add a search/indexing service to the core session manager (ripgrep-backed initially).
+- Expose APIs for text search, file search, and symbol search.
+
+**Summary**
+OpenCode expects fast search endpoints for files, text, and symbols within a workspace. These must be safe and scoped.
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/find`
+- `GET /opencode/find/file`
+- `GET /opencode/find/symbol`
+
+**Core Functionality Required**
+- Text search with pattern, case sensitivity, and result limits.
+- File search with glob/substring match.
+- Symbol indexing (language server or ctags-backed), with caching and incremental updates.
+- Proper path scoping and escaping.
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubs for `/find`, `/find/file`, `/find/symbol`.
+- Add E2E tests with a fixture repo verifying search hits.
diff --git a/research/specs/session-persistence.md b/research/specs/session-persistence.md
new file mode 100644
index 0000000..a8ca6ee
--- /dev/null
+++ b/research/specs/session-persistence.md
@@ -0,0 +1,39 @@
+# Spec: Session Persistence & Metadata
+
+**Proposed API Changes**
+- Add a persistent session store to the core session manager (pluggable backend, default on-disk).
+- Expose session metadata fields in the core API: `title`, `parent_id`, `project_id`, `directory`, `version`, `share_url`, `created_at`, `updated_at`, `status`.
+- Add session list/get/update/delete/fork/share/todo/diff/revert/unrevert/abort/init operations to the core API.
+
+**Summary**
+Bring the core session manager to feature parity with OpenCode’s session metadata model. Sessions must be durable, queryable, and updatable, and must remain consistent with message history and event streams.
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/session`
+- `POST /opencode/session`
+- `GET /opencode/session/{sessionID}`
+- `PATCH /opencode/session/{sessionID}`
+- `DELETE /opencode/session/{sessionID}`
+- `GET /opencode/session/status`
+- `GET /opencode/session/{sessionID}/children`
+- `POST /opencode/session/{sessionID}/fork`
+- `POST /opencode/session/{sessionID}/share`
+- `DELETE /opencode/session/{sessionID}/share`
+- `GET /opencode/session/{sessionID}/todo`
+- `GET /opencode/session/{sessionID}/diff`
+- `POST /opencode/session/{sessionID}/revert`
+- `POST /opencode/session/{sessionID}/unrevert`
+- `POST /opencode/session/{sessionID}/abort`
+- `POST /opencode/session/{sessionID}/init`
+
+**Core Functionality Required**
+- Persistent storage for sessions and metadata (including parent/child relationships).
+- Session status tracking (idle/busy/error) with timestamps.
+- Share URL lifecycle (create/revoke).
+- Forking semantics that clone metadata and link parent/child.
+- Revert/unrevert bookkeeping tied to VCS snapshots (see VCS spec).
+- Consistent ordering and deterministic IDs across restarts.
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubs for: `/session` (GET/POST), `/session/{sessionID}` (GET/PATCH/DELETE), `/session/status`, `/session/{sessionID}/children`, `/session/{sessionID}/fork`, `/session/{sessionID}/share` (POST/DELETE), `/session/{sessionID}/todo`, `/session/{sessionID}/diff`, `/session/{sessionID}/revert`, `/session/{sessionID}/unrevert`, `/session/{sessionID}/abort`, `/session/{sessionID}/init`.
+- Extend opencode-compat E2E tests to verify persistence across server restarts and correct metadata updates.
diff --git a/research/specs/summarize-todo.md b/research/specs/summarize-todo.md
new file mode 100644
index 0000000..04defea
--- /dev/null
+++ b/research/specs/summarize-todo.md
@@ -0,0 +1,21 @@
+# Spec: Session Summarization + Todo
+
+**Proposed API Changes**
+- Add summarization and todo generation to the core session manager.
+- Store summaries/todos as session artifacts with versioning.
+
+**Summary**
+OpenCode expects session summarize and todo endpoints backed by actual model output.
+
+**OpenCode Endpoints (Reference)**
+- `POST /opencode/session/{sessionID}/summarize`
+- `GET /opencode/session/{sessionID}/todo`
+
+**Core Functionality Required**
+- Generate a summary using the selected model/provider.
+- Store and return the latest summary (and optionally history).
+- Generate and store todo items derived from session activity.
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubs for `/session/{sessionID}/summarize` and `/session/{sessionID}/todo`.
+- Add E2E tests validating summary content and todo list structure.
diff --git a/research/specs/toolcall-file-actions.md b/research/specs/toolcall-file-actions.md
new file mode 100644
index 0000000..84c677f
--- /dev/null
+++ b/research/specs/toolcall-file-actions.md
@@ -0,0 +1,23 @@
+# Spec: Tool Calls + File Actions (Session Manager)
+
+**Proposed API Changes**
+- Add tool call lifecycle tracking in the core session manager (call started, delta, completed, result).
+- Add file action integration (write/patch/rename/delete) with audited events.
+
+**Summary**
+OpenCode expects tool calls and file actions to surface through message parts and events. The core session manager must track tool call lifecycles and file actions reliably.
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/event`
+- `GET /opencode/global/event`
+- `POST /opencode/session/{sessionID}/message`
+
+**Core Functionality Required**
+- Explicit tool call tracking with call IDs, arguments, outputs, timing, and status.
+- File action execution (write/patch/rename/delete) with safe path scoping.
+- Emission of file edit events tied to actual writes.
+- Mapping tool call and file action data into universal events for conversion.
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubbed tool/file part generation with real data sourced from session manager tool/file APIs.
+- Add E2E tests to validate tool call lifecycle events and file edit events.
diff --git a/research/specs/tui-control-flow.md b/research/specs/tui-control-flow.md
new file mode 100644
index 0000000..368018a
--- /dev/null
+++ b/research/specs/tui-control-flow.md
@@ -0,0 +1,32 @@
+# Spec: TUI Control Flow
+
+**Proposed API Changes**
+- Add a TUI control queue and state machine to the core session manager.
+- Expose request/response transport for OpenCode TUI controls.
+
+**Summary**
+OpenCode’s TUI endpoints allow a remote controller to drive the UI. We need a server-side queue for control messages and a way to emit responses.
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/tui/control/next`
+- `POST /opencode/tui/control/response`
+- `POST /opencode/tui/append-prompt`
+- `POST /opencode/tui/clear-prompt`
+- `POST /opencode/tui/execute-command`
+- `POST /opencode/tui/open-help`
+- `POST /opencode/tui/open-models`
+- `POST /opencode/tui/open-sessions`
+- `POST /opencode/tui/open-themes`
+- `POST /opencode/tui/publish`
+- `POST /opencode/tui/select-session`
+- `POST /opencode/tui/show-toast`
+- `POST /opencode/tui/submit-prompt`
+
+**Core Functionality Required**
+- Persistent queue of pending UI control actions.
+- Response correlation (request/response IDs).
+- Optional integration with session events for UI feedback.
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubs for all `/tui/*` endpoints.
+- Add E2E tests for control queue ordering and response handling.
diff --git a/research/specs/vcs-integration.md b/research/specs/vcs-integration.md
new file mode 100644
index 0000000..fbbcacb
--- /dev/null
+++ b/research/specs/vcs-integration.md
@@ -0,0 +1,25 @@
+# Spec: VCS Integration
+
+**Proposed API Changes**
+- Add a VCS service to the core session manager (Git-first) with status, diff, branch, and revert operations.
+- Expose APIs for session-level diff and revert/unrevert semantics.
+
+**Summary**
+Enable OpenCode endpoints that depend on repository state, diffs, and revert flows.
+
+**OpenCode Endpoints (Reference)**
+- `GET /opencode/vcs`
+- `GET /opencode/session/{sessionID}/diff`
+- `POST /opencode/session/{sessionID}/revert`
+- `POST /opencode/session/{sessionID}/unrevert`
+
+**Core Functionality Required**
+- Repo discovery from session directory (with safe fallback).
+- Status summary (branch, dirty files, ahead/behind).
+- Diff generation (staged/unstaged, per file and full).
+- Revert/unrevert mechanics with temporary snapshots or stashes.
+- Integration with file status endpoint when available.
+
+**OpenCode Compat Wiring + Tests**
+- Replace stubs for `/vcs`, `/session/{sessionID}/diff`, `/session/{sessionID}/revert`, `/session/{sessionID}/unrevert`.
+- Add E2E tests that modify a fixture repo and validate diff + revert flows.
diff --git a/resources/agent-schemas/artifacts/json-schema/opencode.json b/resources/agent-schemas/artifacts/json-schema/opencode.json
index 82a8235..7086df6 100644
--- a/resources/agent-schemas/artifacts/json-schema/opencode.json
+++ b/resources/agent-schemas/artifacts/json-schema/opencode.json
@@ -282,6 +282,14 @@
},
"deletions": {
"type": "number"
+ },
+ "status": {
+ "type": "string",
+ "enum": [
+ "added",
+ "deleted",
+ "modified"
+ ]
}
},
"required": [
@@ -777,6 +785,60 @@
"text"
]
},
+ "SubtaskPart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "subtask"
+ },
+ "prompt": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "agent": {
+ "type": "string"
+ },
+ "model": {
+ "type": "object",
+ "properties": {
+ "providerID": {
+ "type": "string"
+ },
+ "modelID": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "providerID",
+ "modelID"
+ ]
+ },
+ "command": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "sessionID",
+ "messageID",
+ "type",
+ "prompt",
+ "description",
+ "agent"
+ ]
+ },
"ReasoningPart": {
"type": "object",
"properties": {
@@ -1541,58 +1603,7 @@
"$ref": "#/definitions/TextPart"
},
{
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "sessionID": {
- "type": "string"
- },
- "messageID": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "const": "subtask"
- },
- "prompt": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "agent": {
- "type": "string"
- },
- "model": {
- "type": "object",
- "properties": {
- "providerID": {
- "type": "string"
- },
- "modelID": {
- "type": "string"
- }
- },
- "required": [
- "providerID",
- "modelID"
- ]
- },
- "command": {
- "type": "string"
- }
- },
- "required": [
- "id",
- "sessionID",
- "messageID",
- "type",
- "prompt",
- "description",
- "agent"
- ]
+ "$ref": "#/definitions/SubtaskPart"
},
{
"$ref": "#/definitions/ReasoningPart"
@@ -3085,55 +3096,6 @@
"payload"
]
},
- "BadRequestError": {
- "type": "object",
- "properties": {
- "data": {},
- "errors": {
- "type": "array",
- "items": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {}
- }
- },
- "success": {
- "type": "boolean",
- "const": false
- }
- },
- "required": [
- "data",
- "errors",
- "success"
- ]
- },
- "NotFoundError": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "const": "NotFoundError"
- },
- "data": {
- "type": "object",
- "properties": {
- "message": {
- "type": "string"
- }
- },
- "required": [
- "message"
- ]
- }
- },
- "required": [
- "name",
- "data"
- ]
- },
"KeybindsConfig": {
"description": "Custom keybind configurations",
"type": "object",
@@ -3634,6 +3596,10 @@
"description": "Enable mDNS service discovery",
"type": "boolean"
},
+ "mdnsDomain": {
+ "description": "Custom domain name for mDNS service (default: opencode.local)",
+ "type": "string"
+ },
"cors": {
"description": "Additional domains to allow for CORS",
"type": "array",
@@ -3729,6 +3695,9 @@
},
"doom_loop": {
"$ref": "#/definitions/PermissionActionConfig"
+ },
+ "skill": {
+ "$ref": "#/definitions/PermissionRuleConfig"
}
},
"additionalProperties": {
@@ -3746,6 +3715,10 @@
"model": {
"type": "string"
},
+ "variant": {
+ "description": "Default model variant for this agent (applies only when using the agent's configured model).",
+ "type": "string"
+ },
"temperature": {
"type": "number"
},
@@ -3792,9 +3765,25 @@
"additionalProperties": {}
},
"color": {
- "description": "Hex color code for the agent (e.g., #FF5733)",
- "type": "string",
- "pattern": "^#[0-9a-fA-F]{6}$"
+ "description": "Hex color code (e.g., #FF5733) or theme color (e.g., primary)",
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^#[0-9a-fA-F]{6}$"
+ },
+ {
+ "type": "string",
+ "enum": [
+ "primary",
+ "secondary",
+ "accent",
+ "success",
+ "warning",
+ "error",
+ "info"
+ ]
+ }
+ ]
},
"steps": {
"description": "Maximum number of agentic iterations before forcing text-only response",
@@ -4296,6 +4285,19 @@
]
}
},
+ "skills": {
+ "description": "Additional skill folder paths",
+ "type": "object",
+ "properties": {
+ "paths": {
+ "description": "Additional paths to skill folders",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
"watcher": {
"type": "object",
"properties": {
@@ -4618,73 +4620,6 @@
"experimental": {
"type": "object",
"properties": {
- "hook": {
- "type": "object",
- "properties": {
- "file_edited": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "command": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "environment": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "type": "string"
- }
- }
- },
- "required": [
- "command"
- ]
- }
- }
- },
- "session_completed": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "command": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "environment": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "type": "string"
- }
- }
- },
- "required": [
- "command"
- ]
- }
- }
- }
- },
- "chatMaxRetries": {
- "description": "Number of retries for chat completions on failure",
- "type": "number"
- },
"disable_paste_summary": {
"type": "boolean"
},
@@ -4718,6 +4653,134 @@
},
"additionalProperties": false
},
+ "BadRequestError": {
+ "type": "object",
+ "properties": {
+ "data": {},
+ "errors": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ }
+ },
+ "success": {
+ "type": "boolean",
+ "const": false
+ }
+ },
+ "required": [
+ "data",
+ "errors",
+ "success"
+ ]
+ },
+ "OAuth": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "oauth"
+ },
+ "refresh": {
+ "type": "string"
+ },
+ "access": {
+ "type": "string"
+ },
+ "expires": {
+ "type": "number"
+ },
+ "accountId": {
+ "type": "string"
+ },
+ "enterpriseUrl": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "refresh",
+ "access",
+ "expires"
+ ]
+ },
+ "ApiAuth": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "api"
+ },
+ "key": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "key"
+ ]
+ },
+ "WellKnownAuth": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "wellknown"
+ },
+ "key": {
+ "type": "string"
+ },
+ "token": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "key",
+ "token"
+ ]
+ },
+ "Auth": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/OAuth"
+ },
+ {
+ "$ref": "#/definitions/ApiAuth"
+ },
+ {
+ "$ref": "#/definitions/WellKnownAuth"
+ }
+ ]
+ },
+ "NotFoundError": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "const": "NotFoundError"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "message"
+ ]
+ }
+ },
+ "required": [
+ "name",
+ "data"
+ ]
+ },
"Model": {
"type": "object",
"properties": {
@@ -5431,7 +5494,10 @@
"properties": {
"type": {
"type": "string",
- "const": "text"
+ "enum": [
+ "text",
+ "binary"
+ ]
},
"content": {
"type": "string"
@@ -5682,8 +5748,13 @@
"model": {
"type": "string"
},
- "mcp": {
- "type": "boolean"
+ "source": {
+ "type": "string",
+ "enum": [
+ "command",
+ "mcp",
+ "skill"
+ ]
},
"template": {
"anyOf": [
@@ -5761,6 +5832,9 @@
"providerID"
]
},
+ "variant": {
+ "type": "string"
+ },
"prompt": {
"type": "string"
},
@@ -5837,85 +5911,6 @@
"extensions",
"enabled"
]
- },
- "OAuth": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "const": "oauth"
- },
- "refresh": {
- "type": "string"
- },
- "access": {
- "type": "string"
- },
- "expires": {
- "type": "number"
- },
- "accountId": {
- "type": "string"
- },
- "enterpriseUrl": {
- "type": "string"
- }
- },
- "required": [
- "type",
- "refresh",
- "access",
- "expires"
- ]
- },
- "ApiAuth": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "const": "api"
- },
- "key": {
- "type": "string"
- }
- },
- "required": [
- "type",
- "key"
- ]
- },
- "WellKnownAuth": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "const": "wellknown"
- },
- "key": {
- "type": "string"
- },
- "token": {
- "type": "string"
- }
- },
- "required": [
- "type",
- "key",
- "token"
- ]
- },
- "Auth": {
- "anyOf": [
- {
- "$ref": "#/definitions/OAuth"
- },
- {
- "$ref": "#/definitions/ApiAuth"
- },
- {
- "$ref": "#/definitions/WellKnownAuth"
- }
- ]
}
}
}
\ No newline at end of file
diff --git a/resources/agent-schemas/artifacts/openapi/opencode.json b/resources/agent-schemas/artifacts/openapi/opencode.json
new file mode 100644
index 0000000..3c70324
--- /dev/null
+++ b/resources/agent-schemas/artifacts/openapi/opencode.json
@@ -0,0 +1,10933 @@
+{
+ "openapi": "3.1.1",
+ "info": {
+ "title": "opencode",
+ "description": "opencode api",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/global/health": {
+ "get": {
+ "operationId": "global.health",
+ "summary": "Get health",
+ "description": "Get health information about the OpenCode server.",
+ "responses": {
+ "200": {
+ "description": "Health information",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "healthy": {
+ "type": "boolean",
+ "const": true
+ },
+ "version": {
+ "type": "string"
+ }
+ },
+ "required": ["healthy", "version"]
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.health({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/global/event": {
+ "get": {
+ "operationId": "global.event",
+ "summary": "Get global events",
+ "description": "Subscribe to global events from the OpenCode system using server-sent events.",
+ "responses": {
+ "200": {
+ "description": "Event stream",
+ "content": {
+ "text/event-stream": {
+ "schema": {
+ "$ref": "#/components/schemas/GlobalEvent"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.event({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/global/config": {
+ "get": {
+ "operationId": "global.config.get",
+ "summary": "Get global configuration",
+ "description": "Retrieve the current global OpenCode configuration settings and preferences.",
+ "responses": {
+ "200": {
+ "description": "Get global config info",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Config"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.config.get({\n ...\n})"
+ }
+ ]
+ },
+ "patch": {
+ "operationId": "global.config.update",
+ "summary": "Update global configuration",
+ "description": "Update global OpenCode configuration settings and preferences.",
+ "responses": {
+ "200": {
+ "description": "Successfully updated global config",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Config"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Config"
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.config.update({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/global/dispose": {
+ "post": {
+ "operationId": "global.dispose",
+ "summary": "Dispose instance",
+ "description": "Clean up and dispose all OpenCode instances, releasing all resources.",
+ "responses": {
+ "200": {
+ "description": "Global disposed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.dispose({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/auth/{providerID}": {
+ "put": {
+ "operationId": "auth.set",
+ "summary": "Set auth credentials",
+ "description": "Set authentication credentials",
+ "responses": {
+ "200": {
+ "description": "Successfully set authentication credentials",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "in": "path",
+ "name": "providerID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Auth"
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n ...\n})"
+ }
+ ]
+ },
+ "delete": {
+ "operationId": "auth.remove",
+ "summary": "Remove auth credentials",
+ "description": "Remove authentication credentials",
+ "responses": {
+ "200": {
+ "description": "Successfully removed authentication credentials",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "in": "path",
+ "name": "providerID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/project": {
+ "get": {
+ "operationId": "project.list",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "List all projects",
+ "description": "Get a list of projects that have been opened with OpenCode.",
+ "responses": {
+ "200": {
+ "description": "List of projects",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Project"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.list({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/project/current": {
+ "get": {
+ "operationId": "project.current",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get current project",
+ "description": "Retrieve the currently active project that OpenCode is working with.",
+ "responses": {
+ "200": {
+ "description": "Current project information",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Project"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.current({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/project/{projectID}": {
+ "patch": {
+ "operationId": "project.update",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "projectID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Update project",
+ "description": "Update project properties such as name, icon, and commands.",
+ "responses": {
+ "200": {
+ "description": "Updated project information",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Project"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "icon": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "override": {
+ "type": "string"
+ },
+ "color": {
+ "type": "string"
+ }
+ }
+ },
+ "commands": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "description": "Startup script to run when creating a new workspace (worktree)",
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.update({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/pty": {
+ "get": {
+ "operationId": "pty.list",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "List PTY sessions",
+ "description": "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.",
+ "responses": {
+ "200": {
+ "description": "List of sessions",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pty"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.list({\n ...\n})"
+ }
+ ]
+ },
+ "post": {
+ "operationId": "pty.create",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Create PTY session",
+ "description": "Create a new pseudo-terminal (PTY) session for running shell commands and processes.",
+ "responses": {
+ "200": {
+ "description": "Created session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pty"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string"
+ },
+ "args": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "cwd": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "env": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.create({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/pty/{ptyID}": {
+ "get": {
+ "operationId": "pty.get",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "ptyID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Get PTY session",
+ "description": "Retrieve detailed information about a specific pseudo-terminal (PTY) session.",
+ "responses": {
+ "200": {
+ "description": "Session info",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pty"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.get({\n ...\n})"
+ }
+ ]
+ },
+ "put": {
+ "operationId": "pty.update",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "ptyID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Update PTY session",
+ "description": "Update properties of an existing pseudo-terminal (PTY) session.",
+ "responses": {
+ "200": {
+ "description": "Updated session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pty"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "size": {
+ "type": "object",
+ "properties": {
+ "rows": {
+ "type": "number"
+ },
+ "cols": {
+ "type": "number"
+ }
+ },
+ "required": ["rows", "cols"]
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.update({\n ...\n})"
+ }
+ ]
+ },
+ "delete": {
+ "operationId": "pty.remove",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "ptyID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Remove PTY session",
+ "description": "Remove and terminate a specific pseudo-terminal (PTY) session.",
+ "responses": {
+ "200": {
+ "description": "Session removed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.remove({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/pty/{ptyID}/connect": {
+ "get": {
+ "operationId": "pty.connect",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "ptyID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Connect to PTY session",
+ "description": "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.",
+ "responses": {
+ "200": {
+ "description": "Connected session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.connect({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/config": {
+ "get": {
+ "operationId": "config.get",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get configuration",
+ "description": "Retrieve the current OpenCode configuration settings and preferences.",
+ "responses": {
+ "200": {
+ "description": "Get config info",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Config"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.config.get({\n ...\n})"
+ }
+ ]
+ },
+ "patch": {
+ "operationId": "config.update",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Update configuration",
+ "description": "Update OpenCode configuration settings and preferences.",
+ "responses": {
+ "200": {
+ "description": "Successfully updated config",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Config"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Config"
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.config.update({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/config/providers": {
+ "get": {
+ "operationId": "config.providers",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "List config providers",
+ "description": "Get a list of all configured AI providers and their default models.",
+ "responses": {
+ "200": {
+ "description": "List of providers",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "providers": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Provider"
+ }
+ },
+ "default": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["providers", "default"]
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.config.providers({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/experimental/tool/ids": {
+ "get": {
+ "operationId": "tool.ids",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "List tool IDs",
+ "description": "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
+ "responses": {
+ "200": {
+ "description": "Tool IDs",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ToolIDs"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tool.ids({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/experimental/tool": {
+ "get": {
+ "operationId": "tool.list",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "provider",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "in": "query",
+ "name": "model",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "List tools",
+ "description": "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
+ "responses": {
+ "200": {
+ "description": "Tools",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ToolList"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tool.list({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/experimental/worktree": {
+ "post": {
+ "operationId": "worktree.create",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Create worktree",
+ "description": "Create a new git worktree for the current project and run any configured startup scripts.",
+ "responses": {
+ "200": {
+ "description": "Worktree created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Worktree"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/WorktreeCreateInput"
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.create({\n ...\n})"
+ }
+ ]
+ },
+ "get": {
+ "operationId": "worktree.list",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "List worktrees",
+ "description": "List all sandbox worktrees for the current project.",
+ "responses": {
+ "200": {
+ "description": "List of worktree directories",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.list({\n ...\n})"
+ }
+ ]
+ },
+ "delete": {
+ "operationId": "worktree.remove",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Remove worktree",
+ "description": "Remove a git worktree and delete its branch.",
+ "responses": {
+ "200": {
+ "description": "Worktree removed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/WorktreeRemoveInput"
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.remove({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/experimental/worktree/reset": {
+ "post": {
+ "operationId": "worktree.reset",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Reset worktree",
+ "description": "Reset a worktree branch to the primary default branch.",
+ "responses": {
+ "200": {
+ "description": "Worktree reset",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/WorktreeResetInput"
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.reset({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/experimental/resource": {
+ "get": {
+ "operationId": "experimental.resource.list",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get MCP resources",
+ "description": "Get all available MCP resources from connected servers. Optionally filter by name.",
+ "responses": {
+ "200": {
+ "description": "MCP resources",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/McpResource"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.resource.list({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session": {
+ "get": {
+ "operationId": "session.list",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ },
+ "description": "Filter sessions by project directory"
+ },
+ {
+ "in": "query",
+ "name": "roots",
+ "schema": {
+ "type": "boolean"
+ },
+ "description": "Only return root sessions (no parentID)"
+ },
+ {
+ "in": "query",
+ "name": "start",
+ "schema": {
+ "type": "number"
+ },
+ "description": "Filter sessions updated on or after this timestamp (milliseconds since epoch)"
+ },
+ {
+ "in": "query",
+ "name": "search",
+ "schema": {
+ "type": "string"
+ },
+ "description": "Filter sessions by title (case-insensitive)"
+ },
+ {
+ "in": "query",
+ "name": "limit",
+ "schema": {
+ "type": "number"
+ },
+ "description": "Maximum number of sessions to return"
+ }
+ ],
+ "summary": "List sessions",
+ "description": "Get a list of all OpenCode sessions, sorted by most recently updated.",
+ "responses": {
+ "200": {
+ "description": "List of sessions",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Session"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.list({\n ...\n})"
+ }
+ ]
+ },
+ "post": {
+ "operationId": "session.create",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Create session",
+ "description": "Create a new OpenCode session for interacting with AI assistants and managing conversations.",
+ "responses": {
+ "200": {
+ "description": "Successfully created session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Session"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "parentID": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "title": {
+ "type": "string"
+ },
+ "permission": {
+ "$ref": "#/components/schemas/PermissionRuleset"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.create({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/status": {
+ "get": {
+ "operationId": "session.status",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get session status",
+ "description": "Retrieve the current status of all sessions, including active, idle, and completed states.",
+ "responses": {
+ "200": {
+ "description": "Get session status",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/SessionStatus"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.status({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}": {
+ "get": {
+ "operationId": "session.get",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Get session",
+ "description": "Retrieve detailed information about a specific OpenCode session.",
+ "tags": ["Session"],
+ "responses": {
+ "200": {
+ "description": "Get session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Session"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.get({\n ...\n})"
+ }
+ ]
+ },
+ "delete": {
+ "operationId": "session.delete",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Delete session",
+ "description": "Delete a session and permanently remove all associated data, including messages and history.",
+ "responses": {
+ "200": {
+ "description": "Successfully deleted session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.delete({\n ...\n})"
+ }
+ ]
+ },
+ "patch": {
+ "operationId": "session.update",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Update session",
+ "description": "Update properties of an existing session, such as title or other metadata.",
+ "responses": {
+ "200": {
+ "description": "Successfully updated session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Session"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "archived": {
+ "type": "number"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.update({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/children": {
+ "get": {
+ "operationId": "session.children",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Get session children",
+ "tags": ["Session"],
+ "description": "Retrieve all child sessions that were forked from the specified parent session.",
+ "responses": {
+ "200": {
+ "description": "List of children",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Session"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.children({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/todo": {
+ "get": {
+ "operationId": "session.todo",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Session ID"
+ }
+ ],
+ "summary": "Get session todos",
+ "description": "Retrieve the todo list associated with a specific session, showing tasks and action items.",
+ "responses": {
+ "200": {
+ "description": "Todo list",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.todo({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/init": {
+ "post": {
+ "operationId": "session.init",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Session ID"
+ }
+ ],
+ "summary": "Initialize session",
+ "description": "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.",
+ "responses": {
+ "200": {
+ "description": "200",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "modelID": {
+ "type": "string"
+ },
+ "providerID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string",
+ "pattern": "^msg.*"
+ }
+ },
+ "required": ["modelID", "providerID", "messageID"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.init({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/fork": {
+ "post": {
+ "operationId": "session.fork",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Fork session",
+ "description": "Create a new session by forking an existing session at a specific message point.",
+ "responses": {
+ "200": {
+ "description": "200",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Session"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "messageID": {
+ "type": "string",
+ "pattern": "^msg.*"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.fork({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/abort": {
+ "post": {
+ "operationId": "session.abort",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Abort session",
+ "description": "Abort an active session and stop any ongoing AI processing or command execution.",
+ "responses": {
+ "200": {
+ "description": "Aborted session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.abort({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/share": {
+ "post": {
+ "operationId": "session.share",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Share session",
+ "description": "Create a shareable link for a session, allowing others to view the conversation.",
+ "responses": {
+ "200": {
+ "description": "Successfully shared session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Session"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.share({\n ...\n})"
+ }
+ ]
+ },
+ "delete": {
+ "operationId": "session.unshare",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Unshare session",
+ "description": "Remove the shareable link for a session, making it private again.",
+ "responses": {
+ "200": {
+ "description": "Successfully unshared session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Session"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.unshare({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/diff": {
+ "get": {
+ "operationId": "session.diff",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "required": true
+ },
+ {
+ "in": "query",
+ "name": "messageID",
+ "schema": {
+ "type": "string",
+ "pattern": "^msg.*"
+ }
+ }
+ ],
+ "summary": "Get message diff",
+ "description": "Get the file changes (diff) that resulted from a specific user message in the session.",
+ "responses": {
+ "200": {
+ "description": "Successfully retrieved diff",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FileDiff"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.diff({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/summarize": {
+ "post": {
+ "operationId": "session.summarize",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Session ID"
+ }
+ ],
+ "summary": "Summarize session",
+ "description": "Generate a concise summary of the session using AI compaction to preserve key information.",
+ "responses": {
+ "200": {
+ "description": "Summarized session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "providerID": {
+ "type": "string"
+ },
+ "modelID": {
+ "type": "string"
+ },
+ "auto": {
+ "default": false,
+ "type": "boolean"
+ }
+ },
+ "required": ["providerID", "modelID"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.summarize({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/message": {
+ "get": {
+ "operationId": "session.messages",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Session ID"
+ },
+ {
+ "in": "query",
+ "name": "limit",
+ "schema": {
+ "type": "number"
+ }
+ }
+ ],
+ "summary": "Get session messages",
+ "description": "Retrieve all messages in a session, including user prompts and AI responses.",
+ "responses": {
+ "200": {
+ "description": "List of messages",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "info": {
+ "$ref": "#/components/schemas/Message"
+ },
+ "parts": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Part"
+ }
+ }
+ },
+ "required": ["info", "parts"]
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.messages({\n ...\n})"
+ }
+ ]
+ },
+ "post": {
+ "operationId": "session.prompt",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Session ID"
+ }
+ ],
+ "summary": "Send message",
+ "description": "Create and send a new message to a session, streaming the AI response.",
+ "responses": {
+ "200": {
+ "description": "Created message",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "info": {
+ "$ref": "#/components/schemas/AssistantMessage"
+ },
+ "parts": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Part"
+ }
+ }
+ },
+ "required": ["info", "parts"]
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "messageID": {
+ "type": "string",
+ "pattern": "^msg.*"
+ },
+ "model": {
+ "type": "object",
+ "properties": {
+ "providerID": {
+ "type": "string"
+ },
+ "modelID": {
+ "type": "string"
+ }
+ },
+ "required": ["providerID", "modelID"]
+ },
+ "agent": {
+ "type": "string"
+ },
+ "noReply": {
+ "type": "boolean"
+ },
+ "tools": {
+ "description": "@deprecated tools and permissions have been merged, you can set permissions on the session itself now",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "boolean"
+ }
+ },
+ "system": {
+ "type": "string"
+ },
+ "variant": {
+ "type": "string"
+ },
+ "parts": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/TextPartInput"
+ },
+ {
+ "$ref": "#/components/schemas/FilePartInput"
+ },
+ {
+ "$ref": "#/components/schemas/AgentPartInput"
+ },
+ {
+ "$ref": "#/components/schemas/SubtaskPartInput"
+ }
+ ]
+ }
+ }
+ },
+ "required": ["parts"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.prompt({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/message/{messageID}": {
+ "get": {
+ "operationId": "session.message",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Session ID"
+ },
+ {
+ "in": "path",
+ "name": "messageID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Message ID"
+ }
+ ],
+ "summary": "Get message",
+ "description": "Retrieve a specific message from a session by its message ID.",
+ "responses": {
+ "200": {
+ "description": "Message",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "info": {
+ "$ref": "#/components/schemas/Message"
+ },
+ "parts": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Part"
+ }
+ }
+ },
+ "required": ["info", "parts"]
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.message({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/message/{messageID}/part/{partID}": {
+ "delete": {
+ "operationId": "part.delete",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Session ID"
+ },
+ {
+ "in": "path",
+ "name": "messageID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Message ID"
+ },
+ {
+ "in": "path",
+ "name": "partID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Part ID"
+ }
+ ],
+ "description": "Delete a part from a message",
+ "responses": {
+ "200": {
+ "description": "Successfully deleted part",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.part.delete({\n ...\n})"
+ }
+ ]
+ },
+ "patch": {
+ "operationId": "part.update",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Session ID"
+ },
+ {
+ "in": "path",
+ "name": "messageID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Message ID"
+ },
+ {
+ "in": "path",
+ "name": "partID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Part ID"
+ }
+ ],
+ "description": "Update a part in a message",
+ "responses": {
+ "200": {
+ "description": "Successfully updated part",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Part"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Part"
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.part.update({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/prompt_async": {
+ "post": {
+ "operationId": "session.prompt_async",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Session ID"
+ }
+ ],
+ "summary": "Send async message",
+ "description": "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.",
+ "responses": {
+ "204": {
+ "description": "Prompt accepted"
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "messageID": {
+ "type": "string",
+ "pattern": "^msg.*"
+ },
+ "model": {
+ "type": "object",
+ "properties": {
+ "providerID": {
+ "type": "string"
+ },
+ "modelID": {
+ "type": "string"
+ }
+ },
+ "required": ["providerID", "modelID"]
+ },
+ "agent": {
+ "type": "string"
+ },
+ "noReply": {
+ "type": "boolean"
+ },
+ "tools": {
+ "description": "@deprecated tools and permissions have been merged, you can set permissions on the session itself now",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "boolean"
+ }
+ },
+ "system": {
+ "type": "string"
+ },
+ "variant": {
+ "type": "string"
+ },
+ "parts": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/TextPartInput"
+ },
+ {
+ "$ref": "#/components/schemas/FilePartInput"
+ },
+ {
+ "$ref": "#/components/schemas/AgentPartInput"
+ },
+ {
+ "$ref": "#/components/schemas/SubtaskPartInput"
+ }
+ ]
+ }
+ }
+ },
+ "required": ["parts"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.prompt_async({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/command": {
+ "post": {
+ "operationId": "session.command",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Session ID"
+ }
+ ],
+ "summary": "Send command",
+ "description": "Send a new command to a session for execution by the AI assistant.",
+ "responses": {
+ "200": {
+ "description": "Created message",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "info": {
+ "$ref": "#/components/schemas/AssistantMessage"
+ },
+ "parts": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Part"
+ }
+ }
+ },
+ "required": ["info", "parts"]
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "messageID": {
+ "type": "string",
+ "pattern": "^msg.*"
+ },
+ "agent": {
+ "type": "string"
+ },
+ "model": {
+ "type": "string"
+ },
+ "arguments": {
+ "type": "string"
+ },
+ "command": {
+ "type": "string"
+ },
+ "variant": {
+ "type": "string"
+ },
+ "parts": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "file"
+ },
+ "mime": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "source": {
+ "$ref": "#/components/schemas/FilePartSource"
+ }
+ },
+ "required": ["type", "mime", "url"]
+ }
+ ]
+ }
+ }
+ },
+ "required": ["arguments", "command"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.command({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/shell": {
+ "post": {
+ "operationId": "session.shell",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Session ID"
+ }
+ ],
+ "summary": "Run shell command",
+ "description": "Execute a shell command within the session context and return the AI's response.",
+ "responses": {
+ "200": {
+ "description": "Created message",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AssistantMessage"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "agent": {
+ "type": "string"
+ },
+ "model": {
+ "type": "object",
+ "properties": {
+ "providerID": {
+ "type": "string"
+ },
+ "modelID": {
+ "type": "string"
+ }
+ },
+ "required": ["providerID", "modelID"]
+ },
+ "command": {
+ "type": "string"
+ }
+ },
+ "required": ["agent", "command"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.shell({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/revert": {
+ "post": {
+ "operationId": "session.revert",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Revert message",
+ "description": "Revert a specific message in a session, undoing its effects and restoring the previous state.",
+ "responses": {
+ "200": {
+ "description": "Updated session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Session"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "messageID": {
+ "type": "string",
+ "pattern": "^msg.*"
+ },
+ "partID": {
+ "type": "string",
+ "pattern": "^prt.*"
+ }
+ },
+ "required": ["messageID"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.revert({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/unrevert": {
+ "post": {
+ "operationId": "session.unrevert",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Restore reverted messages",
+ "description": "Restore all previously reverted messages in a session.",
+ "responses": {
+ "200": {
+ "description": "Updated session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Session"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.unrevert({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/session/{sessionID}/permissions/{permissionID}": {
+ "post": {
+ "operationId": "permission.respond",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "sessionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "in": "path",
+ "name": "permissionID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Respond to permission",
+ "deprecated": true,
+ "description": "Approve or deny a permission request from the AI assistant.",
+ "responses": {
+ "200": {
+ "description": "Permission processed successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "response": {
+ "type": "string",
+ "enum": ["once", "always", "reject"]
+ }
+ },
+ "required": ["response"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.respond({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/permission/{requestID}/reply": {
+ "post": {
+ "operationId": "permission.reply",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "requestID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Respond to permission request",
+ "description": "Approve or deny a permission request from the AI assistant.",
+ "responses": {
+ "200": {
+ "description": "Permission processed successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "reply": {
+ "type": "string",
+ "enum": ["once", "always", "reject"]
+ },
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["reply"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.reply({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/permission": {
+ "get": {
+ "operationId": "permission.list",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "List pending permissions",
+ "description": "Get all pending permission requests across all sessions.",
+ "responses": {
+ "200": {
+ "description": "List of pending permissions",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/PermissionRequest"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.list({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/question": {
+ "get": {
+ "operationId": "question.list",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "List pending questions",
+ "description": "Get all pending question requests across all sessions.",
+ "responses": {
+ "200": {
+ "description": "List of pending questions",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/QuestionRequest"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.question.list({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/question/{requestID}/reply": {
+ "post": {
+ "operationId": "question.reply",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "requestID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Reply to question request",
+ "description": "Provide answers to a question request from the AI assistant.",
+ "responses": {
+ "200": {
+ "description": "Question answered successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "answers": {
+ "description": "User answers in order of questions (each answer is an array of selected labels)",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/QuestionAnswer"
+ }
+ }
+ },
+ "required": ["answers"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.question.reply({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/question/{requestID}/reject": {
+ "post": {
+ "operationId": "question.reject",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "requestID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Reject question request",
+ "description": "Reject a question request from the AI assistant.",
+ "responses": {
+ "200": {
+ "description": "Question rejected successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.question.reject({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/provider": {
+ "get": {
+ "operationId": "provider.list",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "List providers",
+ "description": "Get a list of all available AI providers, including both available and connected ones.",
+ "responses": {
+ "200": {
+ "description": "List of providers",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "all": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "api": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "env": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "id": {
+ "type": "string"
+ },
+ "npm": {
+ "type": "string"
+ },
+ "models": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "family": {
+ "type": "string"
+ },
+ "release_date": {
+ "type": "string"
+ },
+ "attachment": {
+ "type": "boolean"
+ },
+ "reasoning": {
+ "type": "boolean"
+ },
+ "temperature": {
+ "type": "boolean"
+ },
+ "tool_call": {
+ "type": "boolean"
+ },
+ "interleaved": {
+ "anyOf": [
+ {
+ "type": "boolean",
+ "const": true
+ },
+ {
+ "type": "object",
+ "properties": {
+ "field": {
+ "type": "string",
+ "enum": ["reasoning_content", "reasoning_details"]
+ }
+ },
+ "required": ["field"],
+ "additionalProperties": false
+ }
+ ]
+ },
+ "cost": {
+ "type": "object",
+ "properties": {
+ "input": {
+ "type": "number"
+ },
+ "output": {
+ "type": "number"
+ },
+ "cache_read": {
+ "type": "number"
+ },
+ "cache_write": {
+ "type": "number"
+ },
+ "context_over_200k": {
+ "type": "object",
+ "properties": {
+ "input": {
+ "type": "number"
+ },
+ "output": {
+ "type": "number"
+ },
+ "cache_read": {
+ "type": "number"
+ },
+ "cache_write": {
+ "type": "number"
+ }
+ },
+ "required": ["input", "output"]
+ }
+ },
+ "required": ["input", "output"]
+ },
+ "limit": {
+ "type": "object",
+ "properties": {
+ "context": {
+ "type": "number"
+ },
+ "input": {
+ "type": "number"
+ },
+ "output": {
+ "type": "number"
+ }
+ },
+ "required": ["context", "output"]
+ },
+ "modalities": {
+ "type": "object",
+ "properties": {
+ "input": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["text", "audio", "image", "video", "pdf"]
+ }
+ },
+ "output": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["text", "audio", "image", "video", "pdf"]
+ }
+ }
+ },
+ "required": ["input", "output"]
+ },
+ "experimental": {
+ "type": "boolean"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["alpha", "beta", "deprecated"]
+ },
+ "options": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "headers": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "provider": {
+ "type": "object",
+ "properties": {
+ "npm": {
+ "type": "string"
+ }
+ },
+ "required": ["npm"]
+ },
+ "variants": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ }
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "release_date",
+ "attachment",
+ "reasoning",
+ "temperature",
+ "tool_call",
+ "limit",
+ "options"
+ ]
+ }
+ }
+ },
+ "required": ["name", "env", "id", "models"]
+ }
+ },
+ "default": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "connected": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["all", "default", "connected"]
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.list({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/provider/auth": {
+ "get": {
+ "operationId": "provider.auth",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get provider auth methods",
+ "description": "Retrieve available authentication methods for all AI providers.",
+ "responses": {
+ "200": {
+ "description": "Provider auth methods",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ProviderAuthMethod"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.auth({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/provider/{providerID}/oauth/authorize": {
+ "post": {
+ "operationId": "provider.oauth.authorize",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "providerID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Provider ID"
+ }
+ ],
+ "summary": "OAuth authorize",
+ "description": "Initiate OAuth authorization for a specific AI provider to get an authorization URL.",
+ "responses": {
+ "200": {
+ "description": "Authorization URL and method",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProviderAuthAuthorization"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "method": {
+ "description": "Auth method index",
+ "type": "number"
+ }
+ },
+ "required": ["method"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.oauth.authorize({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/provider/{providerID}/oauth/callback": {
+ "post": {
+ "operationId": "provider.oauth.callback",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "providerID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "description": "Provider ID"
+ }
+ ],
+ "summary": "OAuth callback",
+ "description": "Handle the OAuth callback from a provider after user authorization.",
+ "responses": {
+ "200": {
+ "description": "OAuth callback processed successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "method": {
+ "description": "Auth method index",
+ "type": "number"
+ },
+ "code": {
+ "description": "OAuth authorization code",
+ "type": "string"
+ }
+ },
+ "required": ["method"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.oauth.callback({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/find": {
+ "get": {
+ "operationId": "find.text",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "pattern",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Find text",
+ "description": "Search for text patterns across files in the project using ripgrep.",
+ "responses": {
+ "200": {
+ "description": "Matches",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["text"]
+ },
+ "lines": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["text"]
+ },
+ "line_number": {
+ "type": "number"
+ },
+ "absolute_offset": {
+ "type": "number"
+ },
+ "submatches": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "match": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["text"]
+ },
+ "start": {
+ "type": "number"
+ },
+ "end": {
+ "type": "number"
+ }
+ },
+ "required": ["match", "start", "end"]
+ }
+ }
+ },
+ "required": ["path", "lines", "line_number", "absolute_offset", "submatches"]
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.find.text({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/find/file": {
+ "get": {
+ "operationId": "find.files",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "query",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ {
+ "in": "query",
+ "name": "dirs",
+ "schema": {
+ "type": "string",
+ "enum": ["true", "false"]
+ }
+ },
+ {
+ "in": "query",
+ "name": "type",
+ "schema": {
+ "type": "string",
+ "enum": ["file", "directory"]
+ }
+ },
+ {
+ "in": "query",
+ "name": "limit",
+ "schema": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 200
+ }
+ }
+ ],
+ "summary": "Find files",
+ "description": "Search for files or directories by name or pattern in the project directory.",
+ "responses": {
+ "200": {
+ "description": "File paths",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.find.files({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/find/symbol": {
+ "get": {
+ "operationId": "find.symbols",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "query",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Find symbols",
+ "description": "Search for workspace symbols like functions, classes, and variables using LSP.",
+ "responses": {
+ "200": {
+ "description": "Symbols",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Symbol"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.find.symbols({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/file": {
+ "get": {
+ "operationId": "file.list",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "List files",
+ "description": "List files and directories in a specified path.",
+ "responses": {
+ "200": {
+ "description": "Files and directories",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FileNode"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.file.list({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/file/content": {
+ "get": {
+ "operationId": "file.read",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "path",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Read file",
+ "description": "Read the content of a specified file.",
+ "responses": {
+ "200": {
+ "description": "File content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FileContent"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.file.read({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/file/status": {
+ "get": {
+ "operationId": "file.status",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get file status",
+ "description": "Get the git status of all files in the project.",
+ "responses": {
+ "200": {
+ "description": "File status",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/File"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.file.status({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/mcp": {
+ "get": {
+ "operationId": "mcp.status",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get MCP status",
+ "description": "Get the status of all Model Context Protocol (MCP) servers.",
+ "responses": {
+ "200": {
+ "description": "MCP server status",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/MCPStatus"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.status({\n ...\n})"
+ }
+ ]
+ },
+ "post": {
+ "operationId": "mcp.add",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Add MCP server",
+ "description": "Dynamically add a new Model Context Protocol (MCP) server to the system.",
+ "responses": {
+ "200": {
+ "description": "MCP server added successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/MCPStatus"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "config": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/McpLocalConfig"
+ },
+ {
+ "$ref": "#/components/schemas/McpRemoteConfig"
+ }
+ ]
+ }
+ },
+ "required": ["name", "config"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.add({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/mcp/{name}/auth": {
+ "post": {
+ "operationId": "mcp.auth.start",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "in": "path",
+ "name": "name",
+ "required": true
+ }
+ ],
+ "summary": "Start MCP OAuth",
+ "description": "Start OAuth authentication flow for a Model Context Protocol (MCP) server.",
+ "responses": {
+ "200": {
+ "description": "OAuth flow started",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "authorizationUrl": {
+ "description": "URL to open in browser for authorization",
+ "type": "string"
+ }
+ },
+ "required": ["authorizationUrl"]
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.start({\n ...\n})"
+ }
+ ]
+ },
+ "delete": {
+ "operationId": "mcp.auth.remove",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "in": "path",
+ "name": "name",
+ "required": true
+ }
+ ],
+ "summary": "Remove MCP OAuth",
+ "description": "Remove OAuth credentials for an MCP server",
+ "responses": {
+ "200": {
+ "description": "OAuth credentials removed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "const": true
+ }
+ },
+ "required": ["success"]
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.remove({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/mcp/{name}/auth/callback": {
+ "post": {
+ "operationId": "mcp.auth.callback",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "in": "path",
+ "name": "name",
+ "required": true
+ }
+ ],
+ "summary": "Complete MCP OAuth",
+ "description": "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.",
+ "responses": {
+ "200": {
+ "description": "OAuth authentication completed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MCPStatus"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "description": "Authorization code from OAuth callback",
+ "type": "string"
+ }
+ },
+ "required": ["code"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.callback({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/mcp/{name}/auth/authenticate": {
+ "post": {
+ "operationId": "mcp.auth.authenticate",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "in": "path",
+ "name": "name",
+ "required": true
+ }
+ ],
+ "summary": "Authenticate MCP OAuth",
+ "description": "Start OAuth flow and wait for callback (opens browser)",
+ "responses": {
+ "200": {
+ "description": "OAuth authentication completed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MCPStatus"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.authenticate({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/mcp/{name}/connect": {
+ "post": {
+ "operationId": "mcp.connect",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "name",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "description": "Connect an MCP server",
+ "responses": {
+ "200": {
+ "description": "MCP server connected successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.connect({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/mcp/{name}/disconnect": {
+ "post": {
+ "operationId": "mcp.disconnect",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "name",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "description": "Disconnect an MCP server",
+ "responses": {
+ "200": {
+ "description": "MCP server disconnected successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.disconnect({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/append-prompt": {
+ "post": {
+ "operationId": "tui.appendPrompt",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Append TUI prompt",
+ "description": "Append prompt to the TUI",
+ "responses": {
+ "200": {
+ "description": "Prompt processed successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["text"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.appendPrompt({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/open-help": {
+ "post": {
+ "operationId": "tui.openHelp",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Open help dialog",
+ "description": "Open the help dialog in the TUI to display user assistance information.",
+ "responses": {
+ "200": {
+ "description": "Help dialog opened successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openHelp({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/open-sessions": {
+ "post": {
+ "operationId": "tui.openSessions",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Open sessions dialog",
+ "description": "Open the session dialog",
+ "responses": {
+ "200": {
+ "description": "Session dialog opened successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openSessions({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/open-themes": {
+ "post": {
+ "operationId": "tui.openThemes",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Open themes dialog",
+ "description": "Open the theme dialog",
+ "responses": {
+ "200": {
+ "description": "Theme dialog opened successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openThemes({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/open-models": {
+ "post": {
+ "operationId": "tui.openModels",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Open models dialog",
+ "description": "Open the model dialog",
+ "responses": {
+ "200": {
+ "description": "Model dialog opened successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openModels({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/submit-prompt": {
+ "post": {
+ "operationId": "tui.submitPrompt",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Submit TUI prompt",
+ "description": "Submit the prompt",
+ "responses": {
+ "200": {
+ "description": "Prompt submitted successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.submitPrompt({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/clear-prompt": {
+ "post": {
+ "operationId": "tui.clearPrompt",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Clear TUI prompt",
+ "description": "Clear the prompt",
+ "responses": {
+ "200": {
+ "description": "Prompt cleared successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.clearPrompt({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/execute-command": {
+ "post": {
+ "operationId": "tui.executeCommand",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Execute TUI command",
+ "description": "Execute a TUI command (e.g. agent_cycle)",
+ "responses": {
+ "200": {
+ "description": "Command executed successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string"
+ }
+ },
+ "required": ["command"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.executeCommand({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/show-toast": {
+ "post": {
+ "operationId": "tui.showToast",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Show TUI toast",
+ "description": "Show a toast notification in the TUI",
+ "responses": {
+ "200": {
+ "description": "Toast notification shown successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "variant": {
+ "type": "string",
+ "enum": ["info", "success", "warning", "error"]
+ },
+ "duration": {
+ "description": "Duration in milliseconds",
+ "default": 5000,
+ "type": "number"
+ }
+ },
+ "required": ["message", "variant"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.showToast({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/publish": {
+ "post": {
+ "operationId": "tui.publish",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Publish TUI event",
+ "description": "Publish a TUI event",
+ "responses": {
+ "200": {
+ "description": "Event published successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/Event.tui.prompt.append"
+ },
+ {
+ "$ref": "#/components/schemas/Event.tui.command.execute"
+ },
+ {
+ "$ref": "#/components/schemas/Event.tui.toast.show"
+ },
+ {
+ "$ref": "#/components/schemas/Event.tui.session.select"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.publish({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/select-session": {
+ "post": {
+ "operationId": "tui.selectSession",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Select session",
+ "description": "Navigate the TUI to display the specified session.",
+ "responses": {
+ "200": {
+ "description": "Session selected successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NotFoundError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "description": "Session ID to navigate to",
+ "type": "string",
+ "pattern": "^ses"
+ }
+ },
+ "required": ["sessionID"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.selectSession({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/control/next": {
+ "get": {
+ "operationId": "tui.control.next",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get next TUI request",
+ "description": "Retrieve the next TUI (Terminal User Interface) request from the queue for processing.",
+ "responses": {
+ "200": {
+ "description": "Next TUI request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string"
+ },
+ "body": {}
+ },
+ "required": ["path", "body"]
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.control.next({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/tui/control/response": {
+ "post": {
+ "operationId": "tui.control.response",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Submit TUI response",
+ "description": "Submit a response to the TUI request queue to complete a pending request.",
+ "responses": {
+ "200": {
+ "description": "Response submitted successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.control.response({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/instance/dispose": {
+ "post": {
+ "operationId": "instance.dispose",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Dispose instance",
+ "description": "Clean up and dispose the current OpenCode instance, releasing all resources.",
+ "responses": {
+ "200": {
+ "description": "Instance disposed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.instance.dispose({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/path": {
+ "get": {
+ "operationId": "path.get",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get paths",
+ "description": "Retrieve the current working directory and related path information for the OpenCode instance.",
+ "responses": {
+ "200": {
+ "description": "Path",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Path"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.path.get({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/vcs": {
+ "get": {
+ "operationId": "vcs.get",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get VCS info",
+ "description": "Retrieve version control system (VCS) information for the current project, such as git branch.",
+ "responses": {
+ "200": {
+ "description": "VCS info",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VcsInfo"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.get({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/command": {
+ "get": {
+ "operationId": "command.list",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "List commands",
+ "description": "Get a list of all available commands in the OpenCode system.",
+ "responses": {
+ "200": {
+ "description": "List of commands",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Command"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.command.list({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/log": {
+ "post": {
+ "operationId": "app.log",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Write log",
+ "description": "Write a log entry to the server logs with specified level and metadata.",
+ "responses": {
+ "200": {
+ "description": "Log entry written successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "service": {
+ "description": "Service name for the log entry",
+ "type": "string"
+ },
+ "level": {
+ "description": "Log level",
+ "type": "string",
+ "enum": ["debug", "info", "error", "warn"]
+ },
+ "message": {
+ "description": "Log message",
+ "type": "string"
+ },
+ "extra": {
+ "description": "Additional metadata for the log entry",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ }
+ },
+ "required": ["service", "level", "message"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.log({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/agent": {
+ "get": {
+ "operationId": "app.agents",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "List agents",
+ "description": "Get a list of all available AI agents in the OpenCode system.",
+ "responses": {
+ "200": {
+ "description": "List of agents",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Agent"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.agents({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/skill": {
+ "get": {
+ "operationId": "app.skills",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "List skills",
+ "description": "Get a list of all available skills in the OpenCode system.",
+ "responses": {
+ "200": {
+ "description": "List of skills",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "content": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "description", "location", "content"]
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.skills({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/lsp": {
+ "get": {
+ "operationId": "lsp.status",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get LSP status",
+ "description": "Get LSP server status",
+ "responses": {
+ "200": {
+ "description": "LSP server status",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/LSPStatus"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.lsp.status({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/formatter": {
+ "get": {
+ "operationId": "formatter.status",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get formatter status",
+ "description": "Get formatter status",
+ "responses": {
+ "200": {
+ "description": "Formatter status",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FormatterStatus"
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.formatter.status({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/event": {
+ "get": {
+ "operationId": "event.subscribe",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Subscribe to events",
+ "description": "Get events",
+ "responses": {
+ "200": {
+ "description": "Event stream",
+ "content": {
+ "text/event-stream": {
+ "schema": {
+ "$ref": "#/components/schemas/Event"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.event.subscribe({\n ...\n})"
+ }
+ ]
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Event.installation.updated": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "installation.updated"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "version": {
+ "type": "string"
+ }
+ },
+ "required": ["version"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.installation.update-available": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "installation.update-available"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "version": {
+ "type": "string"
+ }
+ },
+ "required": ["version"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Project": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "worktree": {
+ "type": "string"
+ },
+ "vcs": {
+ "type": "string",
+ "const": "git"
+ },
+ "name": {
+ "type": "string"
+ },
+ "icon": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "override": {
+ "type": "string"
+ },
+ "color": {
+ "type": "string"
+ }
+ }
+ },
+ "commands": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "description": "Startup script to run when creating a new workspace (worktree)",
+ "type": "string"
+ }
+ }
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "created": {
+ "type": "number"
+ },
+ "updated": {
+ "type": "number"
+ },
+ "initialized": {
+ "type": "number"
+ }
+ },
+ "required": ["created", "updated"]
+ },
+ "sandboxes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["id", "worktree", "time", "sandboxes"]
+ },
+ "Event.project.updated": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "project.updated"
+ },
+ "properties": {
+ "$ref": "#/components/schemas/Project"
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.server.instance.disposed": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "server.instance.disposed"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ }
+ },
+ "required": ["directory"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.server.connected": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "server.connected"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.global.disposed": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "global.disposed"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.lsp.client.diagnostics": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "lsp.client.diagnostics"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "serverID": {
+ "type": "string"
+ },
+ "path": {
+ "type": "string"
+ }
+ },
+ "required": ["serverID", "path"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.lsp.updated": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "lsp.updated"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.file.edited": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "file.edited"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "file": {
+ "type": "string"
+ }
+ },
+ "required": ["file"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "FileDiff": {
+ "type": "object",
+ "properties": {
+ "file": {
+ "type": "string"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ },
+ "additions": {
+ "type": "number"
+ },
+ "deletions": {
+ "type": "number"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["added", "deleted", "modified"]
+ }
+ },
+ "required": ["file", "before", "after", "additions", "deletions"]
+ },
+ "UserMessage": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "role": {
+ "type": "string",
+ "const": "user"
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "created": {
+ "type": "number"
+ }
+ },
+ "required": ["created"]
+ },
+ "summary": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "body": {
+ "type": "string"
+ },
+ "diffs": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FileDiff"
+ }
+ }
+ },
+ "required": ["diffs"]
+ },
+ "agent": {
+ "type": "string"
+ },
+ "model": {
+ "type": "object",
+ "properties": {
+ "providerID": {
+ "type": "string"
+ },
+ "modelID": {
+ "type": "string"
+ }
+ },
+ "required": ["providerID", "modelID"]
+ },
+ "system": {
+ "type": "string"
+ },
+ "tools": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "boolean"
+ }
+ },
+ "variant": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "sessionID", "role", "time", "agent", "model"]
+ },
+ "ProviderAuthError": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "const": "ProviderAuthError"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "providerID": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["providerID", "message"]
+ }
+ },
+ "required": ["name", "data"]
+ },
+ "UnknownError": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "const": "UnknownError"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"]
+ }
+ },
+ "required": ["name", "data"]
+ },
+ "MessageOutputLengthError": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "const": "MessageOutputLengthError"
+ },
+ "data": {
+ "type": "object",
+ "properties": {}
+ }
+ },
+ "required": ["name", "data"]
+ },
+ "MessageAbortedError": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "const": "MessageAbortedError"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"]
+ }
+ },
+ "required": ["name", "data"]
+ },
+ "APIError": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "const": "APIError"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ },
+ "statusCode": {
+ "type": "number"
+ },
+ "isRetryable": {
+ "type": "boolean"
+ },
+ "responseHeaders": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "responseBody": {
+ "type": "string"
+ },
+ "metadata": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["message", "isRetryable"]
+ }
+ },
+ "required": ["name", "data"]
+ },
+ "AssistantMessage": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "role": {
+ "type": "string",
+ "const": "assistant"
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "created": {
+ "type": "number"
+ },
+ "completed": {
+ "type": "number"
+ }
+ },
+ "required": ["created"]
+ },
+ "error": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ProviderAuthError"
+ },
+ {
+ "$ref": "#/components/schemas/UnknownError"
+ },
+ {
+ "$ref": "#/components/schemas/MessageOutputLengthError"
+ },
+ {
+ "$ref": "#/components/schemas/MessageAbortedError"
+ },
+ {
+ "$ref": "#/components/schemas/APIError"
+ }
+ ]
+ },
+ "parentID": {
+ "type": "string"
+ },
+ "modelID": {
+ "type": "string"
+ },
+ "providerID": {
+ "type": "string"
+ },
+ "mode": {
+ "type": "string"
+ },
+ "agent": {
+ "type": "string"
+ },
+ "path": {
+ "type": "object",
+ "properties": {
+ "cwd": {
+ "type": "string"
+ },
+ "root": {
+ "type": "string"
+ }
+ },
+ "required": ["cwd", "root"]
+ },
+ "summary": {
+ "type": "boolean"
+ },
+ "cost": {
+ "type": "number"
+ },
+ "tokens": {
+ "type": "object",
+ "properties": {
+ "input": {
+ "type": "number"
+ },
+ "output": {
+ "type": "number"
+ },
+ "reasoning": {
+ "type": "number"
+ },
+ "cache": {
+ "type": "object",
+ "properties": {
+ "read": {
+ "type": "number"
+ },
+ "write": {
+ "type": "number"
+ }
+ },
+ "required": ["read", "write"]
+ }
+ },
+ "required": ["input", "output", "reasoning", "cache"]
+ },
+ "finish": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "sessionID",
+ "role",
+ "time",
+ "parentID",
+ "modelID",
+ "providerID",
+ "mode",
+ "agent",
+ "path",
+ "cost",
+ "tokens"
+ ]
+ },
+ "Message": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/UserMessage"
+ },
+ {
+ "$ref": "#/components/schemas/AssistantMessage"
+ }
+ ]
+ },
+ "Event.message.updated": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "message.updated"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "info": {
+ "$ref": "#/components/schemas/Message"
+ }
+ },
+ "required": ["info"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.message.removed": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "message.removed"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ }
+ },
+ "required": ["sessionID", "messageID"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "TextPart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "text"
+ },
+ "text": {
+ "type": "string"
+ },
+ "synthetic": {
+ "type": "boolean"
+ },
+ "ignored": {
+ "type": "boolean"
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "number"
+ },
+ "end": {
+ "type": "number"
+ }
+ },
+ "required": ["start"]
+ },
+ "metadata": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ }
+ },
+ "required": ["id", "sessionID", "messageID", "type", "text"]
+ },
+ "SubtaskPart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "subtask"
+ },
+ "prompt": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "agent": {
+ "type": "string"
+ },
+ "model": {
+ "type": "object",
+ "properties": {
+ "providerID": {
+ "type": "string"
+ },
+ "modelID": {
+ "type": "string"
+ }
+ },
+ "required": ["providerID", "modelID"]
+ },
+ "command": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"]
+ },
+ "ReasoningPart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "reasoning"
+ },
+ "text": {
+ "type": "string"
+ },
+ "metadata": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "number"
+ },
+ "end": {
+ "type": "number"
+ }
+ },
+ "required": ["start"]
+ }
+ },
+ "required": ["id", "sessionID", "messageID", "type", "text", "time"]
+ },
+ "FilePartSourceText": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string"
+ },
+ "start": {
+ "type": "integer",
+ "minimum": -9007199254740991,
+ "maximum": 9007199254740991
+ },
+ "end": {
+ "type": "integer",
+ "minimum": -9007199254740991,
+ "maximum": 9007199254740991
+ }
+ },
+ "required": ["value", "start", "end"]
+ },
+ "FileSource": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "$ref": "#/components/schemas/FilePartSourceText"
+ },
+ "type": {
+ "type": "string",
+ "const": "file"
+ },
+ "path": {
+ "type": "string"
+ }
+ },
+ "required": ["text", "type", "path"]
+ },
+ "Range": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "object",
+ "properties": {
+ "line": {
+ "type": "number"
+ },
+ "character": {
+ "type": "number"
+ }
+ },
+ "required": ["line", "character"]
+ },
+ "end": {
+ "type": "object",
+ "properties": {
+ "line": {
+ "type": "number"
+ },
+ "character": {
+ "type": "number"
+ }
+ },
+ "required": ["line", "character"]
+ }
+ },
+ "required": ["start", "end"]
+ },
+ "SymbolSource": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "$ref": "#/components/schemas/FilePartSourceText"
+ },
+ "type": {
+ "type": "string",
+ "const": "symbol"
+ },
+ "path": {
+ "type": "string"
+ },
+ "range": {
+ "$ref": "#/components/schemas/Range"
+ },
+ "name": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "integer",
+ "minimum": -9007199254740991,
+ "maximum": 9007199254740991
+ }
+ },
+ "required": ["text", "type", "path", "range", "name", "kind"]
+ },
+ "ResourceSource": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "$ref": "#/components/schemas/FilePartSourceText"
+ },
+ "type": {
+ "type": "string",
+ "const": "resource"
+ },
+ "clientName": {
+ "type": "string"
+ },
+ "uri": {
+ "type": "string"
+ }
+ },
+ "required": ["text", "type", "clientName", "uri"]
+ },
+ "FilePartSource": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/FileSource"
+ },
+ {
+ "$ref": "#/components/schemas/SymbolSource"
+ },
+ {
+ "$ref": "#/components/schemas/ResourceSource"
+ }
+ ]
+ },
+ "FilePart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "file"
+ },
+ "mime": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "source": {
+ "$ref": "#/components/schemas/FilePartSource"
+ }
+ },
+ "required": ["id", "sessionID", "messageID", "type", "mime", "url"]
+ },
+ "ToolStatePending": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "pending"
+ },
+ "input": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "raw": {
+ "type": "string"
+ }
+ },
+ "required": ["status", "input", "raw"]
+ },
+ "ToolStateRunning": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "running"
+ },
+ "input": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "title": {
+ "type": "string"
+ },
+ "metadata": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "number"
+ }
+ },
+ "required": ["start"]
+ }
+ },
+ "required": ["status", "input", "time"]
+ },
+ "ToolStateCompleted": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "completed"
+ },
+ "input": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "output": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "metadata": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "number"
+ },
+ "end": {
+ "type": "number"
+ },
+ "compacted": {
+ "type": "number"
+ }
+ },
+ "required": ["start", "end"]
+ },
+ "attachments": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FilePart"
+ }
+ }
+ },
+ "required": ["status", "input", "output", "title", "metadata", "time"]
+ },
+ "ToolStateError": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "error"
+ },
+ "input": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "error": {
+ "type": "string"
+ },
+ "metadata": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "number"
+ },
+ "end": {
+ "type": "number"
+ }
+ },
+ "required": ["start", "end"]
+ }
+ },
+ "required": ["status", "input", "error", "time"]
+ },
+ "ToolState": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ToolStatePending"
+ },
+ {
+ "$ref": "#/components/schemas/ToolStateRunning"
+ },
+ {
+ "$ref": "#/components/schemas/ToolStateCompleted"
+ },
+ {
+ "$ref": "#/components/schemas/ToolStateError"
+ }
+ ]
+ },
+ "ToolPart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "tool"
+ },
+ "callID": {
+ "type": "string"
+ },
+ "tool": {
+ "type": "string"
+ },
+ "state": {
+ "$ref": "#/components/schemas/ToolState"
+ },
+ "metadata": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ }
+ },
+ "required": ["id", "sessionID", "messageID", "type", "callID", "tool", "state"]
+ },
+ "StepStartPart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "step-start"
+ },
+ "snapshot": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "sessionID", "messageID", "type"]
+ },
+ "StepFinishPart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "step-finish"
+ },
+ "reason": {
+ "type": "string"
+ },
+ "snapshot": {
+ "type": "string"
+ },
+ "cost": {
+ "type": "number"
+ },
+ "tokens": {
+ "type": "object",
+ "properties": {
+ "input": {
+ "type": "number"
+ },
+ "output": {
+ "type": "number"
+ },
+ "reasoning": {
+ "type": "number"
+ },
+ "cache": {
+ "type": "object",
+ "properties": {
+ "read": {
+ "type": "number"
+ },
+ "write": {
+ "type": "number"
+ }
+ },
+ "required": ["read", "write"]
+ }
+ },
+ "required": ["input", "output", "reasoning", "cache"]
+ }
+ },
+ "required": ["id", "sessionID", "messageID", "type", "reason", "cost", "tokens"]
+ },
+ "SnapshotPart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "snapshot"
+ },
+ "snapshot": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "sessionID", "messageID", "type", "snapshot"]
+ },
+ "PatchPart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "patch"
+ },
+ "hash": {
+ "type": "string"
+ },
+ "files": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["id", "sessionID", "messageID", "type", "hash", "files"]
+ },
+ "AgentPart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "agent"
+ },
+ "name": {
+ "type": "string"
+ },
+ "source": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string"
+ },
+ "start": {
+ "type": "integer",
+ "minimum": -9007199254740991,
+ "maximum": 9007199254740991
+ },
+ "end": {
+ "type": "integer",
+ "minimum": -9007199254740991,
+ "maximum": 9007199254740991
+ }
+ },
+ "required": ["value", "start", "end"]
+ }
+ },
+ "required": ["id", "sessionID", "messageID", "type", "name"]
+ },
+ "RetryPart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "retry"
+ },
+ "attempt": {
+ "type": "number"
+ },
+ "error": {
+ "$ref": "#/components/schemas/APIError"
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "created": {
+ "type": "number"
+ }
+ },
+ "required": ["created"]
+ }
+ },
+ "required": ["id", "sessionID", "messageID", "type", "attempt", "error", "time"]
+ },
+ "CompactionPart": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "compaction"
+ },
+ "auto": {
+ "type": "boolean"
+ }
+ },
+ "required": ["id", "sessionID", "messageID", "type", "auto"]
+ },
+ "Part": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/TextPart"
+ },
+ {
+ "$ref": "#/components/schemas/SubtaskPart"
+ },
+ {
+ "$ref": "#/components/schemas/ReasoningPart"
+ },
+ {
+ "$ref": "#/components/schemas/FilePart"
+ },
+ {
+ "$ref": "#/components/schemas/ToolPart"
+ },
+ {
+ "$ref": "#/components/schemas/StepStartPart"
+ },
+ {
+ "$ref": "#/components/schemas/StepFinishPart"
+ },
+ {
+ "$ref": "#/components/schemas/SnapshotPart"
+ },
+ {
+ "$ref": "#/components/schemas/PatchPart"
+ },
+ {
+ "$ref": "#/components/schemas/AgentPart"
+ },
+ {
+ "$ref": "#/components/schemas/RetryPart"
+ },
+ {
+ "$ref": "#/components/schemas/CompactionPart"
+ }
+ ]
+ },
+ "Event.message.part.updated": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "message.part.updated"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "part": {
+ "$ref": "#/components/schemas/Part"
+ },
+ "delta": {
+ "type": "string"
+ }
+ },
+ "required": ["part"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.message.part.removed": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "message.part.removed"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string"
+ },
+ "partID": {
+ "type": "string"
+ }
+ },
+ "required": ["sessionID", "messageID", "partID"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "PermissionRequest": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^per.*"
+ },
+ "sessionID": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "permission": {
+ "type": "string"
+ },
+ "patterns": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "metadata": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "always": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tool": {
+ "type": "object",
+ "properties": {
+ "messageID": {
+ "type": "string"
+ },
+ "callID": {
+ "type": "string"
+ }
+ },
+ "required": ["messageID", "callID"]
+ }
+ },
+ "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"]
+ },
+ "Event.permission.asked": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "permission.asked"
+ },
+ "properties": {
+ "$ref": "#/components/schemas/PermissionRequest"
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.permission.replied": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "permission.replied"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string"
+ },
+ "requestID": {
+ "type": "string"
+ },
+ "reply": {
+ "type": "string",
+ "enum": ["once", "always", "reject"]
+ }
+ },
+ "required": ["sessionID", "requestID", "reply"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "SessionStatus": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "idle"
+ }
+ },
+ "required": ["type"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "retry"
+ },
+ "attempt": {
+ "type": "number"
+ },
+ "message": {
+ "type": "string"
+ },
+ "next": {
+ "type": "number"
+ }
+ },
+ "required": ["type", "attempt", "message", "next"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "busy"
+ }
+ },
+ "required": ["type"]
+ }
+ ]
+ },
+ "Event.session.status": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "session.status"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string"
+ },
+ "status": {
+ "$ref": "#/components/schemas/SessionStatus"
+ }
+ },
+ "required": ["sessionID", "status"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.session.idle": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "session.idle"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string"
+ }
+ },
+ "required": ["sessionID"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "QuestionOption": {
+ "type": "object",
+ "properties": {
+ "label": {
+ "description": "Display text (1-5 words, concise)",
+ "type": "string"
+ },
+ "description": {
+ "description": "Explanation of choice",
+ "type": "string"
+ }
+ },
+ "required": ["label", "description"]
+ },
+ "QuestionInfo": {
+ "type": "object",
+ "properties": {
+ "question": {
+ "description": "Complete question",
+ "type": "string"
+ },
+ "header": {
+ "description": "Very short label (max 30 chars)",
+ "type": "string"
+ },
+ "options": {
+ "description": "Available choices",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/QuestionOption"
+ }
+ },
+ "multiple": {
+ "description": "Allow selecting multiple choices",
+ "type": "boolean"
+ },
+ "custom": {
+ "description": "Allow typing a custom answer (default: true)",
+ "type": "boolean"
+ }
+ },
+ "required": ["question", "header", "options"]
+ },
+ "QuestionRequest": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^que.*"
+ },
+ "sessionID": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "questions": {
+ "description": "Questions to ask",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/QuestionInfo"
+ }
+ },
+ "tool": {
+ "type": "object",
+ "properties": {
+ "messageID": {
+ "type": "string"
+ },
+ "callID": {
+ "type": "string"
+ }
+ },
+ "required": ["messageID", "callID"]
+ }
+ },
+ "required": ["id", "sessionID", "questions"]
+ },
+ "Event.question.asked": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "question.asked"
+ },
+ "properties": {
+ "$ref": "#/components/schemas/QuestionRequest"
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "QuestionAnswer": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "Event.question.replied": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "question.replied"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string"
+ },
+ "requestID": {
+ "type": "string"
+ },
+ "answers": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/QuestionAnswer"
+ }
+ }
+ },
+ "required": ["sessionID", "requestID", "answers"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.question.rejected": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "question.rejected"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string"
+ },
+ "requestID": {
+ "type": "string"
+ }
+ },
+ "required": ["sessionID", "requestID"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.session.compacted": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "session.compacted"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string"
+ }
+ },
+ "required": ["sessionID"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.file.watcher.updated": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "file.watcher.updated"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "file": {
+ "type": "string"
+ },
+ "event": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "add"
+ },
+ {
+ "type": "string",
+ "const": "change"
+ },
+ {
+ "type": "string",
+ "const": "unlink"
+ }
+ ]
+ }
+ },
+ "required": ["file", "event"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Todo": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "Brief description of the task",
+ "type": "string"
+ },
+ "status": {
+ "description": "Current status of the task: pending, in_progress, completed, cancelled",
+ "type": "string"
+ },
+ "priority": {
+ "description": "Priority level of the task: high, medium, low",
+ "type": "string"
+ },
+ "id": {
+ "description": "Unique identifier for the todo item",
+ "type": "string"
+ }
+ },
+ "required": ["content", "status", "priority", "id"]
+ },
+ "Event.todo.updated": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "todo.updated"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string"
+ },
+ "todos": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Todo"
+ }
+ }
+ },
+ "required": ["sessionID", "todos"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.tui.prompt.append": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "tui.prompt.append"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["text"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.tui.command.execute": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "tui.command.execute"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "command": {
+ "anyOf": [
+ {
+ "type": "string",
+ "enum": [
+ "session.list",
+ "session.new",
+ "session.share",
+ "session.interrupt",
+ "session.compact",
+ "session.page.up",
+ "session.page.down",
+ "session.line.up",
+ "session.line.down",
+ "session.half.page.up",
+ "session.half.page.down",
+ "session.first",
+ "session.last",
+ "prompt.clear",
+ "prompt.submit",
+ "agent.cycle"
+ ]
+ },
+ {
+ "type": "string"
+ }
+ ]
+ }
+ },
+ "required": ["command"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.tui.toast.show": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "tui.toast.show"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "variant": {
+ "type": "string",
+ "enum": ["info", "success", "warning", "error"]
+ },
+ "duration": {
+ "description": "Duration in milliseconds",
+ "default": 5000,
+ "type": "number"
+ }
+ },
+ "required": ["message", "variant"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.tui.session.select": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "tui.session.select"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "description": "Session ID to navigate to",
+ "type": "string",
+ "pattern": "^ses"
+ }
+ },
+ "required": ["sessionID"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.mcp.tools.changed": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "mcp.tools.changed"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "server": {
+ "type": "string"
+ }
+ },
+ "required": ["server"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.mcp.browser.open.failed": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "mcp.browser.open.failed"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "mcpName": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ }
+ },
+ "required": ["mcpName", "url"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.command.executed": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "command.executed"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "sessionID": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "arguments": {
+ "type": "string"
+ },
+ "messageID": {
+ "type": "string",
+ "pattern": "^msg.*"
+ }
+ },
+ "required": ["name", "sessionID", "arguments", "messageID"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "PermissionAction": {
+ "type": "string",
+ "enum": ["allow", "deny", "ask"]
+ },
+ "PermissionRule": {
+ "type": "object",
+ "properties": {
+ "permission": {
+ "type": "string"
+ },
+ "pattern": {
+ "type": "string"
+ },
+ "action": {
+ "$ref": "#/components/schemas/PermissionAction"
+ }
+ },
+ "required": ["permission", "pattern", "action"]
+ },
+ "PermissionRuleset": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/PermissionRule"
+ }
+ },
+ "Session": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "slug": {
+ "type": "string"
+ },
+ "projectID": {
+ "type": "string"
+ },
+ "directory": {
+ "type": "string"
+ },
+ "parentID": {
+ "type": "string",
+ "pattern": "^ses.*"
+ },
+ "summary": {
+ "type": "object",
+ "properties": {
+ "additions": {
+ "type": "number"
+ },
+ "deletions": {
+ "type": "number"
+ },
+ "files": {
+ "type": "number"
+ },
+ "diffs": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FileDiff"
+ }
+ }
+ },
+ "required": ["additions", "deletions", "files"]
+ },
+ "share": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string"
+ }
+ },
+ "required": ["url"]
+ },
+ "title": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "created": {
+ "type": "number"
+ },
+ "updated": {
+ "type": "number"
+ },
+ "compacting": {
+ "type": "number"
+ },
+ "archived": {
+ "type": "number"
+ }
+ },
+ "required": ["created", "updated"]
+ },
+ "permission": {
+ "$ref": "#/components/schemas/PermissionRuleset"
+ },
+ "revert": {
+ "type": "object",
+ "properties": {
+ "messageID": {
+ "type": "string"
+ },
+ "partID": {
+ "type": "string"
+ },
+ "snapshot": {
+ "type": "string"
+ },
+ "diff": {
+ "type": "string"
+ }
+ },
+ "required": ["messageID"]
+ }
+ },
+ "required": ["id", "slug", "projectID", "directory", "title", "version", "time"]
+ },
+ "Event.session.created": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "session.created"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "info": {
+ "$ref": "#/components/schemas/Session"
+ }
+ },
+ "required": ["info"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.session.updated": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "session.updated"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "info": {
+ "$ref": "#/components/schemas/Session"
+ }
+ },
+ "required": ["info"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.session.deleted": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "session.deleted"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "info": {
+ "$ref": "#/components/schemas/Session"
+ }
+ },
+ "required": ["info"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.session.diff": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "session.diff"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string"
+ },
+ "diff": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FileDiff"
+ }
+ }
+ },
+ "required": ["sessionID", "diff"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.session.error": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "session.error"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "sessionID": {
+ "type": "string"
+ },
+ "error": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ProviderAuthError"
+ },
+ {
+ "$ref": "#/components/schemas/UnknownError"
+ },
+ {
+ "$ref": "#/components/schemas/MessageOutputLengthError"
+ },
+ {
+ "$ref": "#/components/schemas/MessageAbortedError"
+ },
+ {
+ "$ref": "#/components/schemas/APIError"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.vcs.branch.updated": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "vcs.branch.updated"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "branch": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Pty": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^pty.*"
+ },
+ "title": {
+ "type": "string"
+ },
+ "command": {
+ "type": "string"
+ },
+ "args": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "cwd": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["running", "exited"]
+ },
+ "pid": {
+ "type": "number"
+ }
+ },
+ "required": ["id", "title", "command", "args", "cwd", "status", "pid"]
+ },
+ "Event.pty.created": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "pty.created"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "info": {
+ "$ref": "#/components/schemas/Pty"
+ }
+ },
+ "required": ["info"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.pty.updated": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "pty.updated"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "info": {
+ "$ref": "#/components/schemas/Pty"
+ }
+ },
+ "required": ["info"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.pty.exited": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "pty.exited"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^pty.*"
+ },
+ "exitCode": {
+ "type": "number"
+ }
+ },
+ "required": ["id", "exitCode"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.pty.deleted": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "pty.deleted"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^pty.*"
+ }
+ },
+ "required": ["id"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.worktree.ready": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "worktree.ready"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "branch": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "branch"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event.worktree.failed": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "worktree.failed"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
+ "Event": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/Event.installation.updated"
+ },
+ {
+ "$ref": "#/components/schemas/Event.installation.update-available"
+ },
+ {
+ "$ref": "#/components/schemas/Event.project.updated"
+ },
+ {
+ "$ref": "#/components/schemas/Event.server.instance.disposed"
+ },
+ {
+ "$ref": "#/components/schemas/Event.server.connected"
+ },
+ {
+ "$ref": "#/components/schemas/Event.global.disposed"
+ },
+ {
+ "$ref": "#/components/schemas/Event.lsp.client.diagnostics"
+ },
+ {
+ "$ref": "#/components/schemas/Event.lsp.updated"
+ },
+ {
+ "$ref": "#/components/schemas/Event.file.edited"
+ },
+ {
+ "$ref": "#/components/schemas/Event.message.updated"
+ },
+ {
+ "$ref": "#/components/schemas/Event.message.removed"
+ },
+ {
+ "$ref": "#/components/schemas/Event.message.part.updated"
+ },
+ {
+ "$ref": "#/components/schemas/Event.message.part.removed"
+ },
+ {
+ "$ref": "#/components/schemas/Event.permission.asked"
+ },
+ {
+ "$ref": "#/components/schemas/Event.permission.replied"
+ },
+ {
+ "$ref": "#/components/schemas/Event.session.status"
+ },
+ {
+ "$ref": "#/components/schemas/Event.session.idle"
+ },
+ {
+ "$ref": "#/components/schemas/Event.question.asked"
+ },
+ {
+ "$ref": "#/components/schemas/Event.question.replied"
+ },
+ {
+ "$ref": "#/components/schemas/Event.question.rejected"
+ },
+ {
+ "$ref": "#/components/schemas/Event.session.compacted"
+ },
+ {
+ "$ref": "#/components/schemas/Event.file.watcher.updated"
+ },
+ {
+ "$ref": "#/components/schemas/Event.todo.updated"
+ },
+ {
+ "$ref": "#/components/schemas/Event.tui.prompt.append"
+ },
+ {
+ "$ref": "#/components/schemas/Event.tui.command.execute"
+ },
+ {
+ "$ref": "#/components/schemas/Event.tui.toast.show"
+ },
+ {
+ "$ref": "#/components/schemas/Event.tui.session.select"
+ },
+ {
+ "$ref": "#/components/schemas/Event.mcp.tools.changed"
+ },
+ {
+ "$ref": "#/components/schemas/Event.mcp.browser.open.failed"
+ },
+ {
+ "$ref": "#/components/schemas/Event.command.executed"
+ },
+ {
+ "$ref": "#/components/schemas/Event.session.created"
+ },
+ {
+ "$ref": "#/components/schemas/Event.session.updated"
+ },
+ {
+ "$ref": "#/components/schemas/Event.session.deleted"
+ },
+ {
+ "$ref": "#/components/schemas/Event.session.diff"
+ },
+ {
+ "$ref": "#/components/schemas/Event.session.error"
+ },
+ {
+ "$ref": "#/components/schemas/Event.vcs.branch.updated"
+ },
+ {
+ "$ref": "#/components/schemas/Event.pty.created"
+ },
+ {
+ "$ref": "#/components/schemas/Event.pty.updated"
+ },
+ {
+ "$ref": "#/components/schemas/Event.pty.exited"
+ },
+ {
+ "$ref": "#/components/schemas/Event.pty.deleted"
+ },
+ {
+ "$ref": "#/components/schemas/Event.worktree.ready"
+ },
+ {
+ "$ref": "#/components/schemas/Event.worktree.failed"
+ }
+ ]
+ },
+ "GlobalEvent": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ },
+ "payload": {
+ "$ref": "#/components/schemas/Event"
+ }
+ },
+ "required": ["directory", "payload"]
+ },
+ "KeybindsConfig": {
+ "description": "Custom keybind configurations",
+ "type": "object",
+ "properties": {
+ "leader": {
+ "description": "Leader key for keybind combinations",
+ "default": "ctrl+x",
+ "type": "string"
+ },
+ "app_exit": {
+ "description": "Exit the application",
+ "default": "ctrl+c,ctrl+d,q",
+ "type": "string"
+ },
+ "editor_open": {
+ "description": "Open external editor",
+ "default": "e",
+ "type": "string"
+ },
+ "theme_list": {
+ "description": "List available themes",
+ "default": "t",
+ "type": "string"
+ },
+ "sidebar_toggle": {
+ "description": "Toggle sidebar",
+ "default": "b",
+ "type": "string"
+ },
+ "scrollbar_toggle": {
+ "description": "Toggle session scrollbar",
+ "default": "none",
+ "type": "string"
+ },
+ "username_toggle": {
+ "description": "Toggle username visibility",
+ "default": "none",
+ "type": "string"
+ },
+ "status_view": {
+ "description": "View status",
+ "default": "s",
+ "type": "string"
+ },
+ "session_export": {
+ "description": "Export session to editor",
+ "default": "x",
+ "type": "string"
+ },
+ "session_new": {
+ "description": "Create a new session",
+ "default": "n",
+ "type": "string"
+ },
+ "session_list": {
+ "description": "List all sessions",
+ "default": "l",
+ "type": "string"
+ },
+ "session_timeline": {
+ "description": "Show session timeline",
+ "default": "g",
+ "type": "string"
+ },
+ "session_fork": {
+ "description": "Fork session from message",
+ "default": "none",
+ "type": "string"
+ },
+ "session_rename": {
+ "description": "Rename session",
+ "default": "ctrl+r",
+ "type": "string"
+ },
+ "session_delete": {
+ "description": "Delete session",
+ "default": "ctrl+d",
+ "type": "string"
+ },
+ "stash_delete": {
+ "description": "Delete stash entry",
+ "default": "ctrl+d",
+ "type": "string"
+ },
+ "model_provider_list": {
+ "description": "Open provider list from model dialog",
+ "default": "ctrl+a",
+ "type": "string"
+ },
+ "model_favorite_toggle": {
+ "description": "Toggle model favorite status",
+ "default": "ctrl+f",
+ "type": "string"
+ },
+ "session_share": {
+ "description": "Share current session",
+ "default": "none",
+ "type": "string"
+ },
+ "session_unshare": {
+ "description": "Unshare current session",
+ "default": "none",
+ "type": "string"
+ },
+ "session_interrupt": {
+ "description": "Interrupt current session",
+ "default": "escape",
+ "type": "string"
+ },
+ "session_compact": {
+ "description": "Compact the session",
+ "default": "c",
+ "type": "string"
+ },
+ "messages_page_up": {
+ "description": "Scroll messages up by one page",
+ "default": "pageup,ctrl+alt+b",
+ "type": "string"
+ },
+ "messages_page_down": {
+ "description": "Scroll messages down by one page",
+ "default": "pagedown,ctrl+alt+f",
+ "type": "string"
+ },
+ "messages_line_up": {
+ "description": "Scroll messages up by one line",
+ "default": "ctrl+alt+y",
+ "type": "string"
+ },
+ "messages_line_down": {
+ "description": "Scroll messages down by one line",
+ "default": "ctrl+alt+e",
+ "type": "string"
+ },
+ "messages_half_page_up": {
+ "description": "Scroll messages up by half page",
+ "default": "ctrl+alt+u",
+ "type": "string"
+ },
+ "messages_half_page_down": {
+ "description": "Scroll messages down by half page",
+ "default": "ctrl+alt+d",
+ "type": "string"
+ },
+ "messages_first": {
+ "description": "Navigate to first message",
+ "default": "ctrl+g,home",
+ "type": "string"
+ },
+ "messages_last": {
+ "description": "Navigate to last message",
+ "default": "ctrl+alt+g,end",
+ "type": "string"
+ },
+ "messages_next": {
+ "description": "Navigate to next message",
+ "default": "none",
+ "type": "string"
+ },
+ "messages_previous": {
+ "description": "Navigate to previous message",
+ "default": "none",
+ "type": "string"
+ },
+ "messages_last_user": {
+ "description": "Navigate to last user message",
+ "default": "none",
+ "type": "string"
+ },
+ "messages_copy": {
+ "description": "Copy message",
+ "default": "y",
+ "type": "string"
+ },
+ "messages_undo": {
+ "description": "Undo message",
+ "default": "u",
+ "type": "string"
+ },
+ "messages_redo": {
+ "description": "Redo message",
+ "default": "r",
+ "type": "string"
+ },
+ "messages_toggle_conceal": {
+ "description": "Toggle code block concealment in messages",
+ "default": "h",
+ "type": "string"
+ },
+ "tool_details": {
+ "description": "Toggle tool details visibility",
+ "default": "none",
+ "type": "string"
+ },
+ "model_list": {
+ "description": "List available models",
+ "default": "m",
+ "type": "string"
+ },
+ "model_cycle_recent": {
+ "description": "Next recently used model",
+ "default": "f2",
+ "type": "string"
+ },
+ "model_cycle_recent_reverse": {
+ "description": "Previous recently used model",
+ "default": "shift+f2",
+ "type": "string"
+ },
+ "model_cycle_favorite": {
+ "description": "Next favorite model",
+ "default": "none",
+ "type": "string"
+ },
+ "model_cycle_favorite_reverse": {
+ "description": "Previous favorite model",
+ "default": "none",
+ "type": "string"
+ },
+ "command_list": {
+ "description": "List available commands",
+ "default": "ctrl+p",
+ "type": "string"
+ },
+ "agent_list": {
+ "description": "List agents",
+ "default": "a",
+ "type": "string"
+ },
+ "agent_cycle": {
+ "description": "Next agent",
+ "default": "tab",
+ "type": "string"
+ },
+ "agent_cycle_reverse": {
+ "description": "Previous agent",
+ "default": "shift+tab",
+ "type": "string"
+ },
+ "variant_cycle": {
+ "description": "Cycle model variants",
+ "default": "ctrl+t",
+ "type": "string"
+ },
+ "input_clear": {
+ "description": "Clear input field",
+ "default": "ctrl+c",
+ "type": "string"
+ },
+ "input_paste": {
+ "description": "Paste from clipboard",
+ "default": "ctrl+v",
+ "type": "string"
+ },
+ "input_submit": {
+ "description": "Submit input",
+ "default": "return",
+ "type": "string"
+ },
+ "input_newline": {
+ "description": "Insert newline in input",
+ "default": "shift+return,ctrl+return,alt+return,ctrl+j",
+ "type": "string"
+ },
+ "input_move_left": {
+ "description": "Move cursor left in input",
+ "default": "left,ctrl+b",
+ "type": "string"
+ },
+ "input_move_right": {
+ "description": "Move cursor right in input",
+ "default": "right,ctrl+f",
+ "type": "string"
+ },
+ "input_move_up": {
+ "description": "Move cursor up in input",
+ "default": "up",
+ "type": "string"
+ },
+ "input_move_down": {
+ "description": "Move cursor down in input",
+ "default": "down",
+ "type": "string"
+ },
+ "input_select_left": {
+ "description": "Select left in input",
+ "default": "shift+left",
+ "type": "string"
+ },
+ "input_select_right": {
+ "description": "Select right in input",
+ "default": "shift+right",
+ "type": "string"
+ },
+ "input_select_up": {
+ "description": "Select up in input",
+ "default": "shift+up",
+ "type": "string"
+ },
+ "input_select_down": {
+ "description": "Select down in input",
+ "default": "shift+down",
+ "type": "string"
+ },
+ "input_line_home": {
+ "description": "Move to start of line in input",
+ "default": "ctrl+a",
+ "type": "string"
+ },
+ "input_line_end": {
+ "description": "Move to end of line in input",
+ "default": "ctrl+e",
+ "type": "string"
+ },
+ "input_select_line_home": {
+ "description": "Select to start of line in input",
+ "default": "ctrl+shift+a",
+ "type": "string"
+ },
+ "input_select_line_end": {
+ "description": "Select to end of line in input",
+ "default": "ctrl+shift+e",
+ "type": "string"
+ },
+ "input_visual_line_home": {
+ "description": "Move to start of visual line in input",
+ "default": "alt+a",
+ "type": "string"
+ },
+ "input_visual_line_end": {
+ "description": "Move to end of visual line in input",
+ "default": "alt+e",
+ "type": "string"
+ },
+ "input_select_visual_line_home": {
+ "description": "Select to start of visual line in input",
+ "default": "alt+shift+a",
+ "type": "string"
+ },
+ "input_select_visual_line_end": {
+ "description": "Select to end of visual line in input",
+ "default": "alt+shift+e",
+ "type": "string"
+ },
+ "input_buffer_home": {
+ "description": "Move to start of buffer in input",
+ "default": "home",
+ "type": "string"
+ },
+ "input_buffer_end": {
+ "description": "Move to end of buffer in input",
+ "default": "end",
+ "type": "string"
+ },
+ "input_select_buffer_home": {
+ "description": "Select to start of buffer in input",
+ "default": "shift+home",
+ "type": "string"
+ },
+ "input_select_buffer_end": {
+ "description": "Select to end of buffer in input",
+ "default": "shift+end",
+ "type": "string"
+ },
+ "input_delete_line": {
+ "description": "Delete line in input",
+ "default": "ctrl+shift+d",
+ "type": "string"
+ },
+ "input_delete_to_line_end": {
+ "description": "Delete to end of line in input",
+ "default": "ctrl+k",
+ "type": "string"
+ },
+ "input_delete_to_line_start": {
+ "description": "Delete to start of line in input",
+ "default": "ctrl+u",
+ "type": "string"
+ },
+ "input_backspace": {
+ "description": "Backspace in input",
+ "default": "backspace,shift+backspace",
+ "type": "string"
+ },
+ "input_delete": {
+ "description": "Delete character in input",
+ "default": "ctrl+d,delete,shift+delete",
+ "type": "string"
+ },
+ "input_undo": {
+ "description": "Undo in input",
+ "default": "ctrl+-,super+z",
+ "type": "string"
+ },
+ "input_redo": {
+ "description": "Redo in input",
+ "default": "ctrl+.,super+shift+z",
+ "type": "string"
+ },
+ "input_word_forward": {
+ "description": "Move word forward in input",
+ "default": "alt+f,alt+right,ctrl+right",
+ "type": "string"
+ },
+ "input_word_backward": {
+ "description": "Move word backward in input",
+ "default": "alt+b,alt+left,ctrl+left",
+ "type": "string"
+ },
+ "input_select_word_forward": {
+ "description": "Select word forward in input",
+ "default": "alt+shift+f,alt+shift+right",
+ "type": "string"
+ },
+ "input_select_word_backward": {
+ "description": "Select word backward in input",
+ "default": "alt+shift+b,alt+shift+left",
+ "type": "string"
+ },
+ "input_delete_word_forward": {
+ "description": "Delete word forward in input",
+ "default": "alt+d,alt+delete,ctrl+delete",
+ "type": "string"
+ },
+ "input_delete_word_backward": {
+ "description": "Delete word backward in input",
+ "default": "ctrl+w,ctrl+backspace,alt+backspace",
+ "type": "string"
+ },
+ "history_previous": {
+ "description": "Previous history item",
+ "default": "up",
+ "type": "string"
+ },
+ "history_next": {
+ "description": "Next history item",
+ "default": "down",
+ "type": "string"
+ },
+ "session_child_cycle": {
+ "description": "Next child session",
+ "default": "right",
+ "type": "string"
+ },
+ "session_child_cycle_reverse": {
+ "description": "Previous child session",
+ "default": "left",
+ "type": "string"
+ },
+ "session_parent": {
+ "description": "Go to parent session",
+ "default": "up",
+ "type": "string"
+ },
+ "terminal_suspend": {
+ "description": "Suspend terminal",
+ "default": "ctrl+z",
+ "type": "string"
+ },
+ "terminal_title_toggle": {
+ "description": "Toggle terminal title",
+ "default": "none",
+ "type": "string"
+ },
+ "tips_toggle": {
+ "description": "Toggle tips on home screen",
+ "default": "h",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "LogLevel": {
+ "description": "Log level",
+ "type": "string",
+ "enum": ["DEBUG", "INFO", "WARN", "ERROR"]
+ },
+ "ServerConfig": {
+ "description": "Server configuration for opencode serve and web commands",
+ "type": "object",
+ "properties": {
+ "port": {
+ "description": "Port to listen on",
+ "type": "integer",
+ "exclusiveMinimum": 0,
+ "maximum": 9007199254740991
+ },
+ "hostname": {
+ "description": "Hostname to listen on",
+ "type": "string"
+ },
+ "mdns": {
+ "description": "Enable mDNS service discovery",
+ "type": "boolean"
+ },
+ "mdnsDomain": {
+ "description": "Custom domain name for mDNS service (default: opencode.local)",
+ "type": "string"
+ },
+ "cors": {
+ "description": "Additional domains to allow for CORS",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "PermissionActionConfig": {
+ "type": "string",
+ "enum": ["ask", "allow", "deny"]
+ },
+ "PermissionObjectConfig": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/PermissionActionConfig"
+ }
+ },
+ "PermissionRuleConfig": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/PermissionActionConfig"
+ },
+ {
+ "$ref": "#/components/schemas/PermissionObjectConfig"
+ }
+ ]
+ },
+ "PermissionConfig": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "__originalKeys": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "read": {
+ "$ref": "#/components/schemas/PermissionRuleConfig"
+ },
+ "edit": {
+ "$ref": "#/components/schemas/PermissionRuleConfig"
+ },
+ "glob": {
+ "$ref": "#/components/schemas/PermissionRuleConfig"
+ },
+ "grep": {
+ "$ref": "#/components/schemas/PermissionRuleConfig"
+ },
+ "list": {
+ "$ref": "#/components/schemas/PermissionRuleConfig"
+ },
+ "bash": {
+ "$ref": "#/components/schemas/PermissionRuleConfig"
+ },
+ "task": {
+ "$ref": "#/components/schemas/PermissionRuleConfig"
+ },
+ "external_directory": {
+ "$ref": "#/components/schemas/PermissionRuleConfig"
+ },
+ "todowrite": {
+ "$ref": "#/components/schemas/PermissionActionConfig"
+ },
+ "todoread": {
+ "$ref": "#/components/schemas/PermissionActionConfig"
+ },
+ "question": {
+ "$ref": "#/components/schemas/PermissionActionConfig"
+ },
+ "webfetch": {
+ "$ref": "#/components/schemas/PermissionActionConfig"
+ },
+ "websearch": {
+ "$ref": "#/components/schemas/PermissionActionConfig"
+ },
+ "codesearch": {
+ "$ref": "#/components/schemas/PermissionActionConfig"
+ },
+ "lsp": {
+ "$ref": "#/components/schemas/PermissionRuleConfig"
+ },
+ "doom_loop": {
+ "$ref": "#/components/schemas/PermissionActionConfig"
+ },
+ "skill": {
+ "$ref": "#/components/schemas/PermissionRuleConfig"
+ }
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/PermissionRuleConfig"
+ }
+ },
+ {
+ "$ref": "#/components/schemas/PermissionActionConfig"
+ }
+ ]
+ },
+ "AgentConfig": {
+ "type": "object",
+ "properties": {
+ "model": {
+ "type": "string"
+ },
+ "variant": {
+ "description": "Default model variant for this agent (applies only when using the agent's configured model).",
+ "type": "string"
+ },
+ "temperature": {
+ "type": "number"
+ },
+ "top_p": {
+ "type": "number"
+ },
+ "prompt": {
+ "type": "string"
+ },
+ "tools": {
+ "description": "@deprecated Use 'permission' field instead",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "boolean"
+ }
+ },
+ "disable": {
+ "type": "boolean"
+ },
+ "description": {
+ "description": "Description of when to use the agent",
+ "type": "string"
+ },
+ "mode": {
+ "type": "string",
+ "enum": ["subagent", "primary", "all"]
+ },
+ "hidden": {
+ "description": "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)",
+ "type": "boolean"
+ },
+ "options": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "color": {
+ "description": "Hex color code (e.g., #FF5733) or theme color (e.g., primary)",
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^#[0-9a-fA-F]{6}$"
+ },
+ {
+ "type": "string",
+ "enum": ["primary", "secondary", "accent", "success", "warning", "error", "info"]
+ }
+ ]
+ },
+ "steps": {
+ "description": "Maximum number of agentic iterations before forcing text-only response",
+ "type": "integer",
+ "exclusiveMinimum": 0,
+ "maximum": 9007199254740991
+ },
+ "maxSteps": {
+ "description": "@deprecated Use 'steps' field instead.",
+ "type": "integer",
+ "exclusiveMinimum": 0,
+ "maximum": 9007199254740991
+ },
+ "permission": {
+ "$ref": "#/components/schemas/PermissionConfig"
+ }
+ },
+ "additionalProperties": {}
+ },
+ "ProviderConfig": {
+ "type": "object",
+ "properties": {
+ "api": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "env": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "id": {
+ "type": "string"
+ },
+ "npm": {
+ "type": "string"
+ },
+ "models": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "family": {
+ "type": "string"
+ },
+ "release_date": {
+ "type": "string"
+ },
+ "attachment": {
+ "type": "boolean"
+ },
+ "reasoning": {
+ "type": "boolean"
+ },
+ "temperature": {
+ "type": "boolean"
+ },
+ "tool_call": {
+ "type": "boolean"
+ },
+ "interleaved": {
+ "anyOf": [
+ {
+ "type": "boolean",
+ "const": true
+ },
+ {
+ "type": "object",
+ "properties": {
+ "field": {
+ "type": "string",
+ "enum": ["reasoning_content", "reasoning_details"]
+ }
+ },
+ "required": ["field"],
+ "additionalProperties": false
+ }
+ ]
+ },
+ "cost": {
+ "type": "object",
+ "properties": {
+ "input": {
+ "type": "number"
+ },
+ "output": {
+ "type": "number"
+ },
+ "cache_read": {
+ "type": "number"
+ },
+ "cache_write": {
+ "type": "number"
+ },
+ "context_over_200k": {
+ "type": "object",
+ "properties": {
+ "input": {
+ "type": "number"
+ },
+ "output": {
+ "type": "number"
+ },
+ "cache_read": {
+ "type": "number"
+ },
+ "cache_write": {
+ "type": "number"
+ }
+ },
+ "required": ["input", "output"]
+ }
+ },
+ "required": ["input", "output"]
+ },
+ "limit": {
+ "type": "object",
+ "properties": {
+ "context": {
+ "type": "number"
+ },
+ "input": {
+ "type": "number"
+ },
+ "output": {
+ "type": "number"
+ }
+ },
+ "required": ["context", "output"]
+ },
+ "modalities": {
+ "type": "object",
+ "properties": {
+ "input": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["text", "audio", "image", "video", "pdf"]
+ }
+ },
+ "output": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["text", "audio", "image", "video", "pdf"]
+ }
+ }
+ },
+ "required": ["input", "output"]
+ },
+ "experimental": {
+ "type": "boolean"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["alpha", "beta", "deprecated"]
+ },
+ "options": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "headers": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "provider": {
+ "type": "object",
+ "properties": {
+ "npm": {
+ "type": "string"
+ }
+ },
+ "required": ["npm"]
+ },
+ "variants": {
+ "description": "Variant-specific configuration",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "disabled": {
+ "description": "Disable this variant for the model",
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": {}
+ }
+ }
+ }
+ }
+ },
+ "whitelist": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "blacklist": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "options": {
+ "type": "object",
+ "properties": {
+ "apiKey": {
+ "type": "string"
+ },
+ "baseURL": {
+ "type": "string"
+ },
+ "enterpriseUrl": {
+ "description": "GitHub Enterprise URL for copilot authentication",
+ "type": "string"
+ },
+ "setCacheKey": {
+ "description": "Enable promptCacheKey for this provider (default false)",
+ "type": "boolean"
+ },
+ "timeout": {
+ "description": "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
+ "anyOf": [
+ {
+ "description": "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
+ "type": "integer",
+ "exclusiveMinimum": 0,
+ "maximum": 9007199254740991
+ },
+ {
+ "description": "Disable timeout for this provider entirely.",
+ "type": "boolean",
+ "const": false
+ }
+ ]
+ }
+ },
+ "additionalProperties": {}
+ }
+ },
+ "additionalProperties": false
+ },
+ "McpLocalConfig": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "description": "Type of MCP server connection",
+ "type": "string",
+ "const": "local"
+ },
+ "command": {
+ "description": "Command and arguments to run the MCP server",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "environment": {
+ "description": "Environment variables to set when running the MCP server",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "enabled": {
+ "description": "Enable or disable the MCP server on startup",
+ "type": "boolean"
+ },
+ "timeout": {
+ "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
+ "type": "integer",
+ "exclusiveMinimum": 0,
+ "maximum": 9007199254740991
+ }
+ },
+ "required": ["type", "command"],
+ "additionalProperties": false
+ },
+ "McpOAuthConfig": {
+ "type": "object",
+ "properties": {
+ "clientId": {
+ "description": "OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted.",
+ "type": "string"
+ },
+ "clientSecret": {
+ "description": "OAuth client secret (if required by the authorization server)",
+ "type": "string"
+ },
+ "scope": {
+ "description": "OAuth scopes to request during authorization",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "McpRemoteConfig": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "description": "Type of MCP server connection",
+ "type": "string",
+ "const": "remote"
+ },
+ "url": {
+ "description": "URL of the remote MCP server",
+ "type": "string"
+ },
+ "enabled": {
+ "description": "Enable or disable the MCP server on startup",
+ "type": "boolean"
+ },
+ "headers": {
+ "description": "Headers to send with the request",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "oauth": {
+ "description": "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.",
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/McpOAuthConfig"
+ },
+ {
+ "type": "boolean",
+ "const": false
+ }
+ ]
+ },
+ "timeout": {
+ "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
+ "type": "integer",
+ "exclusiveMinimum": 0,
+ "maximum": 9007199254740991
+ }
+ },
+ "required": ["type", "url"],
+ "additionalProperties": false
+ },
+ "LayoutConfig": {
+ "description": "@deprecated Always uses stretch layout.",
+ "type": "string",
+ "enum": ["auto", "stretch"]
+ },
+ "Config": {
+ "type": "object",
+ "properties": {
+ "$schema": {
+ "description": "JSON schema reference for configuration validation",
+ "type": "string"
+ },
+ "theme": {
+ "description": "Theme name to use for the interface",
+ "type": "string"
+ },
+ "keybinds": {
+ "$ref": "#/components/schemas/KeybindsConfig"
+ },
+ "logLevel": {
+ "$ref": "#/components/schemas/LogLevel"
+ },
+ "tui": {
+ "description": "TUI specific settings",
+ "type": "object",
+ "properties": {
+ "scroll_speed": {
+ "description": "TUI scroll speed",
+ "type": "number",
+ "minimum": 0.001
+ },
+ "scroll_acceleration": {
+ "description": "Scroll acceleration settings",
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "description": "Enable scroll acceleration",
+ "type": "boolean"
+ }
+ },
+ "required": ["enabled"]
+ },
+ "diff_style": {
+ "description": "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column",
+ "type": "string",
+ "enum": ["auto", "stacked"]
+ }
+ }
+ },
+ "server": {
+ "$ref": "#/components/schemas/ServerConfig"
+ },
+ "command": {
+ "description": "Command configuration, see https://opencode.ai/docs/commands",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "agent": {
+ "type": "string"
+ },
+ "model": {
+ "type": "string"
+ },
+ "subtask": {
+ "type": "boolean"
+ }
+ },
+ "required": ["template"]
+ }
+ },
+ "skills": {
+ "description": "Additional skill folder paths",
+ "type": "object",
+ "properties": {
+ "paths": {
+ "description": "Additional paths to skill folders",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "watcher": {
+ "type": "object",
+ "properties": {
+ "ignore": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "plugin": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "snapshot": {
+ "type": "boolean"
+ },
+ "share": {
+ "description": "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
+ "type": "string",
+ "enum": ["manual", "auto", "disabled"]
+ },
+ "autoshare": {
+ "description": "@deprecated Use 'share' field instead. Share newly created sessions automatically",
+ "type": "boolean"
+ },
+ "autoupdate": {
+ "description": "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "string",
+ "const": "notify"
+ }
+ ]
+ },
+ "disabled_providers": {
+ "description": "Disable providers that are loaded automatically",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "enabled_providers": {
+ "description": "When set, ONLY these providers will be enabled. All other providers will be ignored",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "model": {
+ "description": "Model to use in the format of provider/model, eg anthropic/claude-2",
+ "type": "string"
+ },
+ "small_model": {
+ "description": "Small model to use for tasks like title generation in the format of provider/model",
+ "type": "string"
+ },
+ "default_agent": {
+ "description": "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
+ "type": "string"
+ },
+ "username": {
+ "description": "Custom username to display in conversations instead of system username",
+ "type": "string"
+ },
+ "mode": {
+ "description": "@deprecated Use `agent` field instead.",
+ "type": "object",
+ "properties": {
+ "build": {
+ "$ref": "#/components/schemas/AgentConfig"
+ },
+ "plan": {
+ "$ref": "#/components/schemas/AgentConfig"
+ }
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/AgentConfig"
+ }
+ },
+ "agent": {
+ "description": "Agent configuration, see https://opencode.ai/docs/agents",
+ "type": "object",
+ "properties": {
+ "plan": {
+ "$ref": "#/components/schemas/AgentConfig"
+ },
+ "build": {
+ "$ref": "#/components/schemas/AgentConfig"
+ },
+ "general": {
+ "$ref": "#/components/schemas/AgentConfig"
+ },
+ "explore": {
+ "$ref": "#/components/schemas/AgentConfig"
+ },
+ "title": {
+ "$ref": "#/components/schemas/AgentConfig"
+ },
+ "summary": {
+ "$ref": "#/components/schemas/AgentConfig"
+ },
+ "compaction": {
+ "$ref": "#/components/schemas/AgentConfig"
+ }
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/AgentConfig"
+ }
+ },
+ "provider": {
+ "description": "Custom provider configurations and model overrides",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/ProviderConfig"
+ }
+ },
+ "mcp": {
+ "description": "MCP (Model Context Protocol) server configurations",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/McpLocalConfig"
+ },
+ {
+ "$ref": "#/components/schemas/McpRemoteConfig"
+ }
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ },
+ "required": ["enabled"],
+ "additionalProperties": false
+ }
+ ]
+ }
+ },
+ "formatter": {
+ "anyOf": [
+ {
+ "type": "boolean",
+ "const": false
+ },
+ {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "disabled": {
+ "type": "boolean"
+ },
+ "command": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "environment": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "extensions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ ]
+ },
+ "lsp": {
+ "anyOf": [
+ {
+ "type": "boolean",
+ "const": false
+ },
+ {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "disabled": {
+ "type": "boolean",
+ "const": true
+ }
+ },
+ "required": ["disabled"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "extensions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "disabled": {
+ "type": "boolean"
+ },
+ "env": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "initialization": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ }
+ },
+ "required": ["command"]
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "instructions": {
+ "description": "Additional instruction files or patterns to include",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "layout": {
+ "$ref": "#/components/schemas/LayoutConfig"
+ },
+ "permission": {
+ "$ref": "#/components/schemas/PermissionConfig"
+ },
+ "tools": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "boolean"
+ }
+ },
+ "enterprise": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "description": "Enterprise URL",
+ "type": "string"
+ }
+ }
+ },
+ "compaction": {
+ "type": "object",
+ "properties": {
+ "auto": {
+ "description": "Enable automatic compaction when context is full (default: true)",
+ "type": "boolean"
+ },
+ "prune": {
+ "description": "Enable pruning of old tool outputs (default: true)",
+ "type": "boolean"
+ }
+ }
+ },
+ "experimental": {
+ "type": "object",
+ "properties": {
+ "disable_paste_summary": {
+ "type": "boolean"
+ },
+ "batch_tool": {
+ "description": "Enable the batch tool",
+ "type": "boolean"
+ },
+ "openTelemetry": {
+ "description": "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)",
+ "type": "boolean"
+ },
+ "primary_tools": {
+ "description": "Tools that should only be available to primary agents.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "continue_loop_on_deny": {
+ "description": "Continue the agent loop when a tool call is denied",
+ "type": "boolean"
+ },
+ "mcp_timeout": {
+ "description": "Timeout in milliseconds for model context protocol (MCP) requests",
+ "type": "integer",
+ "exclusiveMinimum": 0,
+ "maximum": 9007199254740991
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "BadRequestError": {
+ "type": "object",
+ "properties": {
+ "data": {},
+ "errors": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ }
+ },
+ "success": {
+ "type": "boolean",
+ "const": false
+ }
+ },
+ "required": ["data", "errors", "success"]
+ },
+ "OAuth": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "oauth"
+ },
+ "refresh": {
+ "type": "string"
+ },
+ "access": {
+ "type": "string"
+ },
+ "expires": {
+ "type": "number"
+ },
+ "accountId": {
+ "type": "string"
+ },
+ "enterpriseUrl": {
+ "type": "string"
+ }
+ },
+ "required": ["type", "refresh", "access", "expires"]
+ },
+ "ApiAuth": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "api"
+ },
+ "key": {
+ "type": "string"
+ }
+ },
+ "required": ["type", "key"]
+ },
+ "WellKnownAuth": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "wellknown"
+ },
+ "key": {
+ "type": "string"
+ },
+ "token": {
+ "type": "string"
+ }
+ },
+ "required": ["type", "key", "token"]
+ },
+ "Auth": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/OAuth"
+ },
+ {
+ "$ref": "#/components/schemas/ApiAuth"
+ },
+ {
+ "$ref": "#/components/schemas/WellKnownAuth"
+ }
+ ]
+ },
+ "NotFoundError": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "const": "NotFoundError"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"]
+ }
+ },
+ "required": ["name", "data"]
+ },
+ "Model": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "providerID": {
+ "type": "string"
+ },
+ "api": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "npm": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "url", "npm"]
+ },
+ "name": {
+ "type": "string"
+ },
+ "family": {
+ "type": "string"
+ },
+ "capabilities": {
+ "type": "object",
+ "properties": {
+ "temperature": {
+ "type": "boolean"
+ },
+ "reasoning": {
+ "type": "boolean"
+ },
+ "attachment": {
+ "type": "boolean"
+ },
+ "toolcall": {
+ "type": "boolean"
+ },
+ "input": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "boolean"
+ },
+ "audio": {
+ "type": "boolean"
+ },
+ "image": {
+ "type": "boolean"
+ },
+ "video": {
+ "type": "boolean"
+ },
+ "pdf": {
+ "type": "boolean"
+ }
+ },
+ "required": ["text", "audio", "image", "video", "pdf"]
+ },
+ "output": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "boolean"
+ },
+ "audio": {
+ "type": "boolean"
+ },
+ "image": {
+ "type": "boolean"
+ },
+ "video": {
+ "type": "boolean"
+ },
+ "pdf": {
+ "type": "boolean"
+ }
+ },
+ "required": ["text", "audio", "image", "video", "pdf"]
+ },
+ "interleaved": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "field": {
+ "type": "string",
+ "enum": ["reasoning_content", "reasoning_details"]
+ }
+ },
+ "required": ["field"]
+ }
+ ]
+ }
+ },
+ "required": ["temperature", "reasoning", "attachment", "toolcall", "input", "output", "interleaved"]
+ },
+ "cost": {
+ "type": "object",
+ "properties": {
+ "input": {
+ "type": "number"
+ },
+ "output": {
+ "type": "number"
+ },
+ "cache": {
+ "type": "object",
+ "properties": {
+ "read": {
+ "type": "number"
+ },
+ "write": {
+ "type": "number"
+ }
+ },
+ "required": ["read", "write"]
+ },
+ "experimentalOver200K": {
+ "type": "object",
+ "properties": {
+ "input": {
+ "type": "number"
+ },
+ "output": {
+ "type": "number"
+ },
+ "cache": {
+ "type": "object",
+ "properties": {
+ "read": {
+ "type": "number"
+ },
+ "write": {
+ "type": "number"
+ }
+ },
+ "required": ["read", "write"]
+ }
+ },
+ "required": ["input", "output", "cache"]
+ }
+ },
+ "required": ["input", "output", "cache"]
+ },
+ "limit": {
+ "type": "object",
+ "properties": {
+ "context": {
+ "type": "number"
+ },
+ "input": {
+ "type": "number"
+ },
+ "output": {
+ "type": "number"
+ }
+ },
+ "required": ["context", "output"]
+ },
+ "status": {
+ "type": "string",
+ "enum": ["alpha", "beta", "deprecated", "active"]
+ },
+ "options": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "headers": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "release_date": {
+ "type": "string"
+ },
+ "variants": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ }
+ }
+ },
+ "required": [
+ "id",
+ "providerID",
+ "api",
+ "name",
+ "capabilities",
+ "cost",
+ "limit",
+ "status",
+ "options",
+ "headers",
+ "release_date"
+ ]
+ },
+ "Provider": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string",
+ "enum": ["env", "config", "custom", "api"]
+ },
+ "env": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "key": {
+ "type": "string"
+ },
+ "options": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "models": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/Model"
+ }
+ }
+ },
+ "required": ["id", "name", "source", "env", "options", "models"]
+ },
+ "ToolIDs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "ToolListItem": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "parameters": {}
+ },
+ "required": ["id", "description", "parameters"]
+ },
+ "ToolList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ToolListItem"
+ }
+ },
+ "Worktree": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "branch": {
+ "type": "string"
+ },
+ "directory": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "branch", "directory"]
+ },
+ "WorktreeCreateInput": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "startCommand": {
+ "description": "Additional startup script to run after the project's start command",
+ "type": "string"
+ }
+ }
+ },
+ "WorktreeRemoveInput": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ }
+ },
+ "required": ["directory"]
+ },
+ "WorktreeResetInput": {
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string"
+ }
+ },
+ "required": ["directory"]
+ },
+ "McpResource": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "uri": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "mimeType": {
+ "type": "string"
+ },
+ "client": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "uri", "client"]
+ },
+ "TextPartInput": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "text"
+ },
+ "text": {
+ "type": "string"
+ },
+ "synthetic": {
+ "type": "boolean"
+ },
+ "ignored": {
+ "type": "boolean"
+ },
+ "time": {
+ "type": "object",
+ "properties": {
+ "start": {
+ "type": "number"
+ },
+ "end": {
+ "type": "number"
+ }
+ },
+ "required": ["start"]
+ },
+ "metadata": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ }
+ },
+ "required": ["type", "text"]
+ },
+ "FilePartInput": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "file"
+ },
+ "mime": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "source": {
+ "$ref": "#/components/schemas/FilePartSource"
+ }
+ },
+ "required": ["type", "mime", "url"]
+ },
+ "AgentPartInput": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "agent"
+ },
+ "name": {
+ "type": "string"
+ },
+ "source": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string"
+ },
+ "start": {
+ "type": "integer",
+ "minimum": -9007199254740991,
+ "maximum": 9007199254740991
+ },
+ "end": {
+ "type": "integer",
+ "minimum": -9007199254740991,
+ "maximum": 9007199254740991
+ }
+ },
+ "required": ["value", "start", "end"]
+ }
+ },
+ "required": ["type", "name"]
+ },
+ "SubtaskPartInput": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "subtask"
+ },
+ "prompt": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "agent": {
+ "type": "string"
+ },
+ "model": {
+ "type": "object",
+ "properties": {
+ "providerID": {
+ "type": "string"
+ },
+ "modelID": {
+ "type": "string"
+ }
+ },
+ "required": ["providerID", "modelID"]
+ },
+ "command": {
+ "type": "string"
+ }
+ },
+ "required": ["type", "prompt", "description", "agent"]
+ },
+ "ProviderAuthMethod": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "oauth"
+ },
+ {
+ "type": "string",
+ "const": "api"
+ }
+ ]
+ },
+ "label": {
+ "type": "string"
+ }
+ },
+ "required": ["type", "label"]
+ },
+ "ProviderAuthAuthorization": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "method": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "auto"
+ },
+ {
+ "type": "string",
+ "const": "code"
+ }
+ ]
+ },
+ "instructions": {
+ "type": "string"
+ }
+ },
+ "required": ["url", "method", "instructions"]
+ },
+ "Symbol": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "number"
+ },
+ "location": {
+ "type": "object",
+ "properties": {
+ "uri": {
+ "type": "string"
+ },
+ "range": {
+ "$ref": "#/components/schemas/Range"
+ }
+ },
+ "required": ["uri", "range"]
+ }
+ },
+ "required": ["name", "kind", "location"]
+ },
+ "FileNode": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "path": {
+ "type": "string"
+ },
+ "absolute": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["file", "directory"]
+ },
+ "ignored": {
+ "type": "boolean"
+ }
+ },
+ "required": ["name", "path", "absolute", "type", "ignored"]
+ },
+ "FileContent": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["text", "binary"]
+ },
+ "content": {
+ "type": "string"
+ },
+ "diff": {
+ "type": "string"
+ },
+ "patch": {
+ "type": "object",
+ "properties": {
+ "oldFileName": {
+ "type": "string"
+ },
+ "newFileName": {
+ "type": "string"
+ },
+ "oldHeader": {
+ "type": "string"
+ },
+ "newHeader": {
+ "type": "string"
+ },
+ "hunks": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "oldStart": {
+ "type": "number"
+ },
+ "oldLines": {
+ "type": "number"
+ },
+ "newStart": {
+ "type": "number"
+ },
+ "newLines": {
+ "type": "number"
+ },
+ "lines": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["oldStart", "oldLines", "newStart", "newLines", "lines"]
+ }
+ },
+ "index": {
+ "type": "string"
+ }
+ },
+ "required": ["oldFileName", "newFileName", "hunks"]
+ },
+ "encoding": {
+ "type": "string",
+ "const": "base64"
+ },
+ "mimeType": {
+ "type": "string"
+ }
+ },
+ "required": ["type", "content"]
+ },
+ "File": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string"
+ },
+ "added": {
+ "type": "integer",
+ "minimum": -9007199254740991,
+ "maximum": 9007199254740991
+ },
+ "removed": {
+ "type": "integer",
+ "minimum": -9007199254740991,
+ "maximum": 9007199254740991
+ },
+ "status": {
+ "type": "string",
+ "enum": ["added", "deleted", "modified"]
+ }
+ },
+ "required": ["path", "added", "removed", "status"]
+ },
+ "MCPStatusConnected": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "connected"
+ }
+ },
+ "required": ["status"]
+ },
+ "MCPStatusDisabled": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "disabled"
+ }
+ },
+ "required": ["status"]
+ },
+ "MCPStatusFailed": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "failed"
+ },
+ "error": {
+ "type": "string"
+ }
+ },
+ "required": ["status", "error"]
+ },
+ "MCPStatusNeedsAuth": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "needs_auth"
+ }
+ },
+ "required": ["status"]
+ },
+ "MCPStatusNeedsClientRegistration": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "needs_client_registration"
+ },
+ "error": {
+ "type": "string"
+ }
+ },
+ "required": ["status", "error"]
+ },
+ "MCPStatus": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/MCPStatusConnected"
+ },
+ {
+ "$ref": "#/components/schemas/MCPStatusDisabled"
+ },
+ {
+ "$ref": "#/components/schemas/MCPStatusFailed"
+ },
+ {
+ "$ref": "#/components/schemas/MCPStatusNeedsAuth"
+ },
+ {
+ "$ref": "#/components/schemas/MCPStatusNeedsClientRegistration"
+ }
+ ]
+ },
+ "Path": {
+ "type": "object",
+ "properties": {
+ "home": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ },
+ "config": {
+ "type": "string"
+ },
+ "worktree": {
+ "type": "string"
+ },
+ "directory": {
+ "type": "string"
+ }
+ },
+ "required": ["home", "state", "config", "worktree", "directory"]
+ },
+ "VcsInfo": {
+ "type": "object",
+ "properties": {
+ "branch": {
+ "type": "string"
+ }
+ },
+ "required": ["branch"]
+ },
+ "Command": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "agent": {
+ "type": "string"
+ },
+ "model": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string",
+ "enum": ["command", "mcp", "skill"]
+ },
+ "template": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "subtask": {
+ "type": "boolean"
+ },
+ "hints": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["name", "template", "hints"]
+ },
+ "Agent": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "mode": {
+ "type": "string",
+ "enum": ["subagent", "primary", "all"]
+ },
+ "native": {
+ "type": "boolean"
+ },
+ "hidden": {
+ "type": "boolean"
+ },
+ "topP": {
+ "type": "number"
+ },
+ "temperature": {
+ "type": "number"
+ },
+ "color": {
+ "type": "string"
+ },
+ "permission": {
+ "$ref": "#/components/schemas/PermissionRuleset"
+ },
+ "model": {
+ "type": "object",
+ "properties": {
+ "modelID": {
+ "type": "string"
+ },
+ "providerID": {
+ "type": "string"
+ }
+ },
+ "required": ["modelID", "providerID"]
+ },
+ "variant": {
+ "type": "string"
+ },
+ "prompt": {
+ "type": "string"
+ },
+ "options": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "steps": {
+ "type": "integer",
+ "exclusiveMinimum": 0,
+ "maximum": 9007199254740991
+ }
+ },
+ "required": ["name", "mode", "permission", "options"]
+ },
+ "LSPStatus": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "root": {
+ "type": "string"
+ },
+ "status": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "connected"
+ },
+ {
+ "type": "string",
+ "const": "error"
+ }
+ ]
+ }
+ },
+ "required": ["id", "name", "root", "status"]
+ },
+ "FormatterStatus": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "extensions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "enabled": {
+ "type": "boolean"
+ }
+ },
+ "required": ["name", "extensions", "enabled"]
+ }
+ }
+ }
+}
diff --git a/resources/agent-schemas/src/opencode.ts b/resources/agent-schemas/src/opencode.ts
index 6f1e6a0..d10c329 100644
--- a/resources/agent-schemas/src/opencode.ts
+++ b/resources/agent-schemas/src/opencode.ts
@@ -1,9 +1,13 @@
+import { existsSync, mkdirSync, writeFileSync } from "fs";
+import { join } from "path";
import { fetchWithCache } from "./cache.js";
import { createNormalizedSchema, openApiToJsonSchema, type NormalizedSchema } from "./normalize.js";
import type { JSONSchema7 } from "json-schema";
-const OPENAPI_URL =
- "https://raw.githubusercontent.com/sst/opencode/dev/packages/sdk/openapi.json";
+const OPENAPI_URLS = [
+ "https://raw.githubusercontent.com/anomalyco/opencode/dev/packages/sdk/openapi.json",
+ "https://raw.githubusercontent.com/sst/opencode/dev/packages/sdk/openapi.json",
+];
// Key schemas we want to extract
const TARGET_SCHEMAS = [
@@ -19,16 +23,40 @@ const TARGET_SCHEMAS = [
"ErrorPart",
];
+const OPENAPI_ARTIFACT_DIR = join(import.meta.dirname, "..", "artifacts", "openapi");
+const OPENAPI_ARTIFACT_PATH = join(OPENAPI_ARTIFACT_DIR, "opencode.json");
+
interface OpenAPISpec {
components?: {
schemas?: Record;
};
}
+function writeOpenApiArtifact(specText: string): void {
+ if (!existsSync(OPENAPI_ARTIFACT_DIR)) {
+ mkdirSync(OPENAPI_ARTIFACT_DIR, { recursive: true });
+ }
+ writeFileSync(OPENAPI_ARTIFACT_PATH, specText);
+ console.log(` [wrote] ${OPENAPI_ARTIFACT_PATH}`);
+}
+
export async function extractOpenCodeSchema(): Promise {
console.log("Extracting OpenCode schema from OpenAPI spec...");
- const specText = await fetchWithCache(OPENAPI_URL);
+ let specText: string | null = null;
+ let lastError: Error | null = null;
+ for (const url of OPENAPI_URLS) {
+ try {
+ specText = await fetchWithCache(url);
+ break;
+ } catch (error) {
+ lastError = error as Error;
+ }
+ }
+ if (!specText) {
+ throw lastError ?? new Error("Failed to fetch OpenCode OpenAPI spec");
+ }
+ writeOpenApiArtifact(specText);
const spec: OpenAPISpec = JSON.parse(specText);
if (!spec.components?.schemas) {
diff --git a/scripts/release/sdk.ts b/scripts/release/sdk.ts
index 42b181d..78f3e6a 100644
--- a/scripts/release/sdk.ts
+++ b/scripts/release/sdk.ts
@@ -18,6 +18,7 @@ const CRATES = [
const CLI_PACKAGES = [
"@sandbox-agent/cli",
"@sandbox-agent/cli-linux-x64",
+ "@sandbox-agent/cli-linux-arm64",
"@sandbox-agent/cli-win32-x64",
"@sandbox-agent/cli-darwin-x64",
"@sandbox-agent/cli-darwin-arm64",
@@ -26,6 +27,7 @@ const CLI_PACKAGES = [
// Mapping from npm package name to Rust target and binary extension
const CLI_PLATFORM_MAP: Record = {
"@sandbox-agent/cli-linux-x64": { target: "x86_64-unknown-linux-musl", binaryExt: "" },
+ "@sandbox-agent/cli-linux-arm64": { target: "aarch64-unknown-linux-musl", binaryExt: "" },
"@sandbox-agent/cli-win32-x64": { target: "x86_64-pc-windows-gnu", binaryExt: ".exe" },
"@sandbox-agent/cli-darwin-x64": { target: "x86_64-apple-darwin", binaryExt: "" },
"@sandbox-agent/cli-darwin-arm64": { target: "aarch64-apple-darwin", binaryExt: "" },
diff --git a/scripts/release/update_version.ts b/scripts/release/update_version.ts
index a1c0d1c..e8c4afc 100644
--- a/scripts/release/update_version.ts
+++ b/scripts/release/update_version.ts
@@ -70,10 +70,11 @@ export async function updateVersion(opts: ReleaseOpts) {
const paths = await glob(globPath, { cwd: opts.root });
assert(paths.length > 0, `no paths matched: ${globPath}`);
for (const path of paths) {
- const file = await fs.readFile(path, "utf-8");
- assert(find.test(file), `file does not match ${find}: ${path}`);
+ const fullPath = `${opts.root}/${path}`;
+ const file = await fs.readFile(fullPath, "utf-8");
+ assert(find.test(file), `file does not match ${find}: ${fullPath}`);
const newFile = file.replace(find, replace);
- await fs.writeFile(path, newFile);
+ await fs.writeFile(fullPath, newFile);
await $({ cwd: opts.root })`git add ${path}`;
}
diff --git a/sdks/cli-shared/package.json b/sdks/cli-shared/package.json
index d6d28ab..0c041d1 100644
--- a/sdks/cli-shared/package.json
+++ b/sdks/cli-shared/package.json
@@ -1,6 +1,6 @@
{
"name": "@sandbox-agent/cli-shared",
- "version": "0.1.6-rc.1",
+ "version": "0.1.6",
"description": "Shared helpers for sandbox-agent CLI and SDK",
"license": "Apache-2.0",
"repository": {
diff --git a/sdks/cli/bin/sandbox-agent b/sdks/cli/bin/sandbox-agent
index 66c1aac..3f1d3ad 100755
--- a/sdks/cli/bin/sandbox-agent
+++ b/sdks/cli/bin/sandbox-agent
@@ -8,7 +8,7 @@ const fs = require("fs");
const path = require("path");
const TRUST_PACKAGES =
- "@sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64";
+ "@sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64";
function formatHint(binPath) {
return formatNonExecutableBinaryMessage({
@@ -37,6 +37,7 @@ const PLATFORMS = {
"darwin-arm64": "@sandbox-agent/cli-darwin-arm64",
"darwin-x64": "@sandbox-agent/cli-darwin-x64",
"linux-x64": "@sandbox-agent/cli-linux-x64",
+ "linux-arm64": "@sandbox-agent/cli-linux-arm64",
"win32-x64": "@sandbox-agent/cli-win32-x64",
};
diff --git a/sdks/cli/package.json b/sdks/cli/package.json
index 968157b..fde9b8b 100644
--- a/sdks/cli/package.json
+++ b/sdks/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@sandbox-agent/cli",
- "version": "0.1.6-rc.1",
+ "version": "0.1.6",
"description": "CLI for sandbox-agent - run AI coding agents in sandboxes",
"license": "Apache-2.0",
"repository": {
@@ -23,6 +23,7 @@
"@sandbox-agent/cli-darwin-arm64": "workspace:*",
"@sandbox-agent/cli-darwin-x64": "workspace:*",
"@sandbox-agent/cli-linux-x64": "workspace:*",
+ "@sandbox-agent/cli-linux-arm64": "workspace:*",
"@sandbox-agent/cli-win32-x64": "workspace:*"
},
"files": [
diff --git a/sdks/cli/platforms/darwin-arm64/package.json b/sdks/cli/platforms/darwin-arm64/package.json
index cd66e2b..cfff424 100644
--- a/sdks/cli/platforms/darwin-arm64/package.json
+++ b/sdks/cli/platforms/darwin-arm64/package.json
@@ -1,6 +1,6 @@
{
"name": "@sandbox-agent/cli-darwin-arm64",
- "version": "0.1.6-rc.1",
+ "version": "0.1.6",
"description": "sandbox-agent CLI binary for macOS ARM64",
"license": "Apache-2.0",
"repository": {
diff --git a/sdks/cli/platforms/darwin-x64/package.json b/sdks/cli/platforms/darwin-x64/package.json
index 02d8f00..8fa6330 100644
--- a/sdks/cli/platforms/darwin-x64/package.json
+++ b/sdks/cli/platforms/darwin-x64/package.json
@@ -1,6 +1,6 @@
{
"name": "@sandbox-agent/cli-darwin-x64",
- "version": "0.1.6-rc.1",
+ "version": "0.1.6",
"description": "sandbox-agent CLI binary for macOS x64",
"license": "Apache-2.0",
"repository": {
diff --git a/sdks/cli/platforms/linux-arm64/package.json b/sdks/cli/platforms/linux-arm64/package.json
new file mode 100644
index 0000000..41db961
--- /dev/null
+++ b/sdks/cli/platforms/linux-arm64/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@sandbox-agent/cli-linux-arm64",
+ "version": "0.1.6",
+ "description": "sandbox-agent CLI binary for Linux arm64",
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/rivet-dev/sandbox-agent"
+ },
+ "os": [
+ "linux"
+ ],
+ "cpu": [
+ "arm64"
+ ],
+ "scripts": {
+ "postinstall": "chmod +x bin/sandbox-agent || true"
+ },
+ "files": [
+ "bin"
+ ]
+}
diff --git a/sdks/cli/platforms/linux-x64/package.json b/sdks/cli/platforms/linux-x64/package.json
index b07eb77..28e3b13 100644
--- a/sdks/cli/platforms/linux-x64/package.json
+++ b/sdks/cli/platforms/linux-x64/package.json
@@ -1,6 +1,6 @@
{
"name": "@sandbox-agent/cli-linux-x64",
- "version": "0.1.6-rc.1",
+ "version": "0.1.6",
"description": "sandbox-agent CLI binary for Linux x64",
"license": "Apache-2.0",
"repository": {
diff --git a/sdks/cli/platforms/win32-x64/package.json b/sdks/cli/platforms/win32-x64/package.json
index 6dbb302..e1f3001 100644
--- a/sdks/cli/platforms/win32-x64/package.json
+++ b/sdks/cli/platforms/win32-x64/package.json
@@ -1,6 +1,6 @@
{
"name": "@sandbox-agent/cli-win32-x64",
- "version": "0.1.6-rc.1",
+ "version": "0.1.6",
"description": "sandbox-agent CLI binary for Windows x64",
"license": "Apache-2.0",
"repository": {
diff --git a/sdks/cli/tests/launcher.test.ts b/sdks/cli/tests/launcher.test.ts
index 1019bdf..0353284 100644
--- a/sdks/cli/tests/launcher.test.ts
+++ b/sdks/cli/tests/launcher.test.ts
@@ -38,6 +38,7 @@ describe("CLI Launcher", () => {
"darwin-arm64": "@sandbox-agent/cli-darwin-arm64",
"darwin-x64": "@sandbox-agent/cli-darwin-x64",
"linux-x64": "@sandbox-agent/cli-linux-x64",
+ "linux-arm64": "@sandbox-agent/cli-linux-arm64",
"win32-x64": "@sandbox-agent/cli-win32-x64",
};
@@ -45,6 +46,7 @@ describe("CLI Launcher", () => {
expect(PLATFORMS["darwin-arm64"]).toBe("@sandbox-agent/cli-darwin-arm64");
expect(PLATFORMS["darwin-x64"]).toBe("@sandbox-agent/cli-darwin-x64");
expect(PLATFORMS["linux-x64"]).toBe("@sandbox-agent/cli-linux-x64");
+ expect(PLATFORMS["linux-arm64"]).toBe("@sandbox-agent/cli-linux-arm64");
expect(PLATFORMS["win32-x64"]).toBe("@sandbox-agent/cli-win32-x64");
});
diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json
index 29bb10e..8b135bf 100644
--- a/sdks/typescript/package.json
+++ b/sdks/typescript/package.json
@@ -1,6 +1,6 @@
{
"name": "sandbox-agent",
- "version": "0.1.6-rc.1",
+ "version": "0.1.6",
"description": "Universal API for automatic coding agents in sandboxes. Supprots Claude Code, Codex, OpenCode, and Amp.",
"license": "Apache-2.0",
"repository": {
diff --git a/sdks/typescript/src/spawn.ts b/sdks/typescript/src/spawn.ts
index 6323e08..380afbc 100644
--- a/sdks/typescript/src/spawn.ts
+++ b/sdks/typescript/src/spawn.ts
@@ -29,11 +29,12 @@ const PLATFORM_PACKAGES: Record = {
"darwin-arm64": "@sandbox-agent/cli-darwin-arm64",
"darwin-x64": "@sandbox-agent/cli-darwin-x64",
"linux-x64": "@sandbox-agent/cli-linux-x64",
+ "linux-arm64": "@sandbox-agent/cli-linux-arm64",
"win32-x64": "@sandbox-agent/cli-win32-x64",
};
const TRUST_PACKAGES =
- "@sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64";
+ "@sandbox-agent/cli-linux-x64 @sandbox-agent/cli-linux-arm64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64";
export function isNodeRuntime(): boolean {
return typeof process !== "undefined" && !!process.versions?.node;
diff --git a/server/packages/sandbox-agent/Cargo.toml b/server/packages/sandbox-agent/Cargo.toml
index 028362a..703c87c 100644
--- a/server/packages/sandbox-agent/Cargo.toml
+++ b/server/packages/sandbox-agent/Cargo.toml
@@ -25,6 +25,7 @@ futures.workspace = true
reqwest.workspace = true
dirs.workspace = true
time.workspace = true
+chrono.workspace = true
tokio.workspace = true
tokio-stream.workspace = true
tower-http.workspace = true
@@ -34,11 +35,15 @@ tracing.workspace = true
tracing-logfmt.workspace = true
tracing-subscriber.workspace = true
include_dir.workspace = true
+base64.workspace = true
tempfile = { workspace = true, optional = true }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
+[target.'cfg(windows)'.dependencies]
+windows = { version = "0.52", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console"] }
+
[dev-dependencies]
http-body-util.workspace = true
insta.workspace = true
diff --git a/server/packages/sandbox-agent/src/lib.rs b/server/packages/sandbox-agent/src/lib.rs
index 2bf872f..81dd9ab 100644
--- a/server/packages/sandbox-agent/src/lib.rs
+++ b/server/packages/sandbox-agent/src/lib.rs
@@ -3,6 +3,8 @@
mod agent_server_logs;
pub mod credentials;
pub mod http_client;
+pub mod opencode_compat;
pub mod router;
+pub mod server_logs;
pub mod telemetry;
pub mod ui;
diff --git a/server/packages/sandbox-agent/src/main.rs b/server/packages/sandbox-agent/src/main.rs
index f198dbe..52cc24d 100644
--- a/server/packages/sandbox-agent/src/main.rs
+++ b/server/packages/sandbox-agent/src/main.rs
@@ -1,7 +1,9 @@
use std::collections::HashMap;
use std::io::Write;
use std::path::PathBuf;
+use std::process::{Child, Command as ProcessCommand, Stdio};
use std::sync::Arc;
+use std::time::{Duration, Instant};
use clap::{Args, Parser, Subcommand};
@@ -21,6 +23,7 @@ use sandbox_agent::router::{
AgentListResponse, AgentModesResponse, CreateSessionResponse, EventsResponse,
SessionListResponse,
};
+use sandbox_agent::server_logs::ServerLogs;
use sandbox_agent::telemetry;
use sandbox_agent::ui;
use sandbox_agent_agent_management::agents::{AgentId, AgentManager, InstallOptions};
@@ -29,7 +32,7 @@ use sandbox_agent_agent_management::credentials::{
ProviderCredentials,
};
use serde::Serialize;
-use serde_json::Value;
+use serde_json::{json, Value};
use thiserror::Error;
use tower_http::cors::{Any, CorsLayer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
@@ -37,6 +40,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilte
const API_PREFIX: &str = "/v1";
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 2468;
+const LOGS_RETENTION: Duration = Duration::from_secs(7 * 24 * 60 * 60);
#[derive(Parser, Debug)]
#[command(name = "sandbox-agent", bin_name = "sandbox-agent")]
@@ -59,6 +63,8 @@ enum Command {
Server(ServerArgs),
/// Call the HTTP API without writing client code.
Api(ApiArgs),
+ /// EXPERIMENTAL: Start a sandbox-agent server and attach an OpenCode session.
+ Opencode(OpencodeArgs),
/// Install or reinstall an agent without running the server.
InstallAgent(InstallAgentArgs),
/// Inspect locally discovered credentials.
@@ -95,6 +101,21 @@ struct ApiArgs {
command: ApiCommand,
}
+#[derive(Args, Debug)]
+struct OpencodeArgs {
+ #[arg(long, short = 'H', default_value = DEFAULT_HOST)]
+ host: String,
+
+ #[arg(long, short = 'p', default_value_t = DEFAULT_PORT)]
+ port: u16,
+
+ #[arg(long)]
+ session_title: Option,
+
+ #[arg(long)]
+ opencode_bin: Option,
+}
+
#[derive(Args, Debug)]
struct CredentialsArgs {
#[command(subcommand)]
@@ -350,8 +371,11 @@ enum CliError {
}
fn main() {
- init_logging();
let cli = Cli::parse();
+ if let Err(err) = init_logging(&cli) {
+ eprintln!("failed to init logging: {err}");
+ std::process::exit(1);
+ }
let result = match &cli.command {
Command::Server(args) => run_server(&cli, args),
@@ -364,7 +388,11 @@ fn main() {
}
}
-fn init_logging() {
+fn init_logging(cli: &Cli) -> Result<(), CliError> {
+ if matches!(cli.command, Command::Server(_)) {
+ maybe_redirect_server_logs();
+ }
+
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(filter)
@@ -374,6 +402,7 @@ fn init_logging() {
.with_writer(std::io::stderr),
)
.init();
+ Ok(())
}
fn run_server(cli: &Cli, server: &ServerArgs) -> Result<(), CliError> {
@@ -435,12 +464,33 @@ fn default_install_dir() -> PathBuf {
.unwrap_or_else(|| PathBuf::from(".").join(".sandbox-agent").join("bin"))
}
+fn default_server_log_dir() -> PathBuf {
+ if let Ok(dir) = std::env::var("SANDBOX_AGENT_LOG_DIR") {
+ return PathBuf::from(dir);
+ }
+ dirs::data_dir()
+ .map(|dir| dir.join("sandbox-agent").join("logs"))
+ .unwrap_or_else(|| PathBuf::from(".").join(".sandbox-agent").join("logs"))
+}
+
+fn maybe_redirect_server_logs() {
+ if std::env::var("SANDBOX_AGENT_LOG_STDOUT").is_ok() {
+ return;
+ }
+
+ let log_dir = default_server_log_dir();
+ if let Err(err) = ServerLogs::new(log_dir, LOGS_RETENTION).start_sync() {
+ eprintln!("failed to redirect logs: {err}");
+ }
+}
+
fn run_client(command: &Command, cli: &Cli) -> Result<(), CliError> {
match command {
Command::Server(_) => Err(CliError::Server(
"server subcommand must be invoked as `sandbox-agent server`".to_string(),
)),
Command::Api(subcommand) => run_api(&subcommand.command, cli),
+ Command::Opencode(args) => run_opencode(cli, args),
Command::InstallAgent(args) => install_agent_local(args),
Command::Credentials(subcommand) => run_credentials(&subcommand.command),
}
@@ -453,6 +503,54 @@ fn run_api(command: &ApiCommand, cli: &Cli) -> Result<(), CliError> {
}
}
+fn run_opencode(cli: &Cli, args: &OpencodeArgs) -> Result<(), CliError> {
+ write_stderr_line("experimental: opencode subcommand may change without notice")?;
+
+ let token = if cli.no_token {
+ None
+ } else {
+ Some(cli.token.clone().ok_or(CliError::MissingToken)?)
+ };
+
+ let mut server_child = spawn_sandbox_agent_server(cli, args, token.as_deref())?;
+ let base_url = format!("http://{}:{}", args.host, args.port);
+ wait_for_health(&mut server_child, &base_url, token.as_deref())?;
+
+ let session_id =
+ create_opencode_session(&base_url, token.as_deref(), args.session_title.as_deref())?;
+ write_stdout_line(&format!("OpenCode session: {session_id}"))?;
+
+ let attach_url = format!("{base_url}/opencode");
+ let opencode_bin = resolve_opencode_bin(args.opencode_bin.as_ref());
+ let mut opencode_cmd = ProcessCommand::new(opencode_bin);
+ opencode_cmd
+ .arg("attach")
+ .arg(&attach_url)
+ .arg("--session")
+ .arg(&session_id)
+ .stdin(Stdio::inherit())
+ .stdout(Stdio::inherit())
+ .stderr(Stdio::inherit());
+ if let Some(token) = token.as_deref() {
+ opencode_cmd.arg("--password").arg(token);
+ }
+
+ let status = opencode_cmd.status().map_err(|err| {
+ terminate_child(&mut server_child);
+ CliError::Server(format!("failed to start opencode: {err}"))
+ })?;
+
+ terminate_child(&mut server_child);
+
+ if !status.success() {
+ return Err(CliError::Server(format!(
+ "opencode exited with status {status}"
+ )));
+ }
+
+ Ok(())
+}
+
fn run_agents(command: &AgentsCommand, cli: &Cli) -> Result<(), CliError> {
match command {
AgentsCommand::List(args) => {
@@ -608,6 +706,116 @@ fn run_sessions(command: &SessionsCommand, cli: &Cli) -> Result<(), CliError> {
}
}
+fn spawn_sandbox_agent_server(
+ cli: &Cli,
+ args: &OpencodeArgs,
+ token: Option<&str>,
+) -> Result {
+ let exe = std::env::current_exe()?;
+ let mut cmd = ProcessCommand::new(exe);
+ cmd.arg("server")
+ .arg("--host")
+ .arg(&args.host)
+ .arg("--port")
+ .arg(args.port.to_string())
+ .stdin(Stdio::inherit())
+ .stdout(Stdio::inherit())
+ .stderr(Stdio::inherit());
+
+ if cli.no_token {
+ cmd.arg("--no-token");
+ } else if let Some(token) = token {
+ cmd.arg("--token").arg(token);
+ }
+
+ cmd.spawn().map_err(CliError::from)
+}
+
+fn wait_for_health(
+ server_child: &mut Child,
+ base_url: &str,
+ token: Option<&str>,
+) -> Result<(), CliError> {
+ let client = HttpClient::builder().build()?;
+ let deadline = Instant::now() + Duration::from_secs(30);
+
+ while Instant::now() < deadline {
+ if let Some(status) = server_child.try_wait()? {
+ return Err(CliError::Server(format!(
+ "sandbox-agent exited before becoming healthy ({status})"
+ )));
+ }
+
+ let url = format!("{base_url}/v1/health");
+ let mut request = client.get(&url);
+ if let Some(token) = token {
+ request = request.bearer_auth(token);
+ }
+ match request.send() {
+ Ok(response) if response.status().is_success() => return Ok(()),
+ _ => {
+ std::thread::sleep(Duration::from_millis(200));
+ }
+ }
+ }
+
+ Err(CliError::Server(
+ "timed out waiting for sandbox-agent health".to_string(),
+ ))
+}
+
+fn create_opencode_session(
+ base_url: &str,
+ token: Option<&str>,
+ title: Option<&str>,
+) -> Result {
+ let client = HttpClient::builder().build()?;
+ let url = format!("{base_url}/opencode/session");
+ let body = if let Some(title) = title {
+ json!({ "title": title })
+ } else {
+ json!({})
+ };
+ let mut request = client.post(&url).json(&body);
+ if let Ok(directory) = std::env::current_dir() {
+ request = request.header(
+ "x-opencode-directory",
+ directory.to_string_lossy().to_string(),
+ );
+ }
+ if let Some(token) = token {
+ request = request.bearer_auth(token);
+ }
+ let response = request.send()?;
+ let status = response.status();
+ let text = response.text()?;
+ if !status.is_success() {
+ print_error_body(&text)?;
+ return Err(CliError::HttpStatus(status));
+ }
+ let body: Value = serde_json::from_str(&text)?;
+ let session_id = body
+ .get("id")
+ .and_then(|value| value.as_str())
+ .ok_or_else(|| CliError::Server("opencode session missing id".to_string()))?;
+ Ok(session_id.to_string())
+}
+
+fn resolve_opencode_bin(explicit: Option<&PathBuf>) -> PathBuf {
+ if let Some(path) = explicit {
+ return path.clone();
+ }
+ if let Ok(path) = std::env::var("OPENCODE_BIN") {
+ return PathBuf::from(path);
+ }
+ PathBuf::from("opencode")
+}
+
+fn terminate_child(child: &mut Child) {
+ let _ = child.kill();
+ let _ = child.wait();
+}
+
fn run_credentials(command: &CredentialsCommand) -> Result<(), CliError> {
match command {
CredentialsCommand::Extract(args) => {
diff --git a/server/packages/sandbox-agent/src/opencode_compat.rs b/server/packages/sandbox-agent/src/opencode_compat.rs
new file mode 100644
index 0000000..427d440
--- /dev/null
+++ b/server/packages/sandbox-agent/src/opencode_compat.rs
@@ -0,0 +1,4338 @@
+//! OpenCode-compatible API handlers mounted under `/opencode`.
+//!
+//! These endpoints implement the full OpenCode OpenAPI surface. Most routes are
+//! stubbed responses with deterministic helpers for snapshot testing. A minimal
+//! in-memory state tracks sessions/messages/ptys to keep behavior coherent.
+
+use std::collections::HashMap;
+use std::convert::Infallible;
+use std::str::FromStr;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::Arc;
+
+use axum::extract::{Path, Query, State};
+use axum::http::{HeaderMap, StatusCode};
+use axum::response::sse::{Event, KeepAlive};
+use axum::response::{IntoResponse, Sse};
+use axum::routing::{get, patch, post, put};
+use axum::{Json, Router};
+use futures::stream;
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value};
+use tokio::sync::{broadcast, Mutex};
+use tokio::time::interval;
+use utoipa::{IntoParams, OpenApi, ToSchema};
+
+use crate::router::{AppState, CreateSessionRequest, PermissionReply};
+use sandbox_agent_agent_management::agents::AgentId;
+use sandbox_agent_error::SandboxError;
+use sandbox_agent_universal_agent_schema::{
+ ContentPart, FileAction, ItemDeltaData, ItemEventData, ItemKind, ItemRole, ItemStatus,
+ PermissionEventData, PermissionStatus, QuestionEventData, QuestionStatus, UniversalEvent,
+ UniversalEventData, UniversalEventType, UniversalItem,
+};
+
+static SESSION_COUNTER: AtomicU64 = AtomicU64::new(1);
+static MESSAGE_COUNTER: AtomicU64 = AtomicU64::new(1);
+static PART_COUNTER: AtomicU64 = AtomicU64::new(1);
+static PTY_COUNTER: AtomicU64 = AtomicU64::new(1);
+static PROJECT_COUNTER: AtomicU64 = AtomicU64::new(1);
+const OPENCODE_PROVIDER_ID: &str = "sandbox-agent";
+const OPENCODE_PROVIDER_NAME: &str = "Sandbox Agent";
+const OPENCODE_DEFAULT_MODEL_ID: &str = "mock";
+const OPENCODE_DEFAULT_AGENT_MODE: &str = "build";
+
+#[derive(Clone, Debug)]
+struct OpenCodeCompatConfig {
+ fixed_time_ms: Option,
+ fixed_directory: Option,
+ fixed_worktree: Option,
+ fixed_home: Option,
+ fixed_state: Option,
+ fixed_config: Option,
+ fixed_branch: Option,
+}
+
+impl OpenCodeCompatConfig {
+ fn from_env() -> Self {
+ Self {
+ fixed_time_ms: std::env::var("OPENCODE_COMPAT_FIXED_TIME_MS")
+ .ok()
+ .and_then(|value| value.parse::().ok()),
+ fixed_directory: std::env::var("OPENCODE_COMPAT_DIRECTORY").ok(),
+ fixed_worktree: std::env::var("OPENCODE_COMPAT_WORKTREE").ok(),
+ fixed_home: std::env::var("OPENCODE_COMPAT_HOME").ok(),
+ fixed_state: std::env::var("OPENCODE_COMPAT_STATE").ok(),
+ fixed_config: std::env::var("OPENCODE_COMPAT_CONFIG").ok(),
+ fixed_branch: std::env::var("OPENCODE_COMPAT_BRANCH").ok(),
+ }
+ }
+
+ fn now_ms(&self) -> i64 {
+ if let Some(value) = self.fixed_time_ms {
+ return value;
+ }
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .map(|d| d.as_millis() as i64)
+ .unwrap_or(0)
+ }
+}
+
+#[derive(Clone, Debug)]
+struct OpenCodeSessionRecord {
+ id: String,
+ slug: String,
+ project_id: String,
+ directory: String,
+ parent_id: Option,
+ title: String,
+ version: String,
+ created_at: i64,
+ updated_at: i64,
+ share_url: Option,
+}
+
+impl OpenCodeSessionRecord {
+ fn to_value(&self) -> Value {
+ let mut map = serde_json::Map::new();
+ map.insert("id".to_string(), json!(self.id));
+ map.insert("slug".to_string(), json!(self.slug));
+ map.insert("projectID".to_string(), json!(self.project_id));
+ map.insert("directory".to_string(), json!(self.directory));
+ map.insert("title".to_string(), json!(self.title));
+ map.insert("version".to_string(), json!(self.version));
+ map.insert(
+ "time".to_string(),
+ json!({
+ "created": self.created_at,
+ "updated": self.updated_at,
+ }),
+ );
+ if let Some(parent_id) = &self.parent_id {
+ map.insert("parentID".to_string(), json!(parent_id));
+ }
+ if let Some(url) = &self.share_url {
+ map.insert("share".to_string(), json!({"url": url}));
+ }
+ Value::Object(map)
+ }
+}
+
+#[derive(Clone, Debug)]
+struct OpenCodeMessageRecord {
+ info: Value,
+ parts: Vec,
+}
+
+#[derive(Clone, Debug)]
+struct OpenCodePtyRecord {
+ id: String,
+ title: String,
+ command: String,
+ args: Vec,
+ cwd: String,
+ status: String,
+ pid: i64,
+}
+
+impl OpenCodePtyRecord {
+ fn to_value(&self) -> Value {
+ json!({
+ "id": self.id,
+ "title": self.title,
+ "command": self.command,
+ "args": self.args,
+ "cwd": self.cwd,
+ "status": self.status,
+ "pid": self.pid,
+ })
+ }
+}
+
+#[derive(Clone, Debug)]
+struct OpenCodePermissionRecord {
+ id: String,
+ session_id: String,
+ permission: String,
+ patterns: Vec,
+ metadata: Value,
+ always: Vec,
+ tool: Option,
+}
+
+impl OpenCodePermissionRecord {
+ fn to_value(&self) -> Value {
+ let mut map = serde_json::Map::new();
+ map.insert("id".to_string(), json!(self.id));
+ map.insert("sessionID".to_string(), json!(self.session_id));
+ map.insert("permission".to_string(), json!(self.permission));
+ map.insert("patterns".to_string(), json!(self.patterns));
+ map.insert("metadata".to_string(), self.metadata.clone());
+ map.insert("always".to_string(), json!(self.always));
+ if let Some(tool) = &self.tool {
+ map.insert("tool".to_string(), tool.clone());
+ }
+ Value::Object(map)
+ }
+}
+
+#[derive(Clone, Debug)]
+struct OpenCodeQuestionRecord {
+ id: String,
+ session_id: String,
+ questions: Vec,
+ tool: Option,
+}
+
+impl OpenCodeQuestionRecord {
+ fn to_value(&self) -> Value {
+ let mut map = serde_json::Map::new();
+ map.insert("id".to_string(), json!(self.id));
+ map.insert("sessionID".to_string(), json!(self.session_id));
+ map.insert("questions".to_string(), json!(self.questions));
+ if let Some(tool) = &self.tool {
+ map.insert("tool".to_string(), tool.clone());
+ }
+ Value::Object(map)
+ }
+}
+
+#[derive(Default, Clone)]
+struct OpenCodeSessionRuntime {
+ last_user_message_id: Option,
+ last_agent: Option,
+ last_model_provider: Option,
+ last_model_id: Option,
+ session_agent_id: Option,
+ session_provider_id: Option,
+ session_model_id: Option,
+ message_id_for_item: HashMap,
+ text_by_message: HashMap,
+ part_id_by_message: HashMap,
+ tool_part_by_call: HashMap,
+ tool_message_by_call: HashMap,
+}
+
+pub struct OpenCodeState {
+ config: OpenCodeCompatConfig,
+ default_project_id: String,
+ sessions: Mutex>,
+ messages: Mutex>>,
+ ptys: Mutex>,
+ permissions: Mutex>,
+ questions: Mutex>,
+ session_runtime: Mutex>,
+ session_streams: Mutex>,
+ event_broadcaster: broadcast::Sender,
+}
+
+impl OpenCodeState {
+ pub fn new() -> Self {
+ let (event_broadcaster, _) = broadcast::channel(256);
+ let project_id = format!("proj_{}", PROJECT_COUNTER.fetch_add(1, Ordering::Relaxed));
+ Self {
+ config: OpenCodeCompatConfig::from_env(),
+ default_project_id: project_id,
+ sessions: Mutex::new(HashMap::new()),
+ messages: Mutex::new(HashMap::new()),
+ ptys: Mutex::new(HashMap::new()),
+ permissions: Mutex::new(HashMap::new()),
+ questions: Mutex::new(HashMap::new()),
+ session_runtime: Mutex::new(HashMap::new()),
+ session_streams: Mutex::new(HashMap::new()),
+ event_broadcaster,
+ }
+ }
+
+ pub fn subscribe(&self) -> broadcast::Receiver {
+ self.event_broadcaster.subscribe()
+ }
+
+ pub fn emit_event(&self, event: Value) {
+ let _ = self.event_broadcaster.send(event);
+ }
+
+ fn now_ms(&self) -> i64 {
+ self.config.now_ms()
+ }
+
+ fn directory_for(&self, headers: &HeaderMap, query: Option<&String>) -> String {
+ if let Some(value) = query {
+ return value.clone();
+ }
+ if let Some(value) = self.config.fixed_directory.as_ref().cloned().or_else(|| {
+ headers
+ .get("x-opencode-directory")
+ .and_then(|v| v.to_str().ok())
+ .map(|v| v.to_string())
+ }) {
+ return value;
+ }
+ std::env::current_dir()
+ .ok()
+ .and_then(|p| p.to_str().map(|v| v.to_string()))
+ .unwrap_or_else(|| ".".to_string())
+ }
+
+ fn worktree_for(&self, directory: &str) -> String {
+ self.config
+ .fixed_worktree
+ .clone()
+ .unwrap_or_else(|| directory.to_string())
+ }
+
+ fn home_dir(&self) -> String {
+ self.config
+ .fixed_home
+ .clone()
+ .or_else(|| std::env::var("HOME").ok())
+ .unwrap_or_else(|| "/".to_string())
+ }
+
+ fn state_dir(&self) -> String {
+ self.config
+ .fixed_state
+ .clone()
+ .unwrap_or_else(|| format!("{}/.local/state/opencode", self.home_dir()))
+ }
+
+ async fn ensure_session(&self, session_id: &str, directory: String) -> Value {
+ let mut sessions = self.sessions.lock().await;
+ if let Some(existing) = sessions.get(session_id) {
+ return existing.to_value();
+ }
+
+ let now = self.now_ms();
+ let record = OpenCodeSessionRecord {
+ id: session_id.to_string(),
+ slug: format!("session-{}", session_id),
+ project_id: self.default_project_id.clone(),
+ directory,
+ parent_id: None,
+ title: format!("Session {}", session_id),
+ version: "0".to_string(),
+ created_at: now,
+ updated_at: now,
+ share_url: None,
+ };
+ let value = record.to_value();
+ sessions.insert(session_id.to_string(), record);
+ drop(sessions);
+
+ self.emit_event(session_event("session.created", &value));
+ value
+ }
+
+ fn config_dir(&self) -> String {
+ self.config
+ .fixed_config
+ .clone()
+ .unwrap_or_else(|| format!("{}/.config/opencode", self.home_dir()))
+ }
+
+ fn branch_name(&self) -> String {
+ self.config
+ .fixed_branch
+ .clone()
+ .unwrap_or_else(|| "main".to_string())
+ }
+
+ async fn update_runtime(
+ &self,
+ session_id: &str,
+ update: impl FnOnce(&mut OpenCodeSessionRuntime),
+ ) -> OpenCodeSessionRuntime {
+ let mut runtimes = self.session_runtime.lock().await;
+ let entry = runtimes
+ .entry(session_id.to_string())
+ .or_insert_with(OpenCodeSessionRuntime::default);
+ update(entry);
+ entry.clone()
+ }
+}
+
+/// Combined app state with OpenCode state.
+pub struct OpenCodeAppState {
+ pub inner: Arc,
+ pub opencode: Arc,
+}
+
+impl OpenCodeAppState {
+ pub fn new(inner: Arc) -> Arc {
+ Arc::new(Self {
+ inner,
+ opencode: Arc::new(OpenCodeState::new()),
+ })
+ }
+}
+
+async fn ensure_backing_session(
+ state: &Arc,
+ session_id: &str,
+ agent: &str,
+) -> Result<(), SandboxError> {
+ let request = CreateSessionRequest {
+ agent: agent.to_string(),
+ agent_mode: None,
+ permission_mode: None,
+ model: None,
+ variant: None,
+ agent_version: None,
+ };
+ match state
+ .inner
+ .session_manager()
+ .create_session(session_id.to_string(), request)
+ .await
+ {
+ Ok(_) => Ok(()),
+ Err(SandboxError::SessionAlreadyExists { .. }) => Ok(()),
+ Err(err) => Err(err),
+ }
+}
+
+async fn ensure_session_stream(state: Arc, session_id: String) {
+ let should_spawn = {
+ let mut streams = state.opencode.session_streams.lock().await;
+ if streams.contains_key(&session_id) {
+ false
+ } else {
+ streams.insert(session_id.clone(), true);
+ true
+ }
+ };
+ if !should_spawn {
+ return;
+ }
+
+ tokio::spawn(async move {
+ let subscription = match state
+ .inner
+ .session_manager()
+ .subscribe(&session_id, 0)
+ .await
+ {
+ Ok(subscription) => subscription,
+ Err(_) => {
+ let mut streams = state.opencode.session_streams.lock().await;
+ streams.remove(&session_id);
+ return;
+ }
+ };
+
+ for event in subscription.initial_events {
+ apply_universal_event(state.clone(), event).await;
+ }
+ let mut receiver = subscription.receiver;
+ loop {
+ match receiver.recv().await {
+ Ok(event) => {
+ apply_universal_event(state.clone(), event).await;
+ }
+ Err(broadcast::error::RecvError::Lagged(_)) => continue,
+ Err(broadcast::error::RecvError::Closed) => break,
+ }
+ }
+ let mut streams = state.opencode.session_streams.lock().await;
+ streams.remove(&session_id);
+ });
+}
+
+#[derive(Debug, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+struct OpenCodeCreateSessionRequest {
+ title: Option,
+ #[serde(rename = "parentID")]
+ parent_id: Option,
+ #[schema(value_type = String)]
+ permission: Option,
+}
+
+#[derive(Debug, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+struct OpenCodeUpdateSessionRequest {
+ title: Option,
+}
+
+#[derive(Debug, Deserialize, IntoParams)]
+struct DirectoryQuery {
+ directory: Option,
+}
+
+#[derive(Debug, Deserialize, IntoParams)]
+struct ToolQuery {
+ directory: Option,
+ provider: Option,
+ model: Option,
+}
+
+#[derive(Debug, Deserialize, IntoParams)]
+struct FindTextQuery {
+ directory: Option,
+ pattern: Option,
+}
+
+#[derive(Debug, Deserialize, IntoParams)]
+struct FindFilesQuery {
+ directory: Option,
+ query: Option,
+}
+
+#[derive(Debug, Deserialize, IntoParams)]
+struct FindSymbolsQuery {
+ directory: Option,
+ query: Option,
+}
+
+#[derive(Debug, Deserialize, IntoParams)]
+struct FileContentQuery {
+ directory: Option,
+ path: Option,
+}
+
+#[derive(Debug, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+struct SessionMessageRequest {
+ #[schema(value_type = Vec)]
+ parts: Option>,
+ #[serde(rename = "messageID")]
+ message_id: Option,
+ agent: Option,
+ #[schema(value_type = String)]
+ model: Option,
+ system: Option,
+ variant: Option,
+}
+
+#[derive(Debug, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+struct SessionCommandRequest {
+ command: Option,
+ arguments: Option,
+ #[serde(rename = "messageID")]
+ message_id: Option,
+ agent: Option,
+ model: Option,
+ variant: Option,
+ #[schema(value_type = Vec)]
+ parts: Option>,
+}
+
+#[derive(Debug, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+struct SessionShellRequest {
+ command: Option,
+ agent: Option,
+ #[schema(value_type = String)]
+ model: Option,
+}
+
+#[derive(Debug, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+struct SessionSummarizeRequest {
+ #[serde(rename = "providerID")]
+ provider_id: Option,
+ #[serde(rename = "modelID")]
+ model_id: Option,
+ auto: Option,
+}
+
+#[derive(Debug, Serialize, Deserialize, ToSchema)]
+struct PermissionReplyRequest {
+ response: Option,
+}
+
+#[derive(Debug, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+struct PermissionGlobalReplyRequest {
+ reply: Option,
+}
+
+#[derive(Debug, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+struct QuestionReplyBody {
+ answers: Option>>,
+}
+
+#[derive(Debug, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+struct PtyCreateRequest {
+ command: Option,
+ args: Option>,
+ cwd: Option,
+ title: Option,
+}
+
+fn next_id(prefix: &str, counter: &AtomicU64) -> String {
+ let id = counter.fetch_add(1, Ordering::Relaxed);
+ format!("{}{}", prefix, id)
+}
+
+fn available_agent_ids() -> Vec {
+ vec![
+ AgentId::Claude,
+ AgentId::Codex,
+ AgentId::Opencode,
+ AgentId::Amp,
+ AgentId::Mock,
+ ]
+}
+
+fn default_agent_id() -> AgentId {
+ AgentId::Mock
+}
+
+fn default_agent_mode() -> &'static str {
+ OPENCODE_DEFAULT_AGENT_MODE
+}
+
+fn resolve_agent_from_model(provider_id: &str, model_id: &str) -> Option {
+ if provider_id == OPENCODE_PROVIDER_ID {
+ AgentId::parse(model_id)
+ } else {
+ None
+ }
+}
+
+fn normalize_agent_mode(agent: Option) -> String {
+ agent
+ .filter(|value| !value.is_empty())
+ .unwrap_or_else(|| default_agent_mode().to_string())
+}
+
+async fn resolve_session_agent(
+ state: &OpenCodeAppState,
+ session_id: &str,
+ requested_provider: Option<&str>,
+ requested_model: Option<&str>,
+) -> (String, String, String) {
+ let mut provider_id = requested_provider
+ .filter(|value| !value.is_empty())
+ .unwrap_or(OPENCODE_PROVIDER_ID)
+ .to_string();
+ let mut model_id = requested_model
+ .filter(|value| !value.is_empty())
+ .unwrap_or(OPENCODE_DEFAULT_MODEL_ID)
+ .to_string();
+ let mut resolved_agent = resolve_agent_from_model(&provider_id, &model_id);
+ if resolved_agent.is_none() {
+ provider_id = OPENCODE_PROVIDER_ID.to_string();
+ model_id = OPENCODE_DEFAULT_MODEL_ID.to_string();
+ resolved_agent = Some(default_agent_id());
+ }
+
+ let mut resolved_agent_id: Option = None;
+ let mut resolved_provider: Option = None;
+ let mut resolved_model: Option = None;
+
+ state
+ .opencode
+ .update_runtime(session_id, |runtime| {
+ if runtime.session_agent_id.is_none() {
+ let agent = resolved_agent.unwrap_or_else(default_agent_id);
+ runtime.session_agent_id = Some(agent.as_str().to_string());
+ runtime.session_provider_id = Some(provider_id.clone());
+ runtime.session_model_id = Some(model_id.clone());
+ }
+ resolved_agent_id = runtime.session_agent_id.clone();
+ resolved_provider = runtime.session_provider_id.clone();
+ resolved_model = runtime.session_model_id.clone();
+ })
+ .await;
+
+ (
+ resolved_agent_id.unwrap_or_else(|| default_agent_id().as_str().to_string()),
+ resolved_provider.unwrap_or(provider_id),
+ resolved_model.unwrap_or(model_id),
+ )
+}
+
+fn agent_display_name(agent: AgentId) -> &'static str {
+ match agent {
+ AgentId::Claude => "Claude",
+ AgentId::Codex => "Codex",
+ AgentId::Opencode => "OpenCode",
+ AgentId::Amp => "Amp",
+ AgentId::Mock => "Mock",
+ }
+}
+
+fn model_config_entry(agent: AgentId) -> Value {
+ json!({
+ "id": agent.as_str(),
+ "providerID": OPENCODE_PROVIDER_ID,
+ "api": {
+ "id": "sandbox-agent",
+ "url": "http://localhost",
+ "npm": "@sandbox-agent/sdk"
+ },
+ "name": agent_display_name(agent),
+ "family": "sandbox-agent",
+ "capabilities": {
+ "temperature": true,
+ "reasoning": true,
+ "attachment": false,
+ "toolcall": true,
+ "input": {
+ "text": true,
+ "audio": false,
+ "image": false,
+ "video": false,
+ "pdf": false
+ },
+ "output": {
+ "text": true,
+ "audio": false,
+ "image": false,
+ "video": false,
+ "pdf": false
+ },
+ "interleaved": false
+ },
+ "cost": {
+ "input": 0,
+ "output": 0,
+ "cache": {"read": 0, "write": 0}
+ },
+ "limit": {
+ "context": 128000,
+ "output": 4096
+ },
+ "status": "active",
+ "options": {},
+ "headers": {},
+ "release_date": "2024-01-01",
+ "variants": {}
+ })
+}
+
+fn model_summary_entry(agent: AgentId) -> Value {
+ json!({
+ "id": agent.as_str(),
+ "name": agent_display_name(agent),
+ "release_date": "2024-01-01",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "options": {},
+ "limit": {
+ "context": 128000,
+ "output": 4096
+ }
+ })
+}
+
+fn bad_request(message: &str) -> (StatusCode, Json) {
+ (
+ StatusCode::BAD_REQUEST,
+ Json(json!({
+ "data": {},
+ "errors": [{"message": message}],
+ "success": false,
+ })),
+ )
+}
+
+fn not_found(message: &str) -> (StatusCode, Json) {
+ (
+ StatusCode::NOT_FOUND,
+ Json(json!({
+ "name": "NotFoundError",
+ "data": {"message": message},
+ })),
+ )
+}
+
+fn internal_error(message: &str) -> (StatusCode, Json) {
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({
+ "data": {},
+ "errors": [{"message": message}],
+ "success": false,
+ })),
+ )
+}
+
+fn sandbox_error_response(err: SandboxError) -> (StatusCode, Json) {
+ match err {
+ SandboxError::SessionNotFound { .. } => not_found("Session not found"),
+ SandboxError::InvalidRequest { message } => bad_request(&message),
+ other => internal_error(&other.to_string()),
+ }
+}
+
+fn parse_permission_reply_value(value: Option<&str>) -> Result {
+ let value = value.unwrap_or("once").to_ascii_lowercase();
+ match value.as_str() {
+ "once" | "allow" | "approve" => Ok(PermissionReply::Once),
+ "always" => Ok(PermissionReply::Always),
+ "reject" | "deny" => Ok(PermissionReply::Reject),
+ other => PermissionReply::from_str(other),
+ }
+}
+
+fn bool_ok(value: bool) -> (StatusCode, Json) {
+ (StatusCode::OK, Json(json!(value)))
+}
+
+fn build_user_message(
+ session_id: &str,
+ message_id: &str,
+ created_at: i64,
+ agent: &str,
+ provider_id: &str,
+ model_id: &str,
+) -> Value {
+ json!({
+ "id": message_id,
+ "sessionID": session_id,
+ "role": "user",
+ "time": {"created": created_at},
+ "agent": agent,
+ "model": {"providerID": provider_id, "modelID": model_id},
+ })
+}
+
+fn build_assistant_message(
+ session_id: &str,
+ message_id: &str,
+ parent_id: &str,
+ created_at: i64,
+ directory: &str,
+ worktree: &str,
+ agent: &str,
+ provider_id: &str,
+ model_id: &str,
+) -> Value {
+ json!({
+ "id": message_id,
+ "sessionID": session_id,
+ "role": "assistant",
+ "time": {"created": created_at},
+ "parentID": parent_id,
+ "modelID": model_id,
+ "providerID": provider_id,
+ "mode": "default",
+ "agent": agent,
+ "path": {"cwd": directory, "root": worktree},
+ "cost": 0,
+ "finish": "stop",
+ "tokens": {
+ "input": 0,
+ "output": 0,
+ "reasoning": 0,
+ "cache": {"read": 0, "write": 0}
+ }
+ })
+}
+
+fn build_text_part(session_id: &str, message_id: &str, text: &str) -> Value {
+ json!({
+ "id": next_id("part_", &PART_COUNTER),
+ "sessionID": session_id,
+ "messageID": message_id,
+ "type": "text",
+ "text": text,
+ })
+}
+
+fn part_id_from_input(input: &Value) -> String {
+ input
+ .get("id")
+ .and_then(|v| v.as_str())
+ .filter(|v| !v.is_empty())
+ .map(|v| v.to_string())
+ .unwrap_or_else(|| next_id("part_", &PART_COUNTER))
+}
+
+fn build_file_part(session_id: &str, message_id: &str, input: &Value) -> Value {
+ let mut map = serde_json::Map::new();
+ map.insert("id".to_string(), json!(part_id_from_input(input)));
+ map.insert("sessionID".to_string(), json!(session_id));
+ map.insert("messageID".to_string(), json!(message_id));
+ map.insert("type".to_string(), json!("file"));
+ map.insert(
+ "mime".to_string(),
+ input
+ .get("mime")
+ .cloned()
+ .unwrap_or_else(|| json!("application/octet-stream")),
+ );
+ map.insert(
+ "url".to_string(),
+ input.get("url").cloned().unwrap_or_else(|| json!("")),
+ );
+ if let Some(filename) = input.get("filename") {
+ map.insert("filename".to_string(), filename.clone());
+ }
+ if let Some(source) = input.get("source") {
+ map.insert("source".to_string(), source.clone());
+ }
+ Value::Object(map)
+}
+
+fn build_agent_part(session_id: &str, message_id: &str, input: &Value) -> Value {
+ let mut map = serde_json::Map::new();
+ map.insert("id".to_string(), json!(part_id_from_input(input)));
+ map.insert("sessionID".to_string(), json!(session_id));
+ map.insert("messageID".to_string(), json!(message_id));
+ map.insert("type".to_string(), json!("agent"));
+ map.insert(
+ "name".to_string(),
+ input.get("name").cloned().unwrap_or_else(|| json!("")),
+ );
+ if let Some(source) = input.get("source") {
+ map.insert("source".to_string(), source.clone());
+ }
+ Value::Object(map)
+}
+
+fn build_subtask_part(session_id: &str, message_id: &str, input: &Value) -> Value {
+ let mut map = serde_json::Map::new();
+ map.insert("id".to_string(), json!(part_id_from_input(input)));
+ map.insert("sessionID".to_string(), json!(session_id));
+ map.insert("messageID".to_string(), json!(message_id));
+ map.insert("type".to_string(), json!("subtask"));
+ map.insert(
+ "prompt".to_string(),
+ input.get("prompt").cloned().unwrap_or_else(|| json!("")),
+ );
+ map.insert(
+ "description".to_string(),
+ input
+ .get("description")
+ .cloned()
+ .unwrap_or_else(|| json!("")),
+ );
+ map.insert(
+ "agent".to_string(),
+ input.get("agent").cloned().unwrap_or_else(|| json!("")),
+ );
+ if let Some(model) = input.get("model") {
+ map.insert("model".to_string(), model.clone());
+ }
+ if let Some(command) = input.get("command") {
+ map.insert("command".to_string(), command.clone());
+ }
+ Value::Object(map)
+}
+
+fn normalize_part(session_id: &str, message_id: &str, input: &Value) -> Value {
+ match input.get("type").and_then(|v| v.as_str()) {
+ Some("file") => build_file_part(session_id, message_id, input),
+ Some("agent") => build_agent_part(session_id, message_id, input),
+ Some("subtask") => build_subtask_part(session_id, message_id, input),
+ _ => build_text_part(
+ session_id,
+ message_id,
+ input
+ .get("text")
+ .and_then(|v| v.as_str())
+ .unwrap_or("")
+ .trim(),
+ ),
+ }
+}
+
+fn message_id_for_sequence(sequence: u64) -> String {
+ format!("msg_{:020}", sequence)
+}
+
+fn unique_assistant_message_id(
+ runtime: &OpenCodeSessionRuntime,
+ parent_id: Option<&String>,
+ sequence: u64,
+) -> String {
+ let base = match parent_id {
+ Some(parent) => format!("{parent}_assistant"),
+ None => message_id_for_sequence(sequence),
+ };
+ if runtime.message_id_for_item.values().any(|id| id == &base) {
+ format!("{base}_{:020}", sequence)
+ } else {
+ base
+ }
+}
+
+fn extract_text_from_content(parts: &[ContentPart]) -> Option {
+ let mut text = String::new();
+ for part in parts {
+ match part {
+ ContentPart::Text { text: chunk } => {
+ text.push_str(chunk);
+ }
+ ContentPart::Json { json } => {
+ if let Ok(chunk) = serde_json::to_string(json) {
+ text.push_str(&chunk);
+ }
+ }
+ ContentPart::Status { label, detail } => {
+ text.push_str(label);
+ if let Some(detail) = detail {
+ if !detail.is_empty() {
+ text.push_str(": ");
+ text.push_str(detail);
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ if text.is_empty() {
+ None
+ } else {
+ Some(text)
+ }
+}
+
+fn build_text_part_with_id(session_id: &str, message_id: &str, part_id: &str, text: &str) -> Value {
+ json!({
+ "id": part_id,
+ "sessionID": session_id,
+ "messageID": message_id,
+ "type": "text",
+ "text": text,
+ })
+}
+
+fn build_reasoning_part(
+ session_id: &str,
+ message_id: &str,
+ part_id: &str,
+ text: &str,
+ now: i64,
+) -> Value {
+ json!({
+ "id": part_id,
+ "sessionID": session_id,
+ "messageID": message_id,
+ "type": "reasoning",
+ "text": text,
+ "metadata": {},
+ "time": {"start": now, "end": now},
+ })
+}
+
+fn build_tool_part(
+ session_id: &str,
+ message_id: &str,
+ part_id: &str,
+ call_id: &str,
+ tool: &str,
+ state: Value,
+) -> Value {
+ json!({
+ "id": part_id,
+ "sessionID": session_id,
+ "messageID": message_id,
+ "type": "tool",
+ "callID": call_id,
+ "tool": tool,
+ "state": state,
+ "metadata": {},
+ })
+}
+
+fn file_source_from_diff(path: &str, diff: &str) -> Value {
+ json!({
+ "type": "file",
+ "path": path,
+ "text": {
+ "value": diff,
+ "start": 0,
+ "end": diff.len() as i64,
+ }
+ })
+}
+
+fn build_file_part_from_path(
+ session_id: &str,
+ message_id: &str,
+ path: &str,
+ mime: &str,
+ diff: Option<&str>,
+) -> Value {
+ let mut map = serde_json::Map::new();
+ map.insert("id".to_string(), json!(next_id("part_", &PART_COUNTER)));
+ map.insert("sessionID".to_string(), json!(session_id));
+ map.insert("messageID".to_string(), json!(message_id));
+ map.insert("type".to_string(), json!("file"));
+ map.insert("mime".to_string(), json!(mime));
+ map.insert("url".to_string(), json!(format!("file://{}", path)));
+ map.insert("filename".to_string(), json!(path));
+ if let Some(diff) = diff {
+ map.insert("source".to_string(), file_source_from_diff(path, diff));
+ }
+ Value::Object(map)
+}
+
+fn session_event(event_type: &str, session: &Value) -> Value {
+ json!({
+ "type": event_type,
+ "properties": {"info": session}
+ })
+}
+
+fn message_event(event_type: &str, message: &Value) -> Value {
+ let session_id = message
+ .get("sessionID")
+ .and_then(|v| v.as_str())
+ .map(|v| v.to_string());
+ let mut props = serde_json::Map::new();
+ props.insert("info".to_string(), message.clone());
+ if let Some(session_id) = session_id {
+ props.insert("sessionID".to_string(), json!(session_id));
+ }
+ Value::Object({
+ let mut map = serde_json::Map::new();
+ map.insert("type".to_string(), json!(event_type));
+ map.insert("properties".to_string(), Value::Object(props));
+ map
+ })
+}
+
+fn part_event_with_delta(event_type: &str, part: &Value, delta: Option<&str>) -> Value {
+ let mut props = serde_json::Map::new();
+ props.insert("part".to_string(), part.clone());
+ if let Some(session_id) = part.get("sessionID").and_then(|v| v.as_str()) {
+ props.insert("sessionID".to_string(), json!(session_id));
+ }
+ if let Some(message_id) = part.get("messageID").and_then(|v| v.as_str()) {
+ props.insert("messageID".to_string(), json!(message_id));
+ }
+ if let Some(delta) = delta {
+ props.insert("delta".to_string(), json!(delta));
+ }
+ Value::Object({
+ let mut map = serde_json::Map::new();
+ map.insert("type".to_string(), json!(event_type));
+ map.insert("properties".to_string(), Value::Object(props));
+ map
+ })
+}
+
+fn part_event(event_type: &str, part: &Value) -> Value {
+ part_event_with_delta(event_type, part, None)
+}
+
+fn emit_file_edited(state: &OpenCodeState, path: &str) {
+ state.emit_event(json!({
+ "type": "file.edited",
+ "properties": {"file": path}
+ }));
+}
+
+fn permission_event(event_type: &str, permission: &Value) -> Value {
+ json!({
+ "type": event_type,
+ "properties": permission
+ })
+}
+
+fn question_event(event_type: &str, question: &Value) -> Value {
+ json!({
+ "type": event_type,
+ "properties": question
+ })
+}
+
+fn message_id_from_info(info: &Value) -> Option {
+ info.get("id")
+ .and_then(|v| v.as_str())
+ .map(|v| v.to_string())
+}
+
+async fn upsert_message_info(state: &OpenCodeState, session_id: &str, info: Value) -> Vec {
+ let mut messages = state.messages.lock().await;
+ let entry = messages.entry(session_id.to_string()).or_default();
+ let message_id = message_id_from_info(&info);
+ if let Some(message_id) = message_id.clone() {
+ if let Some(existing) = entry.iter_mut().find(|record| {
+ message_id_from_info(&record.info).as_deref() == Some(message_id.as_str())
+ }) {
+ existing.info = info.clone();
+ } else {
+ entry.push(OpenCodeMessageRecord {
+ info: info.clone(),
+ parts: Vec::new(),
+ });
+ }
+ entry.sort_by(|a, b| {
+ let a_id = message_id_from_info(&a.info).unwrap_or_default();
+ let b_id = message_id_from_info(&b.info).unwrap_or_default();
+ a_id.cmp(&b_id)
+ });
+ }
+ entry.iter().map(|record| record.info.clone()).collect()
+}
+
+async fn upsert_message_part(
+ state: &OpenCodeState,
+ session_id: &str,
+ message_id: &str,
+ part: Value,
+) {
+ let mut messages = state.messages.lock().await;
+ let entry = messages.entry(session_id.to_string()).or_default();
+ let record = if let Some(record) = entry
+ .iter_mut()
+ .find(|record| message_id_from_info(&record.info).as_deref() == Some(message_id))
+ {
+ record
+ } else {
+ entry.push(OpenCodeMessageRecord {
+ info: json!({"id": message_id, "sessionID": session_id, "role": "assistant", "time": {"created": 0}}),
+ parts: Vec::new(),
+ });
+ entry.last_mut().expect("record just inserted")
+ };
+
+ let part_id = part.get("id").and_then(|v| v.as_str()).unwrap_or("");
+ if let Some(existing) = record
+ .parts
+ .iter_mut()
+ .find(|p| p.get("id").and_then(|v| v.as_str()) == Some(part_id))
+ {
+ *existing = part;
+ } else {
+ record.parts.push(part);
+ }
+ record.parts.sort_by(|a, b| {
+ let a_id = a.get("id").and_then(|v| v.as_str()).unwrap_or("");
+ let b_id = b.get("id").and_then(|v| v.as_str()).unwrap_or("");
+ a_id.cmp(b_id)
+ });
+}
+
+async fn session_directory(state: &OpenCodeState, session_id: &str) -> String {
+ let sessions = state.sessions.lock().await;
+ if let Some(session) = sessions.get(session_id) {
+ return session.directory.clone();
+ }
+ std::env::current_dir()
+ .ok()
+ .and_then(|p| p.to_str().map(|v| v.to_string()))
+ .unwrap_or_else(|| ".".to_string())
+}
+
+#[derive(Default)]
+struct ToolContentInfo {
+ call_id: Option,
+ tool_name: Option,
+ arguments: Option,
+ output: Option,
+ file_refs: Vec<(String, FileAction, Option)>,
+}
+
+fn extract_tool_content(parts: &[ContentPart]) -> ToolContentInfo {
+ let mut info = ToolContentInfo::default();
+ for part in parts {
+ match part {
+ ContentPart::ToolCall {
+ name,
+ arguments,
+ call_id,
+ } => {
+ info.call_id = Some(call_id.clone());
+ info.tool_name = Some(name.clone());
+ info.arguments = Some(arguments.clone());
+ }
+ ContentPart::ToolResult { call_id, output } => {
+ info.call_id = Some(call_id.clone());
+ info.output = Some(output.clone());
+ }
+ ContentPart::FileRef { path, action, diff } => {
+ info.file_refs
+ .push((path.clone(), action.clone(), diff.clone()));
+ }
+ _ => {}
+ }
+ }
+ info
+}
+
+fn tool_input_from_arguments(arguments: Option<&str>) -> Value {
+ let Some(arguments) = arguments else {
+ return json!({});
+ };
+ if let Ok(value) = serde_json::from_str::(arguments) {
+ if value.is_object() {
+ return value;
+ }
+ }
+ json!({ "arguments": arguments })
+}
+
+fn patterns_from_metadata(metadata: &Option) -> Vec {
+ let mut patterns = Vec::new();
+ let Some(metadata) = metadata else {
+ return patterns;
+ };
+ if let Some(path) = metadata.get("path").and_then(|v| v.as_str()) {
+ patterns.push(path.to_string());
+ }
+ if let Some(paths) = metadata.get("paths").and_then(|v| v.as_array()) {
+ for value in paths {
+ if let Some(path) = value.as_str() {
+ patterns.push(path.to_string());
+ }
+ }
+ }
+ if let Some(patterns_value) = metadata.get("patterns").and_then(|v| v.as_array()) {
+ for value in patterns_value {
+ if let Some(pattern) = value.as_str() {
+ patterns.push(pattern.to_string());
+ }
+ }
+ }
+ patterns
+}
+
+async fn apply_universal_event(state: Arc, event: UniversalEvent) {
+ match event.event_type {
+ UniversalEventType::ItemStarted | UniversalEventType::ItemCompleted => {
+ if let UniversalEventData::Item(ItemEventData { item }) = &event.data {
+ apply_item_event(state, event.clone(), item.clone()).await;
+ }
+ }
+ UniversalEventType::ItemDelta => {
+ if let UniversalEventData::ItemDelta(ItemDeltaData {
+ item_id,
+ native_item_id,
+ delta,
+ }) = &event.data
+ {
+ apply_item_delta(
+ state,
+ event.clone(),
+ item_id.clone(),
+ native_item_id.clone(),
+ delta.clone(),
+ )
+ .await;
+ }
+ }
+ UniversalEventType::SessionEnded => {
+ let session_id = event.session_id.clone();
+ state.opencode.emit_event(json!({
+ "type": "session.status",
+ "properties": {"sessionID": session_id, "status": {"type": "idle"}}
+ }));
+ state.opencode.emit_event(json!({
+ "type": "session.idle",
+ "properties": {"sessionID": event.session_id}
+ }));
+ }
+ UniversalEventType::PermissionRequested | UniversalEventType::PermissionResolved => {
+ if let UniversalEventData::Permission(permission) = &event.data {
+ apply_permission_event(state, event.clone(), permission.clone()).await;
+ }
+ }
+ UniversalEventType::QuestionRequested | UniversalEventType::QuestionResolved => {
+ if let UniversalEventData::Question(question) = &event.data {
+ apply_question_event(state, event.clone(), question.clone()).await;
+ }
+ }
+ UniversalEventType::Error => {
+ if let UniversalEventData::Error(error) = &event.data {
+ state.opencode.emit_event(json!({
+ "type": "session.error",
+ "properties": {
+ "sessionID": event.session_id,
+ "error": {
+ "data": {"message": error.message},
+ "code": error.code,
+ "details": error.details,
+ }
+ }
+ }));
+ }
+ }
+ _ => {}
+ }
+}
+
+async fn apply_permission_event(
+ state: Arc,
+ event: UniversalEvent,
+ permission: PermissionEventData,
+) {
+ let session_id = event.session_id.clone();
+ match permission.status {
+ PermissionStatus::Requested => {
+ let record = OpenCodePermissionRecord {
+ id: permission.permission_id.clone(),
+ session_id: session_id.clone(),
+ permission: permission.action.clone(),
+ patterns: patterns_from_metadata(&permission.metadata),
+ metadata: permission.metadata.clone().unwrap_or_else(|| json!({})),
+ always: Vec::new(),
+ tool: None,
+ };
+ let value = record.to_value();
+ let mut permissions = state.opencode.permissions.lock().await;
+ permissions.insert(record.id.clone(), record);
+ drop(permissions);
+ state
+ .opencode
+ .emit_event(permission_event("permission.asked", &value));
+ }
+ PermissionStatus::Approved | PermissionStatus::Denied => {
+ let reply = match permission.status {
+ PermissionStatus::Approved => "once",
+ PermissionStatus::Denied => "reject",
+ PermissionStatus::Requested => "once",
+ };
+ let event_value = json!({
+ "sessionID": session_id,
+ "requestID": permission.permission_id,
+ "reply": reply,
+ });
+ let mut permissions = state.opencode.permissions.lock().await;
+ permissions.remove(&permission.permission_id);
+ drop(permissions);
+ state
+ .opencode
+ .emit_event(permission_event("permission.replied", &event_value));
+ }
+ }
+}
+
+async fn apply_question_event(
+ state: Arc,
+ event: UniversalEvent,
+ question: QuestionEventData,
+) {
+ let session_id = event.session_id.clone();
+ match question.status {
+ QuestionStatus::Requested => {
+ let options: Vec = question
+ .options
+ .iter()
+ .map(|option| {
+ json!({
+ "label": option,
+ "description": ""
+ })
+ })
+ .collect();
+ let question_info = json!({
+ "header": "Question",
+ "question": question.prompt,
+ "options": options,
+ });
+ let record = OpenCodeQuestionRecord {
+ id: question.question_id.clone(),
+ session_id: session_id.clone(),
+ questions: vec![question_info],
+ tool: None,
+ };
+ let value = record.to_value();
+ let mut questions = state.opencode.questions.lock().await;
+ questions.insert(record.id.clone(), record);
+ drop(questions);
+ state
+ .opencode
+ .emit_event(question_event("question.asked", &value));
+ }
+ QuestionStatus::Answered => {
+ let answers = question
+ .response
+ .clone()
+ .map(|answer| vec![vec![answer]])
+ .unwrap_or_else(|| Vec::>::new());
+ let event_value = json!({
+ "sessionID": session_id,
+ "requestID": question.question_id,
+ "answers": answers,
+ });
+ let mut questions = state.opencode.questions.lock().await;
+ questions.remove(&question.question_id);
+ drop(questions);
+ state
+ .opencode
+ .emit_event(question_event("question.replied", &event_value));
+ }
+ QuestionStatus::Rejected => {
+ let event_value = json!({
+ "sessionID": session_id,
+ "requestID": question.question_id,
+ });
+ let mut questions = state.opencode.questions.lock().await;
+ questions.remove(&question.question_id);
+ drop(questions);
+ state
+ .opencode
+ .emit_event(question_event("question.rejected", &event_value));
+ }
+ }
+}
+
+async fn apply_item_event(
+ state: Arc,
+ event: UniversalEvent,
+ item: UniversalItem,
+) {
+ if matches!(item.kind, ItemKind::ToolCall | ItemKind::ToolResult) {
+ apply_tool_item_event(state, event, item).await;
+ return;
+ }
+ if item.kind != ItemKind::Message {
+ return;
+ }
+ if matches!(item.role, Some(ItemRole::User)) {
+ return;
+ }
+ let session_id = event.session_id.clone();
+ let item_id_key = if item.item_id.is_empty() {
+ None
+ } else {
+ Some(item.item_id.clone())
+ };
+ let native_id_key = item.native_item_id.clone();
+ let mut message_id: Option = None;
+ let mut parent_id: Option = None;
+ let runtime = state
+ .opencode
+ .update_runtime(&session_id, |runtime| {
+ parent_id = item
+ .parent_id
+ .as_ref()
+ .and_then(|parent| runtime.message_id_for_item.get(parent).cloned())
+ .or_else(|| runtime.last_user_message_id.clone());
+ if let Some(existing) = item_id_key
+ .clone()
+ .and_then(|key| runtime.message_id_for_item.get(&key).cloned())
+ .or_else(|| {
+ native_id_key
+ .clone()
+ .and_then(|key| runtime.message_id_for_item.get(&key).cloned())
+ })
+ {
+ message_id = Some(existing);
+ } else {
+ let new_id =
+ unique_assistant_message_id(runtime, parent_id.as_ref(), event.sequence);
+ message_id = Some(new_id);
+ }
+ if let Some(id) = message_id.clone() {
+ if let Some(item_key) = item_id_key.clone() {
+ runtime.message_id_for_item.insert(item_key, id.clone());
+ }
+ if let Some(native_key) = native_id_key.clone() {
+ runtime.message_id_for_item.insert(native_key, id.clone());
+ }
+ }
+ })
+ .await;
+ let message_id = message_id.unwrap_or_else(|| {
+ unique_assistant_message_id(&runtime, parent_id.as_ref(), event.sequence)
+ });
+ let parent_id = parent_id.or_else(|| runtime.last_user_message_id.clone());
+ let agent = runtime
+ .last_agent
+ .clone()
+ .unwrap_or_else(|| default_agent_mode().to_string());
+ let provider_id = runtime
+ .last_model_provider
+ .clone()
+ .unwrap_or_else(|| OPENCODE_PROVIDER_ID.to_string());
+ let model_id = runtime
+ .last_model_id
+ .clone()
+ .unwrap_or_else(|| OPENCODE_DEFAULT_MODEL_ID.to_string());
+ let directory = session_directory(&state.opencode, &session_id).await;
+ let worktree = state.opencode.worktree_for(&directory);
+ let now = state.opencode.now_ms();
+
+ let mut info = build_assistant_message(
+ &session_id,
+ &message_id,
+ parent_id.as_deref().unwrap_or(""),
+ now,
+ &directory,
+ &worktree,
+ &agent,
+ &provider_id,
+ &model_id,
+ );
+ if event.event_type == UniversalEventType::ItemCompleted {
+ if let Some(obj) = info.as_object_mut() {
+ if let Some(time) = obj.get_mut("time").and_then(|v| v.as_object_mut()) {
+ time.insert("completed".to_string(), json!(now));
+ }
+ }
+ }
+ upsert_message_info(&state.opencode, &session_id, info.clone()).await;
+ state
+ .opencode
+ .emit_event(message_event("message.updated", &info));
+
+ let mut runtime = state
+ .opencode
+ .update_runtime(&session_id, |runtime| {
+ if runtime.last_user_message_id.is_none() {
+ runtime.last_user_message_id = parent_id.clone();
+ }
+ })
+ .await;
+
+ if let Some(text) = extract_text_from_content(&item.content) {
+ let part_id = runtime
+ .part_id_by_message
+ .entry(message_id.clone())
+ .or_insert_with(|| format!("{}_text", message_id))
+ .clone();
+ runtime
+ .text_by_message
+ .insert(message_id.clone(), text.clone());
+ let part = build_text_part_with_id(&session_id, &message_id, &part_id, &text);
+ upsert_message_part(&state.opencode, &session_id, &message_id, part.clone()).await;
+ state
+ .opencode
+ .emit_event(part_event("message.part.updated", &part));
+ let _ = state
+ .opencode
+ .update_runtime(&session_id, |runtime| {
+ runtime
+ .text_by_message
+ .insert(message_id.clone(), text.clone());
+ runtime
+ .part_id_by_message
+ .insert(message_id.clone(), part_id.clone());
+ })
+ .await;
+ }
+
+ for part in item.content.iter() {
+ match part {
+ ContentPart::Reasoning { text, .. } => {
+ let part_id = next_id("part_", &PART_COUNTER);
+ let reasoning_part =
+ build_reasoning_part(&session_id, &message_id, &part_id, text, now);
+ upsert_message_part(
+ &state.opencode,
+ &session_id,
+ &message_id,
+ reasoning_part.clone(),
+ )
+ .await;
+ state
+ .opencode
+ .emit_event(part_event("message.part.updated", &reasoning_part));
+ }
+ ContentPart::ToolCall {
+ name,
+ arguments,
+ call_id,
+ } => {
+ let part_id = runtime
+ .tool_part_by_call
+ .entry(call_id.clone())
+ .or_insert_with(|| next_id("part_", &PART_COUNTER))
+ .clone();
+ let state_value = json!({
+ "status": "pending",
+ "input": {"arguments": arguments},
+ "raw": arguments,
+ });
+ let tool_part = build_tool_part(
+ &session_id,
+ &message_id,
+ &part_id,
+ call_id,
+ name,
+ state_value,
+ );
+ upsert_message_part(&state.opencode, &session_id, &message_id, tool_part.clone())
+ .await;
+ state
+ .opencode
+ .emit_event(part_event("message.part.updated", &tool_part));
+ let _ = state
+ .opencode
+ .update_runtime(&session_id, |runtime| {
+ runtime
+ .tool_part_by_call
+ .insert(call_id.clone(), part_id.clone());
+ runtime
+ .tool_message_by_call
+ .insert(call_id.clone(), message_id.clone());
+ })
+ .await;
+ }
+ ContentPart::ToolResult { call_id, output } => {
+ let part_id = runtime
+ .tool_part_by_call
+ .entry(call_id.clone())
+ .or_insert_with(|| next_id("part_", &PART_COUNTER))
+ .clone();
+ let state_value = json!({
+ "status": "completed",
+ "input": {},
+ "output": output,
+ "title": "Tool result",
+ "metadata": {},
+ "time": {"start": now, "end": now},
+ "attachments": [],
+ });
+ let tool_part = build_tool_part(
+ &session_id,
+ &message_id,
+ &part_id,
+ call_id,
+ "tool",
+ state_value,
+ );
+ upsert_message_part(&state.opencode, &session_id, &message_id, tool_part.clone())
+ .await;
+ state
+ .opencode
+ .emit_event(part_event("message.part.updated", &tool_part));
+ let _ = state
+ .opencode
+ .update_runtime(&session_id, |runtime| {
+ runtime
+ .tool_part_by_call
+ .insert(call_id.clone(), part_id.clone());
+ runtime
+ .tool_message_by_call
+ .insert(call_id.clone(), message_id.clone());
+ })
+ .await;
+ }
+ ContentPart::FileRef { path, action, diff } => {
+ let mime = match action {
+ FileAction::Patch => "text/x-diff",
+ _ => "text/plain",
+ };
+ let part = build_file_part_from_path(
+ &session_id,
+ &message_id,
+ path,
+ mime,
+ diff.as_deref(),
+ );
+ upsert_message_part(&state.opencode, &session_id, &message_id, part.clone()).await;
+ state
+ .opencode
+ .emit_event(part_event("message.part.updated", &part));
+ if matches!(action, FileAction::Write | FileAction::Patch) {
+ emit_file_edited(&state.opencode, path);
+ }
+ }
+ ContentPart::Image { path, mime } => {
+ let mime = mime.as_deref().unwrap_or("image/png");
+ let part = build_file_part_from_path(&session_id, &message_id, path, mime, None);
+ upsert_message_part(&state.opencode, &session_id, &message_id, part.clone()).await;
+ state
+ .opencode
+ .emit_event(part_event("message.part.updated", &part));
+ }
+ _ => {}
+ }
+ }
+
+ if event.event_type == UniversalEventType::ItemCompleted {
+ state.opencode.emit_event(json!({
+ "type": "session.status",
+ "properties": {
+ "sessionID": session_id,
+ "status": {"type": "idle"}
+ }
+ }));
+ state.opencode.emit_event(json!({
+ "type": "session.idle",
+ "properties": { "sessionID": session_id }
+ }));
+ }
+}
+
+async fn apply_tool_item_event(
+ state: Arc,
+ event: UniversalEvent,
+ item: UniversalItem,
+) {
+ let session_id = event.session_id.clone();
+ let tool_info = extract_tool_content(&item.content);
+ let call_id = match tool_info.call_id.clone() {
+ Some(call_id) => call_id,
+ None => return,
+ };
+
+ let item_id_key = if item.item_id.is_empty() {
+ None
+ } else {
+ Some(item.item_id.clone())
+ };
+ let native_id_key = item.native_item_id.clone();
+ let mut message_id: Option = None;
+ let mut parent_id: Option = None;
+ let runtime = state
+ .opencode
+ .update_runtime(&session_id, |runtime| {
+ parent_id = item
+ .parent_id
+ .as_ref()
+ .and_then(|parent| runtime.message_id_for_item.get(parent).cloned())
+ .or_else(|| runtime.last_user_message_id.clone());
+ if let Some(existing) = item_id_key
+ .clone()
+ .and_then(|key| runtime.message_id_for_item.get(&key).cloned())
+ .or_else(|| {
+ native_id_key
+ .clone()
+ .and_then(|key| runtime.message_id_for_item.get(&key).cloned())
+ })
+ .or_else(|| runtime.tool_message_by_call.get(&call_id).cloned())
+ {
+ message_id = Some(existing);
+ } else {
+ let new_id =
+ unique_assistant_message_id(runtime, parent_id.as_ref(), event.sequence);
+ message_id = Some(new_id);
+ }
+ if let Some(id) = message_id.clone() {
+ if let Some(item_key) = item_id_key.clone() {
+ runtime.message_id_for_item.insert(item_key, id.clone());
+ }
+ if let Some(native_key) = native_id_key.clone() {
+ runtime.message_id_for_item.insert(native_key, id.clone());
+ }
+ runtime
+ .tool_message_by_call
+ .insert(call_id.clone(), id.clone());
+ }
+ })
+ .await;
+
+ let message_id = message_id.unwrap_or_else(|| {
+ unique_assistant_message_id(&runtime, parent_id.as_ref(), event.sequence)
+ });
+ let parent_id = parent_id.or_else(|| runtime.last_user_message_id.clone());
+ let agent = runtime
+ .last_agent
+ .clone()
+ .unwrap_or_else(|| default_agent_mode().to_string());
+ let provider_id = runtime
+ .last_model_provider
+ .clone()
+ .unwrap_or_else(|| OPENCODE_PROVIDER_ID.to_string());
+ let model_id = runtime
+ .last_model_id
+ .clone()
+ .unwrap_or_else(|| OPENCODE_DEFAULT_MODEL_ID.to_string());
+ let directory = session_directory(&state.opencode, &session_id).await;
+ let worktree = state.opencode.worktree_for(&directory);
+ let now = state.opencode.now_ms();
+
+ let mut info = build_assistant_message(
+ &session_id,
+ &message_id,
+ parent_id.as_deref().unwrap_or(""),
+ now,
+ &directory,
+ &worktree,
+ &agent,
+ &provider_id,
+ &model_id,
+ );
+ if event.event_type == UniversalEventType::ItemCompleted {
+ if let Some(obj) = info.as_object_mut() {
+ if let Some(time) = obj.get_mut("time").and_then(|v| v.as_object_mut()) {
+ time.insert("completed".to_string(), json!(now));
+ }
+ }
+ }
+ upsert_message_info(&state.opencode, &session_id, info.clone()).await;
+ state
+ .opencode
+ .emit_event(message_event("message.updated", &info));
+
+ let mut attachments = Vec::new();
+ if item.kind == ItemKind::ToolResult && event.event_type == UniversalEventType::ItemCompleted {
+ for (path, action, diff) in tool_info.file_refs.iter() {
+ let mime = match action {
+ FileAction::Patch => "text/x-diff",
+ _ => "text/plain",
+ };
+ let part =
+ build_file_part_from_path(&session_id, &message_id, path, mime, diff.as_deref());
+ upsert_message_part(&state.opencode, &session_id, &message_id, part.clone()).await;
+ state
+ .opencode
+ .emit_event(part_event("message.part.updated", &part));
+ attachments.push(part.clone());
+ if matches!(action, FileAction::Write | FileAction::Patch) {
+ emit_file_edited(&state.opencode, path);
+ }
+ }
+ }
+
+ let part_id = runtime
+ .tool_part_by_call
+ .get(&call_id)
+ .cloned()
+ .unwrap_or_else(|| next_id("part_", &PART_COUNTER));
+ let tool_name = tool_info
+ .tool_name
+ .clone()
+ .unwrap_or_else(|| "tool".to_string());
+ let input_value = tool_input_from_arguments(tool_info.arguments.as_deref());
+ let raw_args = tool_info.arguments.clone().unwrap_or_default();
+ let output_value = tool_info
+ .output
+ .clone()
+ .or_else(|| extract_text_from_content(&item.content));
+
+ let state_value = match event.event_type {
+ UniversalEventType::ItemStarted => {
+ if item.kind == ItemKind::ToolResult {
+ json!({
+ "status": "running",
+ "input": input_value,
+ "time": {"start": now}
+ })
+ } else {
+ json!({
+ "status": "pending",
+ "input": input_value,
+ "raw": raw_args,
+ })
+ }
+ }
+ UniversalEventType::ItemCompleted => {
+ if item.kind == ItemKind::ToolResult {
+ if matches!(item.status, ItemStatus::Failed) {
+ json!({
+ "status": "error",
+ "input": input_value,
+ "error": output_value.unwrap_or_else(|| "Tool failed".to_string()),
+ "metadata": {},
+ "time": {"start": now, "end": now},
+ })
+ } else {
+ json!({
+ "status": "completed",
+ "input": input_value,
+ "output": output_value.unwrap_or_default(),
+ "title": "Tool result",
+ "metadata": {},
+ "time": {"start": now, "end": now},
+ "attachments": attachments,
+ })
+ }
+ } else {
+ json!({
+ "status": "running",
+ "input": input_value,
+ "time": {"start": now},
+ })
+ }
+ }
+ _ => json!({
+ "status": "pending",
+ "input": input_value,
+ "raw": raw_args,
+ }),
+ };
+
+ let tool_part = build_tool_part(
+ &session_id,
+ &message_id,
+ &part_id,
+ &call_id,
+ &tool_name,
+ state_value,
+ );
+ upsert_message_part(&state.opencode, &session_id, &message_id, tool_part.clone()).await;
+ state
+ .opencode
+ .emit_event(part_event("message.part.updated", &tool_part));
+
+ let _ = state
+ .opencode
+ .update_runtime(&session_id, |runtime| {
+ runtime
+ .tool_part_by_call
+ .insert(call_id.clone(), part_id.clone());
+ runtime
+ .tool_message_by_call
+ .insert(call_id.clone(), message_id.clone());
+ })
+ .await;
+}
+
+async fn apply_item_delta(
+ state: Arc,
+ event: UniversalEvent,
+ item_id: String,
+ native_item_id: Option,
+ delta: String,
+) {
+ let session_id = event.session_id.clone();
+ let item_id_key = if item_id.is_empty() {
+ None
+ } else {
+ Some(item_id)
+ };
+ let native_id_key = native_item_id;
+ let is_user_delta = item_id_key
+ .as_ref()
+ .map(|value| value.starts_with("user_"))
+ .unwrap_or(false)
+ || native_id_key
+ .as_ref()
+ .map(|value| value.starts_with("user_"))
+ .unwrap_or(false);
+ if is_user_delta {
+ return;
+ }
+ let mut message_id: Option = None;
+ let mut parent_id: Option = None;
+ let runtime = state
+ .opencode
+ .update_runtime(&session_id, |runtime| {
+ parent_id = runtime.last_user_message_id.clone();
+ if let Some(existing) = item_id_key
+ .clone()
+ .and_then(|key| runtime.message_id_for_item.get(&key).cloned())
+ .or_else(|| {
+ native_id_key
+ .clone()
+ .and_then(|key| runtime.message_id_for_item.get(&key).cloned())
+ })
+ {
+ message_id = Some(existing);
+ } else {
+ let new_id =
+ unique_assistant_message_id(runtime, parent_id.as_ref(), event.sequence);
+ message_id = Some(new_id);
+ }
+ if let Some(id) = message_id.clone() {
+ if let Some(item_key) = item_id_key.clone() {
+ runtime.message_id_for_item.insert(item_key, id.clone());
+ }
+ if let Some(native_key) = native_id_key.clone() {
+ runtime.message_id_for_item.insert(native_key, id.clone());
+ }
+ }
+ })
+ .await;
+ let message_id = message_id.unwrap_or_else(|| {
+ unique_assistant_message_id(&runtime, parent_id.as_ref(), event.sequence)
+ });
+ let parent_id = parent_id.or_else(|| runtime.last_user_message_id.clone());
+ let directory = session_directory(&state.opencode, &session_id).await;
+ let worktree = state.opencode.worktree_for(&directory);
+ let now = state.opencode.now_ms();
+ let agent = runtime
+ .last_agent
+ .clone()
+ .unwrap_or_else(|| default_agent_mode().to_string());
+ let provider_id = runtime
+ .last_model_provider
+ .clone()
+ .unwrap_or_else(|| OPENCODE_PROVIDER_ID.to_string());
+ let model_id = runtime
+ .last_model_id
+ .clone()
+ .unwrap_or_else(|| OPENCODE_DEFAULT_MODEL_ID.to_string());
+ let info = build_assistant_message(
+ &session_id,
+ &message_id,
+ parent_id.as_deref().unwrap_or(""),
+ now,
+ &directory,
+ &worktree,
+ &agent,
+ &provider_id,
+ &model_id,
+ );
+ upsert_message_info(&state.opencode, &session_id, info.clone()).await;
+ state
+ .opencode
+ .emit_event(message_event("message.updated", &info));
+ let mut text = runtime
+ .text_by_message
+ .get(&message_id)
+ .cloned()
+ .unwrap_or_default();
+ text.push_str(&delta);
+ let part_id = runtime
+ .part_id_by_message
+ .get(&message_id)
+ .cloned()
+ .unwrap_or_else(|| format!("{}_text", message_id));
+ let part = build_text_part_with_id(&session_id, &message_id, &part_id, &text);
+ upsert_message_part(&state.opencode, &session_id, &message_id, part.clone()).await;
+ state
+ .opencode
+ .emit_event(part_event("message.part.updated", &part));
+ let _ = state
+ .opencode
+ .update_runtime(&session_id, |runtime| {
+ runtime.text_by_message.insert(message_id.clone(), text);
+ runtime
+ .part_id_by_message
+ .insert(message_id.clone(), part_id.clone());
+ })
+ .await;
+}
+
+/// Build OpenCode-compatible router.
+pub fn build_opencode_router(state: Arc) -> Router {
+ Router::new()
+ // Core metadata
+ .route("/agent", get(oc_agent_list))
+ .route("/command", get(oc_command_list))
+ .route("/config", get(oc_config_get).patch(oc_config_patch))
+ .route("/config/providers", get(oc_config_providers))
+ .route("/event", get(oc_event_subscribe))
+ .route("/global/event", get(oc_global_event))
+ .route("/global/health", get(oc_global_health))
+ .route(
+ "/global/config",
+ get(oc_global_config_get).patch(oc_global_config_patch),
+ )
+ .route("/global/dispose", post(oc_global_dispose))
+ .route("/instance/dispose", post(oc_instance_dispose))
+ .route("/log", post(oc_log))
+ .route("/lsp", get(oc_lsp_status))
+ .route("/formatter", get(oc_formatter_status))
+ .route("/path", get(oc_path))
+ .route("/vcs", get(oc_vcs))
+ .route("/project", get(oc_project_list))
+ .route("/project/current", get(oc_project_current))
+ .route("/project/:projectID", patch(oc_project_update))
+ // Sessions
+ .route("/session", post(oc_session_create).get(oc_session_list))
+ .route("/session/status", get(oc_session_status))
+ .route(
+ "/session/:sessionID",
+ get(oc_session_get)
+ .patch(oc_session_update)
+ .delete(oc_session_delete),
+ )
+ .route("/session/:sessionID/abort", post(oc_session_abort))
+ .route("/session/:sessionID/children", get(oc_session_children))
+ .route("/session/:sessionID/init", post(oc_session_init))
+ .route("/session/:sessionID/fork", post(oc_session_fork))
+ .route("/session/:sessionID/diff", get(oc_session_diff))
+ .route("/session/:sessionID/summarize", post(oc_session_summarize))
+ .route(
+ "/session/:sessionID/message",
+ post(oc_session_message_create).get(oc_session_messages),
+ )
+ .route(
+ "/session/:sessionID/message/:messageID",
+ get(oc_session_message_get),
+ )
+ .route(
+ "/session/:sessionID/message/:messageID/part/:partID",
+ patch(oc_message_part_update).delete(oc_message_part_delete),
+ )
+ .route(
+ "/session/:sessionID/prompt_async",
+ post(oc_session_prompt_async),
+ )
+ .route("/session/:sessionID/command", post(oc_session_command))
+ .route("/session/:sessionID/shell", post(oc_session_shell))
+ .route("/session/:sessionID/revert", post(oc_session_revert))
+ .route("/session/:sessionID/unrevert", post(oc_session_unrevert))
+ .route(
+ "/session/:sessionID/permissions/:permissionID",
+ post(oc_session_permission_reply),
+ )
+ .route(
+ "/session/:sessionID/share",
+ post(oc_session_share).delete(oc_session_unshare),
+ )
+ .route("/session/:sessionID/todo", get(oc_session_todo))
+ // Permissions + questions (global)
+ .route("/permission", get(oc_permission_list))
+ .route("/permission/:requestID/reply", post(oc_permission_reply))
+ .route("/question", get(oc_question_list))
+ .route("/question/:requestID/reply", post(oc_question_reply))
+ .route("/question/:requestID/reject", post(oc_question_reject))
+ // Providers
+ .route("/provider", get(oc_provider_list))
+ .route("/provider/auth", get(oc_provider_auth))
+ .route(
+ "/provider/:providerID/oauth/authorize",
+ post(oc_provider_oauth_authorize),
+ )
+ .route(
+ "/provider/:providerID/oauth/callback",
+ post(oc_provider_oauth_callback),
+ )
+ // Auth
+ .route("/auth/:providerID", put(oc_auth_set).delete(oc_auth_remove))
+ // PTY
+ .route("/pty", get(oc_pty_list).post(oc_pty_create))
+ .route(
+ "/pty/:ptyID",
+ get(oc_pty_get).put(oc_pty_update).delete(oc_pty_delete),
+ )
+ .route("/pty/:ptyID/connect", get(oc_pty_connect))
+ // Files
+ .route("/file", get(oc_file_list))
+ .route("/file/content", get(oc_file_content))
+ .route("/file/status", get(oc_file_status))
+ // Find
+ .route("/find", get(oc_find_text))
+ .route("/find/file", get(oc_find_files))
+ .route("/find/symbol", get(oc_find_symbols))
+ // MCP
+ .route("/mcp", get(oc_mcp_list).post(oc_mcp_register))
+ .route(
+ "/mcp/:name/auth",
+ post(oc_mcp_auth).delete(oc_mcp_auth_remove),
+ )
+ .route("/mcp/:name/auth/callback", post(oc_mcp_auth_callback))
+ .route("/mcp/:name/auth/authenticate", post(oc_mcp_authenticate))
+ .route("/mcp/:name/connect", post(oc_mcp_connect))
+ .route("/mcp/:name/disconnect", post(oc_mcp_disconnect))
+ // Experimental
+ .route("/experimental/tool/ids", get(oc_tool_ids))
+ .route("/experimental/tool", get(oc_tool_list))
+ .route("/experimental/resource", get(oc_resource_list))
+ .route(
+ "/experimental/worktree",
+ get(oc_worktree_list)
+ .post(oc_worktree_create)
+ .delete(oc_worktree_delete),
+ )
+ .route("/experimental/worktree/reset", post(oc_worktree_reset))
+ // Skills
+ .route("/skill", get(oc_skill_list))
+ // TUI
+ .route("/tui/control/next", get(oc_tui_next))
+ .route("/tui/control/response", post(oc_tui_response))
+ .route("/tui/append-prompt", post(oc_tui_append_prompt))
+ .route("/tui/open-help", post(oc_tui_open_help))
+ .route("/tui/open-sessions", post(oc_tui_open_sessions))
+ .route("/tui/open-themes", post(oc_tui_open_themes))
+ .route("/tui/open-models", post(oc_tui_open_models))
+ .route("/tui/submit-prompt", post(oc_tui_submit_prompt))
+ .route("/tui/clear-prompt", post(oc_tui_clear_prompt))
+ .route("/tui/execute-command", post(oc_tui_execute_command))
+ .route("/tui/show-toast", post(oc_tui_show_toast))
+ .route("/tui/publish", post(oc_tui_publish))
+ .route("/tui/select-session", post(oc_tui_select_session))
+ .with_state(state)
+}
+
+// ===================================================================================
+// Handler implementations
+// ===================================================================================
+
+#[utoipa::path(
+ get,
+ path = "/agent",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_agent_list(State(state): State>) -> impl IntoResponse {
+ let agent = json!({
+ "name": OPENCODE_PROVIDER_NAME,
+ "description": "Sandbox Agent compatibility layer",
+ "mode": "all",
+ "native": false,
+ "hidden": false,
+ "permission": [],
+ "options": {},
+ });
+ (StatusCode::OK, Json(json!([agent])))
+}
+
+#[utoipa::path(
+ get,
+ path = "/command",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_command_list() -> impl IntoResponse {
+ (StatusCode::OK, Json(json!([])))
+}
+
+#[utoipa::path(
+ get,
+ path = "/config",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_config_get() -> impl IntoResponse {
+ (StatusCode::OK, Json(json!({})))
+}
+
+#[utoipa::path(
+ patch,
+ path = "/config",
+ request_body = String,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_config_patch(Json(body): Json) -> impl IntoResponse {
+ (StatusCode::OK, Json(body))
+}
+
+#[utoipa::path(
+ get,
+ path = "/config/providers",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_config_providers() -> impl IntoResponse {
+ let mut models = serde_json::Map::new();
+ for agent in available_agent_ids() {
+ models.insert(agent.as_str().to_string(), model_config_entry(agent));
+ }
+ let providers = json!({
+ "providers": [
+ {
+ "id": OPENCODE_PROVIDER_ID,
+ "name": OPENCODE_PROVIDER_NAME,
+ "source": "custom",
+ "env": [],
+ "key": "",
+ "options": {},
+ "models": Value::Object(models),
+ }
+ ],
+ "default": {
+ OPENCODE_PROVIDER_ID: OPENCODE_DEFAULT_MODEL_ID
+ }
+ });
+ (StatusCode::OK, Json(providers))
+}
+
+#[utoipa::path(
+ get,
+ path = "/event",
+ responses((status = 200, description = "SSE event stream")),
+ tag = "opencode"
+)]
+async fn oc_event_subscribe(
+ State(state): State>,
+ headers: HeaderMap,
+ Query(query): Query,
+) -> Sse>> {
+ let receiver = state.opencode.subscribe();
+ let directory = state
+ .opencode
+ .directory_for(&headers, query.directory.as_ref());
+ let branch = state.opencode.branch_name();
+ state.opencode.emit_event(json!({
+ "type": "server.connected",
+ "properties": {}
+ }));
+ state.opencode.emit_event(json!({
+ "type": "worktree.ready",
+ "properties": {
+ "name": directory,
+ "branch": branch,
+ }
+ }));
+
+ let heartbeat_payload = json!({
+ "type": "server.heartbeat",
+ "properties": {}
+ });
+ let stream = stream::unfold(
+ (receiver, interval(std::time::Duration::from_secs(30))),
+ move |(mut rx, mut ticker)| {
+ let heartbeat = heartbeat_payload.clone();
+ async move {
+ tokio::select! {
+ _ = ticker.tick() => {
+ let sse_event = Event::default()
+ .json_data(&heartbeat)
+ .unwrap_or_else(|_| Event::default().data("{}"));
+ Some((Ok(sse_event), (rx, ticker)))
+ }
+ event = rx.recv() => {
+ match event {
+ Ok(event) => {
+ let sse_event = Event::default()
+ .json_data(&event)
+ .unwrap_or_else(|_| Event::default().data("{}"));
+ Some((Ok(sse_event), (rx, ticker)))
+ }
+ Err(broadcast::error::RecvError::Lagged(_)) => {
+ Some((Ok(Event::default().comment("lagged")), (rx, ticker)))
+ }
+ Err(broadcast::error::RecvError::Closed) => None,
+ }
+ }
+ }
+ }
+ },
+ );
+
+ Sse::new(stream).keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(15)))
+}
+
+#[utoipa::path(
+ get,
+ path = "/global/event",
+ responses((status = 200, description = "SSE event stream")),
+ tag = "opencode"
+)]
+async fn oc_global_event(
+ State(state): State>,
+ headers: HeaderMap,
+ Query(query): Query,
+) -> Sse>> {
+ let receiver = state.opencode.subscribe();
+ let directory = state
+ .opencode
+ .directory_for(&headers, query.directory.as_ref());
+ let branch = state.opencode.branch_name();
+ state.opencode.emit_event(json!({
+ "type": "server.connected",
+ "properties": {}
+ }));
+ state.opencode.emit_event(json!({
+ "type": "worktree.ready",
+ "properties": {
+ "name": directory.clone(),
+ "branch": branch,
+ }
+ }));
+
+ let heartbeat_payload = json!({
+ "payload": {
+ "type": "server.heartbeat",
+ "properties": {}
+ }
+ });
+ let stream = stream::unfold(
+ (receiver, interval(std::time::Duration::from_secs(30))),
+ move |(mut rx, mut ticker)| {
+ let directory = directory.clone();
+ let heartbeat = heartbeat_payload.clone();
+ async move {
+ tokio::select! {
+ _ = ticker.tick() => {
+ let sse_event = Event::default()
+ .json_data(&heartbeat)
+ .unwrap_or_else(|_| Event::default().data("{}"));
+ Some((Ok(sse_event), (rx, ticker)))
+ }
+ event = rx.recv() => {
+ match event {
+ Ok(event) => {
+ let payload = json!({"directory": directory, "payload": event});
+ let sse_event = Event::default()
+ .json_data(&payload)
+ .unwrap_or_else(|_| Event::default().data("{}"));
+ Some((Ok(sse_event), (rx, ticker)))
+ }
+ Err(broadcast::error::RecvError::Lagged(_)) => {
+ Some((Ok(Event::default().comment("lagged")), (rx, ticker)))
+ }
+ Err(broadcast::error::RecvError::Closed) => None,
+ }
+ }
+ }
+ }
+ },
+ );
+
+ Sse::new(stream).keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(15)))
+}
+
+#[utoipa::path(
+ get,
+ path = "/global/health",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_global_health() -> impl IntoResponse {
+ (
+ StatusCode::OK,
+ Json(json!({
+ "healthy": true,
+ "version": env!("CARGO_PKG_VERSION"),
+ })),
+ )
+}
+
+#[utoipa::path(
+ get,
+ path = "/global/config",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_global_config_get() -> impl IntoResponse {
+ (StatusCode::OK, Json(json!({})))
+}
+
+#[utoipa::path(
+ patch,
+ path = "/global/config",
+ request_body = String,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_global_config_patch(Json(body): Json) -> impl IntoResponse {
+ (StatusCode::OK, Json(body))
+}
+
+#[utoipa::path(
+ post,
+ path = "/global/dispose",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_global_dispose() -> impl IntoResponse {
+ bool_ok(true)
+}
+
+#[utoipa::path(
+ post,
+ path = "/instance/dispose",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_instance_dispose() -> impl IntoResponse {
+ bool_ok(true)
+}
+
+#[utoipa::path(
+ post,
+ path = "/log",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_log() -> impl IntoResponse {
+ bool_ok(true)
+}
+
+#[utoipa::path(
+ get,
+ path = "/lsp",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_lsp_status() -> impl IntoResponse {
+ (StatusCode::OK, Json(json!([])))
+}
+
+#[utoipa::path(
+ get,
+ path = "/formatter",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_formatter_status() -> impl IntoResponse {
+ (StatusCode::OK, Json(json!([])))
+}
+
+#[utoipa::path(
+ get,
+ path = "/path",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_path(
+ State(state): State>,
+ headers: HeaderMap,
+) -> impl IntoResponse {
+ let directory = state.opencode.directory_for(&headers, None);
+ let worktree = state.opencode.worktree_for(&directory);
+ (
+ StatusCode::OK,
+ Json(json!({
+ "home": state.opencode.home_dir(),
+ "state": state.opencode.state_dir(),
+ "config": state.opencode.config_dir(),
+ "worktree": worktree,
+ "directory": directory,
+ })),
+ )
+}
+
+#[utoipa::path(
+ get,
+ path = "/vcs",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_vcs(State(state): State>) -> impl IntoResponse {
+ (
+ StatusCode::OK,
+ Json(json!({
+ "branch": state.opencode.branch_name(),
+ })),
+ )
+}
+
+#[utoipa::path(
+ get,
+ path = "/project",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_project_list(
+ State(state): State>,
+ headers: HeaderMap,
+) -> impl IntoResponse {
+ let directory = state.opencode.directory_for(&headers, None);
+ let worktree = state.opencode.worktree_for(&directory);
+ let now = state.opencode.now_ms();
+ let project = json!({
+ "id": state.opencode.default_project_id.clone(),
+ "worktree": worktree,
+ "vcs": "git",
+ "name": "sandbox-agent",
+ "time": {"created": now, "updated": now},
+ "sandboxes": [],
+ });
+ (StatusCode::OK, Json(json!([project])))
+}
+
+#[utoipa::path(
+ get,
+ path = "/project/current",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_project_current(
+ State(state): State>,
+ headers: HeaderMap,
+) -> impl IntoResponse {
+ let directory = state.opencode.directory_for(&headers, None);
+ let worktree = state.opencode.worktree_for(&directory);
+ let now = state.opencode.now_ms();
+ (
+ StatusCode::OK,
+ Json(json!({
+ "id": state.opencode.default_project_id.clone(),
+ "worktree": worktree,
+ "vcs": "git",
+ "name": "sandbox-agent",
+ "time": {"created": now, "updated": now},
+ "sandboxes": [],
+ })),
+ )
+}
+
+#[utoipa::path(
+ patch,
+ path = "/project/{projectID}",
+ params(("projectID" = String, Path, description = "Project ID")),
+ request_body = String,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_project_update(
+ State(state): State>,
+ Path(_project_id): Path,
+ headers: HeaderMap,
+) -> impl IntoResponse {
+ oc_project_current(State(state), headers).await
+}
+
+#[utoipa::path(
+ post,
+ path = "/session",
+ request_body = OpenCodeCreateSessionRequest,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_create(
+ State(state): State>,
+ headers: HeaderMap,
+ Query(query): Query,
+ body: Option>,
+) -> impl IntoResponse {
+ let body = body.map(|j| j.0).unwrap_or(OpenCodeCreateSessionRequest {
+ title: None,
+ parent_id: None,
+ permission: None,
+ });
+ let directory = state
+ .opencode
+ .directory_for(&headers, query.directory.as_ref());
+ let now = state.opencode.now_ms();
+ let id = next_id("ses_", &SESSION_COUNTER);
+ let slug = format!("session-{}", id);
+ let title = body.title.unwrap_or_else(|| format!("Session {}", id));
+ let record = OpenCodeSessionRecord {
+ id: id.clone(),
+ slug,
+ project_id: state.opencode.default_project_id.clone(),
+ directory,
+ parent_id: body.parent_id,
+ title,
+ version: "0".to_string(),
+ created_at: now,
+ updated_at: now,
+ share_url: None,
+ };
+
+ let session_value = record.to_value();
+
+ let mut sessions = state.opencode.sessions.lock().await;
+ sessions.insert(id.clone(), record);
+ drop(sessions);
+
+ state
+ .opencode
+ .emit_event(session_event("session.created", &session_value));
+
+ (StatusCode::OK, Json(session_value))
+}
+
+#[utoipa::path(
+ get,
+ path = "/session",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_list(State(state): State>) -> impl IntoResponse {
+ let sessions = state.opencode.sessions.lock().await;
+ let values: Vec = sessions.values().map(|s| s.to_value()).collect();
+ (StatusCode::OK, Json(json!(values)))
+}
+
+#[utoipa::path(
+ get,
+ path = "/session/{sessionID}",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ responses((status = 200), (status = 404)),
+ tag = "opencode"
+)]
+async fn oc_session_get(
+ State(state): State>,
+ Path(session_id): Path,
+ _headers: HeaderMap,
+ _query: Query,
+) -> impl IntoResponse {
+ let sessions = state.opencode.sessions.lock().await;
+ if let Some(session) = sessions.get(&session_id) {
+ return (StatusCode::OK, Json(session.to_value())).into_response();
+ }
+ not_found("Session not found").into_response()
+}
+
+#[utoipa::path(
+ patch,
+ path = "/session/{sessionID}",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ request_body = OpenCodeUpdateSessionRequest,
+ responses((status = 200), (status = 404)),
+ tag = "opencode"
+)]
+async fn oc_session_update(
+ State(state): State>,
+ Path(session_id): Path,
+ Json(body): Json,
+) -> impl IntoResponse {
+ let mut sessions = state.opencode.sessions.lock().await;
+ if let Some(session) = sessions.get_mut(&session_id) {
+ if let Some(title) = body.title {
+ session.title = title;
+ session.updated_at = state.opencode.now_ms();
+ }
+ let value = session.to_value();
+ state
+ .opencode
+ .emit_event(session_event("session.updated", &value));
+ return (StatusCode::OK, Json(value)).into_response();
+ }
+ not_found("Session not found").into_response()
+}
+
+#[utoipa::path(
+ delete,
+ path = "/session/{sessionID}",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ responses((status = 200), (status = 404)),
+ tag = "opencode"
+)]
+async fn oc_session_delete(
+ State(state): State>,
+ Path(session_id): Path,
+) -> impl IntoResponse {
+ let mut sessions = state.opencode.sessions.lock().await;
+ if let Some(session) = sessions.remove(&session_id) {
+ state
+ .opencode
+ .emit_event(session_event("session.deleted", &session.to_value()));
+ return bool_ok(true).into_response();
+ }
+ not_found("Session not found").into_response()
+}
+
+#[utoipa::path(
+ get,
+ path = "/session/status",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_status(State(state): State>) -> impl IntoResponse {
+ let sessions = state.opencode.sessions.lock().await;
+ let mut status_map = serde_json::Map::new();
+ for id in sessions.keys() {
+ status_map.insert(id.clone(), json!({"type": "idle"}));
+ }
+ (StatusCode::OK, Json(Value::Object(status_map)))
+}
+
+#[utoipa::path(
+ post,
+ path = "/session/{sessionID}/abort",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_abort(
+ State(_state): State>,
+ Path(_session_id): Path,
+) -> impl IntoResponse {
+ bool_ok(true)
+}
+
+#[utoipa::path(
+ get,
+ path = "/session/{sessionID}/children",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_children() -> impl IntoResponse {
+ (StatusCode::OK, Json(json!([])))
+}
+
+#[utoipa::path(
+ post,
+ path = "/session/{sessionID}/init",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_init() -> impl IntoResponse {
+ bool_ok(true)
+}
+
+#[utoipa::path(
+ post,
+ path = "/session/{sessionID}/fork",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ request_body = String,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_fork(
+ State(state): State>,
+ Path(session_id): Path,
+ headers: HeaderMap,
+ Query(query): Query,
+) -> impl IntoResponse {
+ let directory = state
+ .opencode
+ .directory_for(&headers, query.directory.as_ref());
+ let now = state.opencode.now_ms();
+ let id = next_id("ses_", &SESSION_COUNTER);
+ let slug = format!("session-{}", id);
+ let title = format!("Fork of {}", session_id);
+ let record = OpenCodeSessionRecord {
+ id: id.clone(),
+ slug,
+ project_id: state.opencode.default_project_id.clone(),
+ directory,
+ parent_id: Some(session_id),
+ title,
+ version: "0".to_string(),
+ created_at: now,
+ updated_at: now,
+ share_url: None,
+ };
+
+ let value = record.to_value();
+ let mut sessions = state.opencode.sessions.lock().await;
+ sessions.insert(id.clone(), record);
+ drop(sessions);
+
+ state
+ .opencode
+ .emit_event(session_event("session.created", &value));
+
+ (StatusCode::OK, Json(value))
+}
+
+#[utoipa::path(
+ get,
+ path = "/session/{sessionID}/diff",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_diff() -> impl IntoResponse {
+ (StatusCode::OK, Json(json!([])))
+}
+
+#[utoipa::path(
+ post,
+ path = "/session/{sessionID}/summarize",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ request_body = SessionSummarizeRequest,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_summarize(Json(body): Json) -> impl IntoResponse {
+ if body.provider_id.is_none() || body.model_id.is_none() {
+ return bad_request("providerID and modelID are required");
+ }
+ bool_ok(true)
+}
+
+#[utoipa::path(
+ get,
+ path = "/session/{sessionID}/message",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_messages(
+ State(state): State>,
+ Path(session_id): Path,
+) -> impl IntoResponse {
+ let messages = state.opencode.messages.lock().await;
+ let entries = messages.get(&session_id).cloned().unwrap_or_default();
+ let values: Vec = entries
+ .into_iter()
+ .map(|record| json!({"info": record.info, "parts": record.parts}))
+ .collect();
+ (StatusCode::OK, Json(json!(values)))
+}
+
+#[utoipa::path(
+ post,
+ path = "/session/{sessionID}/message",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ request_body = SessionMessageRequest,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_message_create(
+ State(state): State>,
+ Path(session_id): Path,
+ headers: HeaderMap,
+ Query(query): Query,
+ Json(body): Json,
+) -> impl IntoResponse {
+ if std::env::var("OPENCODE_COMPAT_LOG_BODY").is_ok() {
+ tracing::info!(
+ target = "sandbox_agent::opencode",
+ ?body,
+ "opencode prompt body"
+ );
+ }
+ let directory = state
+ .opencode
+ .directory_for(&headers, query.directory.as_ref());
+ let _ = state
+ .opencode
+ .ensure_session(&session_id, directory.clone())
+ .await;
+ let worktree = state.opencode.worktree_for(&directory);
+ let agent_mode = normalize_agent_mode(body.agent.clone());
+ let requested_provider = body
+ .model
+ .as_ref()
+ .and_then(|v| v.get("providerID"))
+ .and_then(|v| v.as_str());
+ let requested_model = body
+ .model
+ .as_ref()
+ .and_then(|v| v.get("modelID"))
+ .and_then(|v| v.as_str());
+ let (session_agent, provider_id, model_id) =
+ resolve_session_agent(&state, &session_id, requested_provider, requested_model).await;
+
+ let parts_input = body.parts.unwrap_or_default();
+ if parts_input.is_empty() {
+ return bad_request("parts are required").into_response();
+ }
+
+ let now = state.opencode.now_ms();
+ let user_message_id = body
+ .message_id
+ .clone()
+ .unwrap_or_else(|| next_id("msg_", &MESSAGE_COUNTER));
+
+ state.opencode.emit_event(json!({
+ "type": "session.status",
+ "properties": {
+ "sessionID": session_id,
+ "status": {"type": "busy"}
+ }
+ }));
+
+ let mut user_message = build_user_message(
+ &session_id,
+ &user_message_id,
+ now,
+ &agent_mode,
+ &provider_id,
+ &model_id,
+ );
+ if let Some(obj) = user_message.as_object_mut() {
+ if let Some(time) = obj.get_mut("time").and_then(|v| v.as_object_mut()) {
+ time.insert("completed".to_string(), json!(now));
+ }
+ }
+
+ let parts: Vec = parts_input
+ .iter()
+ .map(|part| normalize_part(&session_id, &user_message_id, part))
+ .collect();
+
+ upsert_message_info(&state.opencode, &session_id, user_message.clone()).await;
+ for part in &parts {
+ upsert_message_part(&state.opencode, &session_id, &user_message_id, part.clone()).await;
+ }
+
+ state
+ .opencode
+ .emit_event(message_event("message.updated", &user_message));
+ for part in &parts {
+ state
+ .opencode
+ .emit_event(part_event("message.part.updated", part));
+ }
+
+ let _ = state
+ .opencode
+ .update_runtime(&session_id, |runtime| {
+ runtime.last_user_message_id = Some(user_message_id.clone());
+ runtime.last_agent = Some(agent_mode.clone());
+ runtime.last_model_provider = Some(provider_id.clone());
+ runtime.last_model_id = Some(model_id.clone());
+ })
+ .await;
+
+ if let Err(err) = ensure_backing_session(&state, &session_id, &session_agent).await {
+ tracing::warn!(
+ target = "sandbox_agent::opencode",
+ ?err,
+ "failed to ensure backing session"
+ );
+ } else {
+ ensure_session_stream(state.clone(), session_id.clone()).await;
+ }
+
+ let prompt_text = parts_input
+ .iter()
+ .find_map(|part| part.get("text").and_then(|v| v.as_str()))
+ .unwrap_or("")
+ .to_string();
+ if !prompt_text.is_empty() {
+ if let Err(err) = state
+ .inner
+ .session_manager()
+ .send_message(session_id.clone(), prompt_text)
+ .await
+ {
+ tracing::warn!(
+ target = "sandbox_agent::opencode",
+ ?err,
+ "failed to send message to backing agent"
+ );
+ }
+ }
+
+ let assistant_message = build_assistant_message(
+ &session_id,
+ &format!("{user_message_id}_pending"),
+ &user_message_id,
+ now,
+ &directory,
+ &worktree,
+ &agent_mode,
+ &provider_id,
+ &model_id,
+ );
+
+ (
+ StatusCode::OK,
+ Json(json!({
+ "info": assistant_message,
+ "parts": [],
+ })),
+ )
+ .into_response()
+}
+
+#[utoipa::path(
+ get,
+ path = "/session/{sessionID}/message/{messageID}",
+ params(
+ ("sessionID" = String, Path, description = "Session ID"),
+ ("messageID" = String, Path, description = "Message ID")
+ ),
+ responses((status = 200), (status = 404)),
+ tag = "opencode"
+)]
+async fn oc_session_message_get(
+ State(state): State>,
+ Path((session_id, message_id)): Path<(String, String)>,
+) -> impl IntoResponse {
+ let messages = state.opencode.messages.lock().await;
+ if let Some(entries) = messages.get(&session_id) {
+ if let Some(record) = entries.iter().find(|record| {
+ record
+ .info
+ .get("id")
+ .and_then(|v| v.as_str())
+ .map(|id| id == message_id)
+ .unwrap_or(false)
+ }) {
+ return (
+ StatusCode::OK,
+ Json(json!({
+ "info": record.info.clone(),
+ "parts": record.parts.clone()
+ })),
+ )
+ .into_response();
+ }
+ }
+ not_found("Message not found").into_response()
+}
+
+#[utoipa::path(
+ patch,
+ path = "/session/{sessionID}/message/{messageID}/part/{partID}",
+ params(
+ ("sessionID" = String, Path, description = "Session ID"),
+ ("messageID" = String, Path, description = "Message ID"),
+ ("partID" = String, Path, description = "Part ID")
+ ),
+ request_body = String,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_message_part_update(
+ State(state): State>,
+ Path((session_id, message_id, part_id)): Path<(String, String, String)>,
+ Json(mut part_value): Json,
+) -> impl IntoResponse {
+ if let Some(obj) = part_value.as_object_mut() {
+ obj.insert("id".to_string(), json!(part_id));
+ obj.insert("sessionID".to_string(), json!(session_id));
+ obj.insert("messageID".to_string(), json!(message_id));
+ }
+
+ state
+ .opencode
+ .emit_event(part_event("message.part.updated", &part_value));
+
+ (StatusCode::OK, Json(part_value))
+}
+
+#[utoipa::path(
+ delete,
+ path = "/session/{sessionID}/message/{messageID}/part/{partID}",
+ params(
+ ("sessionID" = String, Path, description = "Session ID"),
+ ("messageID" = String, Path, description = "Message ID"),
+ ("partID" = String, Path, description = "Part ID")
+ ),
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_message_part_delete(
+ State(state): State>,
+ Path((session_id, message_id, part_id)): Path<(String, String, String)>,
+) -> impl IntoResponse {
+ let part_value = json!({
+ "id": part_id,
+ "sessionID": session_id,
+ "messageID": message_id,
+ "type": "text",
+ "text": "",
+ });
+ state
+ .opencode
+ .emit_event(part_event("message.part.removed", &part_value));
+ bool_ok(true)
+}
+
+#[utoipa::path(
+ post,
+ path = "/session/{sessionID}/prompt_async",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ request_body = SessionMessageRequest,
+ responses((status = 204)),
+ tag = "opencode"
+)]
+async fn oc_session_prompt_async(
+ State(state): State>,
+ Path(session_id): Path,
+ headers: HeaderMap,
+ Query(query): Query,
+ Json(body): Json,
+) -> impl IntoResponse {
+ let _ = oc_session_message_create(
+ State(state),
+ Path(session_id),
+ headers,
+ Query(query),
+ Json(body),
+ )
+ .await;
+ StatusCode::NO_CONTENT
+}
+
+#[utoipa::path(
+ post,
+ path = "/session/{sessionID}/command",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ request_body = SessionCommandRequest,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_command(
+ State(state): State>,
+ Path(session_id): Path,
+ headers: HeaderMap,
+ Query(query): Query,
+ Json(body): Json,
+) -> impl IntoResponse {
+ if body.command.is_none() || body.arguments.is_none() {
+ return bad_request("command and arguments are required").into_response();
+ }
+ let directory = state
+ .opencode
+ .directory_for(&headers, query.directory.as_ref());
+ let worktree = state.opencode.worktree_for(&directory);
+ let now = state.opencode.now_ms();
+ let assistant_message_id = next_id("msg_", &MESSAGE_COUNTER);
+ let agent = normalize_agent_mode(body.agent.clone());
+ let assistant_message = build_assistant_message(
+ &session_id,
+ &assistant_message_id,
+ "msg_parent",
+ now,
+ &directory,
+ &worktree,
+ &agent,
+ OPENCODE_PROVIDER_ID,
+ OPENCODE_DEFAULT_MODEL_ID,
+ );
+
+ (
+ StatusCode::OK,
+ Json(json!({
+ "info": assistant_message,
+ "parts": [],
+ })),
+ )
+ .into_response()
+}
+
+#[utoipa::path(
+ post,
+ path = "/session/{sessionID}/shell",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ request_body = SessionShellRequest,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_shell(
+ State(state): State>,
+ Path(session_id): Path,
+ headers: HeaderMap,
+ Query(query): Query,
+ Json(body): Json,
+) -> impl IntoResponse {
+ if body.command.is_none() || body.agent.is_none() {
+ return bad_request("agent and command are required").into_response();
+ }
+ let directory = state
+ .opencode
+ .directory_for(&headers, query.directory.as_ref());
+ let worktree = state.opencode.worktree_for(&directory);
+ let now = state.opencode.now_ms();
+ let assistant_message_id = next_id("msg_", &MESSAGE_COUNTER);
+ let assistant_message = build_assistant_message(
+ &session_id,
+ &assistant_message_id,
+ "msg_parent",
+ now,
+ &directory,
+ &worktree,
+ &normalize_agent_mode(body.agent.clone()),
+ body.model
+ .as_ref()
+ .and_then(|v| v.get("providerID"))
+ .and_then(|v| v.as_str())
+ .unwrap_or(OPENCODE_PROVIDER_ID),
+ body.model
+ .as_ref()
+ .and_then(|v| v.get("modelID"))
+ .and_then(|v| v.as_str())
+ .unwrap_or(OPENCODE_DEFAULT_MODEL_ID),
+ );
+ (StatusCode::OK, Json(assistant_message)).into_response()
+}
+
+#[utoipa::path(
+ post,
+ path = "/session/{sessionID}/revert",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ request_body = String,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_revert(
+ State(state): State>,
+ Path(session_id): Path,
+ headers: HeaderMap,
+ Query(query): Query,
+) -> impl IntoResponse {
+ oc_session_get(State(state), Path(session_id), headers, Query(query)).await
+}
+
+#[utoipa::path(
+ post,
+ path = "/session/{sessionID}/unrevert",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ request_body = String,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_unrevert(
+ State(state): State>,
+ Path(session_id): Path,
+ headers: HeaderMap,
+ Query(query): Query,
+) -> impl IntoResponse {
+ oc_session_get(State(state), Path(session_id), headers, Query(query)).await
+}
+
+#[utoipa::path(
+ post,
+ path = "/session/{sessionID}/permissions/{permissionID}",
+ params(
+ ("sessionID" = String, Path, description = "Session ID"),
+ ("permissionID" = String, Path, description = "Permission ID")
+ ),
+ request_body = PermissionReplyRequest,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_permission_reply(
+ State(state): State>,
+ Path((session_id, permission_id)): Path<(String, String)>,
+ Json(body): Json,
+) -> impl IntoResponse {
+ let reply = match parse_permission_reply_value(body.response.as_deref()) {
+ Ok(reply) => reply,
+ Err(message) => return bad_request(&message).into_response(),
+ };
+ match state
+ .inner
+ .session_manager()
+ .reply_permission(&session_id, &permission_id, reply)
+ .await
+ {
+ Ok(_) => bool_ok(true).into_response(),
+ Err(err) => sandbox_error_response(err).into_response(),
+ }
+}
+
+#[utoipa::path(
+ post,
+ path = "/session/{sessionID}/share",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_share(
+ State(state): State>,
+ Path(session_id): Path,
+) -> impl IntoResponse {
+ let mut sessions = state.opencode.sessions.lock().await;
+ if let Some(session) = sessions.get_mut(&session_id) {
+ session.share_url = Some(format!("https://share.local/{}", session_id));
+ let value = session.to_value();
+ return (StatusCode::OK, Json(value)).into_response();
+ }
+ not_found("Session not found").into_response()
+}
+
+#[utoipa::path(
+ delete,
+ path = "/session/{sessionID}/share",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_unshare(
+ State(state): State>,
+ Path(session_id): Path,
+) -> impl IntoResponse {
+ let mut sessions = state.opencode.sessions.lock().await;
+ if let Some(session) = sessions.get_mut(&session_id) {
+ session.share_url = None;
+ let value = session.to_value();
+ return (StatusCode::OK, Json(value)).into_response();
+ }
+ not_found("Session not found").into_response()
+}
+
+#[utoipa::path(
+ get,
+ path = "/session/{sessionID}/todo",
+ params(("sessionID" = String, Path, description = "Session ID")),
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_session_todo() -> impl IntoResponse {
+ (StatusCode::OK, Json(json!([])))
+}
+
+#[utoipa::path(
+ get,
+ path = "/permission",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_permission_list(State(state): State>) -> impl IntoResponse {
+ let pending = state
+ .inner
+ .session_manager()
+ .list_pending_permissions()
+ .await;
+ let mut values = Vec::new();
+ for item in pending {
+ let record = OpenCodePermissionRecord {
+ id: item.permission_id,
+ session_id: item.session_id,
+ permission: item.action,
+ patterns: patterns_from_metadata(&item.metadata),
+ metadata: item.metadata.unwrap_or_else(|| json!({})),
+ always: Vec::new(),
+ tool: None,
+ };
+ values.push(record.to_value());
+ }
+ values.sort_by(|a, b| {
+ let a_id = a.get("id").and_then(|v| v.as_str()).unwrap_or("");
+ let b_id = b.get("id").and_then(|v| v.as_str()).unwrap_or("");
+ a_id.cmp(b_id)
+ });
+ (StatusCode::OK, Json(json!(values)))
+}
+
+#[utoipa::path(
+ post,
+ path = "/permission/{requestID}/reply",
+ params(("requestID" = String, Path, description = "Permission request ID")),
+ request_body = PermissionGlobalReplyRequest,
+ responses((status = 200), (status = 404)),
+ tag = "opencode"
+)]
+async fn oc_permission_reply(
+ State(state): State>,
+ Path(request_id): Path,
+ Json(body): Json,
+) -> impl IntoResponse {
+ let reply = match parse_permission_reply_value(body.reply.as_deref()) {
+ Ok(reply) => reply,
+ Err(message) => return bad_request(&message).into_response(),
+ };
+ let session_id = state
+ .inner
+ .session_manager()
+ .list_pending_permissions()
+ .await
+ .into_iter()
+ .find(|item| item.permission_id == request_id)
+ .map(|item| item.session_id);
+ let Some(session_id) = session_id else {
+ return not_found("Permission request not found").into_response();
+ };
+ match state
+ .inner
+ .session_manager()
+ .reply_permission(&session_id, &request_id, reply)
+ .await
+ {
+ Ok(_) => bool_ok(true).into_response(),
+ Err(err) => sandbox_error_response(err).into_response(),
+ }
+}
+
+#[utoipa::path(
+ get,
+ path = "/question",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_question_list(State(state): State>) -> impl IntoResponse {
+ let pending = state.inner.session_manager().list_pending_questions().await;
+ let mut values = Vec::new();
+ for item in pending {
+ let options: Vec = item
+ .options
+ .iter()
+ .map(|option| json!({"label": option, "description": ""}))
+ .collect();
+ let record = OpenCodeQuestionRecord {
+ id: item.question_id,
+ session_id: item.session_id,
+ questions: vec![json!({
+ "header": "Question",
+ "question": item.prompt,
+ "options": options,
+ })],
+ tool: None,
+ };
+ values.push(record.to_value());
+ }
+ values.sort_by(|a, b| {
+ let a_id = a.get("id").and_then(|v| v.as_str()).unwrap_or("");
+ let b_id = b.get("id").and_then(|v| v.as_str()).unwrap_or("");
+ a_id.cmp(b_id)
+ });
+ (StatusCode::OK, Json(json!(values)))
+}
+
+#[utoipa::path(
+ post,
+ path = "/question/{requestID}/reply",
+ params(("requestID" = String, Path, description = "Question request ID")),
+ request_body = QuestionReplyBody,
+ responses((status = 200), (status = 404)),
+ tag = "opencode"
+)]
+async fn oc_question_reply(
+ State(state): State>,
+ Path(request_id): Path,
+ Json(body): Json,
+) -> impl IntoResponse {
+ let session_id = state
+ .inner
+ .session_manager()
+ .list_pending_questions()
+ .await
+ .into_iter()
+ .find(|item| item.question_id == request_id)
+ .map(|item| item.session_id);
+ let Some(session_id) = session_id else {
+ return not_found("Question request not found").into_response();
+ };
+ let answers = body.answers.unwrap_or_default();
+ match state
+ .inner
+ .session_manager()
+ .reply_question(&session_id, &request_id, answers)
+ .await
+ {
+ Ok(_) => bool_ok(true).into_response(),
+ Err(err) => sandbox_error_response(err).into_response(),
+ }
+}
+
+#[utoipa::path(
+ post,
+ path = "/question/{requestID}/reject",
+ params(("requestID" = String, Path, description = "Question request ID")),
+ responses((status = 200), (status = 404)),
+ tag = "opencode"
+)]
+async fn oc_question_reject(
+ State(state): State>,
+ Path(request_id): Path,
+) -> impl IntoResponse {
+ let session_id = state
+ .inner
+ .session_manager()
+ .list_pending_questions()
+ .await
+ .into_iter()
+ .find(|item| item.question_id == request_id)
+ .map(|item| item.session_id);
+ let Some(session_id) = session_id else {
+ return not_found("Question request not found").into_response();
+ };
+ match state
+ .inner
+ .session_manager()
+ .reject_question(&session_id, &request_id)
+ .await
+ {
+ Ok(_) => bool_ok(true).into_response(),
+ Err(err) => sandbox_error_response(err).into_response(),
+ }
+}
+
+#[utoipa::path(
+ get,
+ path = "/provider",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_provider_list() -> impl IntoResponse {
+ let mut models = serde_json::Map::new();
+ for agent in available_agent_ids() {
+ models.insert(agent.as_str().to_string(), model_summary_entry(agent));
+ }
+ let providers = json!({
+ "all": [
+ {
+ "id": OPENCODE_PROVIDER_ID,
+ "name": OPENCODE_PROVIDER_NAME,
+ "env": [],
+ "models": Value::Object(models),
+ }
+ ],
+ "default": {
+ OPENCODE_PROVIDER_ID: OPENCODE_DEFAULT_MODEL_ID
+ },
+ "connected": [OPENCODE_PROVIDER_ID]
+ });
+ (StatusCode::OK, Json(providers))
+}
+
+#[utoipa::path(
+ get,
+ path = "/provider/auth",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_provider_auth() -> impl IntoResponse {
+ let auth = json!({
+ OPENCODE_PROVIDER_ID: []
+ });
+ (StatusCode::OK, Json(auth))
+}
+
+#[utoipa::path(
+ post,
+ path = "/provider/{providerID}/oauth/authorize",
+ params(("providerID" = String, Path, description = "Provider ID")),
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_provider_oauth_authorize(Path(provider_id): Path) -> impl IntoResponse {
+ (
+ StatusCode::OK,
+ Json(json!({
+ "url": format!("https://auth.local/{}/authorize", provider_id),
+ "method": "auto",
+ "instructions": "stub",
+ })),
+ )
+}
+
+#[utoipa::path(
+ post,
+ path = "/provider/{providerID}/oauth/callback",
+ params(("providerID" = String, Path, description = "Provider ID")),
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_provider_oauth_callback(Path(_provider_id): Path) -> impl IntoResponse {
+ bool_ok(true)
+}
+
+#[utoipa::path(
+ put,
+ path = "/auth/{providerID}",
+ params(("providerID" = String, Path, description = "Provider ID")),
+ request_body = String,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_auth_set(
+ Path(_provider_id): Path,
+ Json(_body): Json,
+) -> impl IntoResponse {
+ bool_ok(true)
+}
+
+#[utoipa::path(
+ delete,
+ path = "/auth/{providerID}",
+ params(("providerID" = String, Path, description = "Provider ID")),
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_auth_remove(Path(_provider_id): Path) -> impl IntoResponse {
+ bool_ok(true)
+}
+
+#[utoipa::path(
+ get,
+ path = "/pty",
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_pty_list(State(state): State>) -> impl IntoResponse {
+ let ptys = state.opencode.ptys.lock().await;
+ let values: Vec = ptys.values().map(|p| p.to_value()).collect();
+ (StatusCode::OK, Json(json!(values)))
+}
+
+#[utoipa::path(
+ post,
+ path = "/pty",
+ request_body = PtyCreateRequest,
+ responses((status = 200)),
+ tag = "opencode"
+)]
+async fn oc_pty_create(
+ State(state): State>,
+ headers: HeaderMap,
+ Query(query): Query