Compare commits

..
42 Commits
Author SHA1 Message Date
opencode 491abd6b5b release: v0.5.13 2025-08-21 15:27:25 +00:00
Dax Raad 4518f96e3d add plugin hook for config 2025-08-21 11:22:24 -04:00
GitHub Action a9dcbedf99 ignore: update download stats 2025-08-21 2025-08-21 12:04:21 +00:00
Aiden ClineandGitHub 9231043eb4 tweak: adjust plan -> build transition prompt (#2111) 2025-08-21 06:52:38 -05:00
Frank 04dcd87170 fix do migration 2025-08-21 18:02:32 +08:00
Frank c31fd9ed79 fix do migration 2025-08-21 17:52:55 +08:00
Jay V 2989d92794 docs: update 2025-08-20 17:58:56 -04:00
Jay V 256d074411 docs: gitlab 2025-08-20 17:51:16 -04:00
Jay V 8b01676ec0 docs: edit 2025-08-20 17:39:37 -04:00
Lee TickettandGitHub 34c6c8494a docs: Add GitLab CLI agent integration doc (#2103) 2025-08-20 17:37:43 -04:00
Dax Raad 522bed6b7d ignore: cloud stuff 2025-08-20 17:01:18 -04:00
Vincent BernatandGitHub dda672284c fix: ignore case when checking Qwen in model ID for todos (#2122) 2025-08-20 14:44:27 -05:00
Jay Vandopencode 6018364164 docs: edit 2025-08-20 18:22:48 +00:00
opencode bc0d438cee release: v0.5.12 2025-08-20 18:22:48 +00:00
Jay V abef91c223 docs: edit server 2025-08-20 14:13:02 -04:00
Dax Raad 1bbf6d38e5 ci: turn back on aur 2025-08-20 12:46:17 -04:00
opencode c9c9db1e8d release: v0.5.11 2025-08-20 16:36:05 +00:00
Dax Raad b11fe9fbc6 ignore: remove import 2025-08-20 12:29:24 -04:00
Dax Raad 60f3d413de remove auto browser open for now 2025-08-20 12:28:00 -04:00
opencode 1df2d78b85 release: v0.5.10 2025-08-20 16:12:00 +00:00
opencode 2286a872c1 release: v0.5.9 2025-08-20 15:51:24 +00:00
Dax Raad 8a83301e0d copilot auth update version 2025-08-20 11:46:14 -04:00
GitHub Action 9bc40f00e3 ignore: update download stats 2025-08-20 2025-08-20 12:04:23 +00:00
opencode c3c440948a release: v0.5.8 2025-08-20 05:08:31 +00:00
Dax Raad aa10f8a7f6 sonic model 2025-08-20 01:02:41 -04:00
Aiden ClineandGitHub a2db58f125 fix: don't let --continue access subagent session (#2091) 2025-08-19 22:40:07 -05:00
Aiden ClineandGitHub 574be9febf fix: keybind panic (#2092) 2025-08-19 22:39:59 -05:00
Aiden ClineandGitHub 5b05ede748 fix: agent casing issue (#2081) 2025-08-19 18:08:56 -05:00
Aiden ClineandGitHub 4032426185 docs: remove non existent keybind (#2080) 2025-08-19 17:39:02 -05:00
Jay V 8d8045ff95 docs: add sdk doc 2025-08-19 18:11:36 -04:00
Jay V b3c8bec019 docs: edit server 2025-08-19 17:21:45 -04:00
Aiden ClineandGitHub 25f43adaa0 tweak: notify agent it is in build mode when switching from plan mode (#2065) 2025-08-19 15:32:31 -05:00
Timo ClasenandGitHub 4913ee6afd fix(TUI): make it less shimmer (#2076) 2025-08-19 15:30:54 -05:00
c59ded82b3 docs: document server API endpoints (#2019)
Co-authored-by: Jay <air@live.ca>
2025-08-19 16:13:02 -04:00
Aiden ClineandGitHub 40bdbf92a3 fix: tui panic from logger (#2075) 2025-08-19 14:47:44 -05:00
Aiden ClineGitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
ad76d7e57d fix: add type checking for MCP tool path parameters (#2073)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-08-19 13:38:33 -05:00
GitHub Action 863ae6fa7d ignore: update download stats 2025-08-19 2025-08-19 12:04:15 +00:00
Aiden ClineandGitHub 8f230ad4b4 fix: interface conversion panic (#2060) 2025-08-19 05:25:46 -05:00
Aiden ClineandGitHub c0f90eb564 tweak: better agent create error handling (#2058) 2025-08-19 00:14:50 -05:00
DaxandGitHub 50fb337270 Update duplicate-issues.yml 2025-08-18 20:00:59 -04:00
Aiden ClineandGitHub e08ec077b0 fix: ensure name isn't added as field in options: {...} (#2053) 2025-08-18 18:15:20 -05:00
Aiden ClineandGitHub 796245d146 blacklist gpt-5-chat-latest (#2048) 2025-08-18 17:50:38 -04:00
57 changed files with 1025 additions and 121 deletions
+1 -3
View File
@@ -36,10 +36,8 @@ jobs:
Issue number:
${{ github.event.issue.number }}
Issue body:
${{ github.event.issue.body }}
Please search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
Consider:
1. Similar titles or descriptions
2. Same error messages or symptoms
+3
View File
@@ -24,3 +24,6 @@ example, if the page title is "Models", avoid using a section title like "Add
new models". This might be unavoidable in some cases, but try to avoid it.
Check out the /packages/web/src/content/docs/docs/index.mdx as an example.
For JS or TS code snippets remove trailing semicolons and any trailing commas
that might not be needed.
+3
View File
@@ -52,3 +52,6 @@
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
+11 -10
View File
@@ -16,6 +16,7 @@
"dependencies": {
"@ibm/plex": "6.4.1",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opencode/cloud-core": "workspace:*",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.1.0",
@@ -25,7 +26,7 @@
},
"cloud/core": {
"name": "@opencode/cloud-core",
"version": "0.5.5",
"version": "0.5.12",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"drizzle-orm": "0.41.0",
@@ -39,7 +40,7 @@
},
"cloud/function": {
"name": "@opencode/cloud-function",
"version": "0.5.5",
"version": "0.5.12",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -59,7 +60,7 @@
},
"cloud/web": {
"name": "@opencode/cloud-web",
"version": "0.5.5",
"version": "0.5.12",
"dependencies": {
"@kobalte/core": "0.13.9",
"@openauthjs/solid": "0.0.0-20250322224806",
@@ -78,7 +79,7 @@
},
"packages/function": {
"name": "@opencode/function",
"version": "0.5.5",
"version": "0.5.12",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -93,7 +94,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "0.5.5",
"version": "0.5.12",
"bin": {
"opencode": "./bin/opencode",
},
@@ -143,7 +144,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "0.5.5",
"version": "0.5.12",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
},
@@ -155,7 +156,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.5.5",
"version": "0.5.12",
"devDependencies": {
"@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:",
@@ -164,7 +165,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "0.5.5",
"version": "0.5.12",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -186,7 +187,7 @@
"sharp": "0.32.5",
"shiki": "3.4.2",
"solid-js": "1.9.7",
"toolbeam-docs-theme": "0.4.3",
"toolbeam-docs-theme": "0.4.5",
},
"devDependencies": {
"@types/node": "catalog:",
@@ -2804,7 +2805,7 @@
"toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="],
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.3", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-3um/NsSq4xFeKbKrNGPHIzfTixwnEVvroqA8Q+lecnYHHJ5TtiYTggHDqewOW+I67t0J1IVBwVKUPjxiQfIcog=="],
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.5", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-8yu1Q57pYujy1EnQuZ5JzrV6uzTWwUAcTkBkkujtFNNUBC8jTASWp5WypiViVMd59oFqzedy1PR0G9nBBhZvRQ=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
+3 -2
View File
@@ -5,7 +5,7 @@
"dev": "vinxi dev --host 0.0.0.0",
"build": "vinxi build",
"start": "vinxi start",
"version": "0.5.7"
"version": "0.5.13"
},
"dependencies": {
"@ibm/plex": "6.4.1",
@@ -14,7 +14,8 @@
"@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.1.0",
"solid-js": "^1.9.5",
"vinxi": "^0.5.7"
"vinxi": "^0.5.7",
"@opencode/cloud-core": "workspace:*"
},
"engines": {
"node": ">=22"
+4 -2
View File
@@ -1,7 +1,7 @@
import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import { ErrorBoundary, Suspense } from "solid-js";
import "@ibm/plex/css/ibm-plex.css";
import "./app.css";
@@ -11,7 +11,9 @@ export default function App() {
root={props => (
<MetaProvider>
<Title>SolidStart - Basic</Title>
<Suspense>{props.children}</Suspense>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense>{props.children}</Suspense>
</ErrorBoundary>
</MetaProvider>
)}
>
+83 -2
View File
@@ -1,9 +1,90 @@
import { useSession } from "vinxi/http"
import { createClient } from "@openauthjs/openauth/client"
import { getRequestEvent } from "solid-js/web"
import { and, Database, eq, inArray } from "@opencode/cloud-core/drizzle/index.js"
import { WorkspaceTable } from "@opencode/cloud-core/schema/workspace.sql.js"
import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
import { query, redirect } from "@solidjs/router"
import { AccountTable } from "@opencode/cloud-core/schema/account.sql.js"
import { Actor } from "@opencode/cloud-core/actor.js"
export async function withActor<T>(fn: () => T) {
const actor = await getActor()
return Actor.provide(actor.type, actor.properties, fn)
}
export const getActor = query(async (): Promise<Actor.Info> => {
"use server"
const evt = getRequestEvent()
const url = new URL(evt!.request.headers.get("referer") ?? evt!.request.url)
const auth = await useAuthSession()
const [workspaceHint] = url.pathname.split("/").filter((x) => x.length > 0)
if (!workspaceHint) {
if (auth.data.current) {
const current = auth.data.account[auth.data.current]
return {
type: "account",
properties: {
email: current.email,
accountID: current.id,
},
}
}
if (Object.keys(auth.data.account).length > 0) {
const current = Object.values(auth.data.account)[0]
await auth.update(val => ({
...val,
current: current.id,
}))
return {
type: "account",
properties: {
email: current.email,
accountID: current.id,
},
}
}
return {
type: "public",
properties: {},
}
}
const accounts = Object.keys(auth.data.account)
const result = await Database.transaction(async (tx) => {
return await tx.select({
user: UserTable
})
.from(AccountTable)
.innerJoin(UserTable, and(eq(UserTable.email, AccountTable.email)))
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
.where(
and(
inArray(AccountTable.id, accounts),
eq(WorkspaceTable.id, workspaceHint),
)
)
.limit(1)
.execute()
.then((x) => x[0])
})
if (result) {
return {
type: "user",
properties: {
userID: result.user.id,
workspaceID: result.user.workspaceID,
},
}
}
throw redirect("/auth/authorize")
}, "actor")
export const AuthClient = createClient({
clientID: "app",
issuer: "https://auth.dev.opencode.ai",
issuer: import.meta.env.VITE_AUTH_URL,
})
export interface AuthSession {
@@ -15,7 +96,6 @@ export interface AuthSession {
}
export function useAuthSession() {
"use server"
return useSession<AuthSession>({
password: "0".repeat(32),
@@ -26,3 +106,4 @@ export function useAuthSession() {
export function AuthProvider() {
}
+15
View File
@@ -0,0 +1,15 @@
import { createAsync, query } from "@solidjs/router"
import { getActor, withActor } from "~/context/auth"
const getPosts = query(async () => {
"use server"
return withActor(() => {
return "ok"
})
}, "posts")
export default function () {
const actor = createAsync(async () => getActor())
return <div>{JSON.stringify(actor())}</div>
}
-5
View File
@@ -5,11 +5,6 @@ export async function GET(input: APIEvent) {
const url = new URL(input.request.url)
const code = url.searchParams.get("code")
if (!code) throw new Error("No code found")
const redirectURI = `${url.origin}${url.pathname}`
console.log({
redirectURI,
code,
})
const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
if (result.err) {
throw new Error(result.err.message)
+18
View File
@@ -6,6 +6,9 @@ import IMG_SPLASH from "../asset/screenshot-splash.webp"
import IMG_VSCODE from "../asset/screenshot-vscode.webp"
import IMG_GITHUB from "../asset/screenshot-github.webp"
import { IconCopy, IconCheck } from "../component/icon"
import { createAsync, query, redirect, RouteDefinition } from "@solidjs/router"
import { getActor, withActor } from "~/context/auth"
import { Account } from "@opencode/cloud-core/account.js"
function CopyStatus() {
return (
@@ -16,7 +19,22 @@ function CopyStatus() {
)
}
const isLoggedIn = query(async () => {
"use server"
const actor = await getActor()
if (actor.type === "account") {
const workspaces = await withActor(() => Account.workspaces())
throw redirect("/" + workspaces[0].id)
}
return
}, "isLoggedIn")
export default function Home() {
createAsync(() => isLoggedIn(), {
deferStream: true,
})
onMount(() => {
const commands = document.querySelectorAll("[data-copy]")
for (const button of commands) {
+9
View File
@@ -0,0 +1,9 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cloud-core",
"version": "0.5.7",
"version": "0.5.13",
"private": true,
"type": "module",
"dependencies": {
-1
View File
@@ -20,7 +20,6 @@ export namespace Actor {
properties: {
userID: string
workspaceID: string
email: string
}
}
+3 -2
View File
@@ -3,7 +3,7 @@ import { Resource } from "sst"
export * from "drizzle-orm"
import postgres from "postgres"
function createClient() {
const createClient = memo(() => {
const client = postgres({
idle_timeout: 30000,
connect_timeout: 30000,
@@ -19,12 +19,13 @@ function createClient() {
})
return drizzle(client, {})
}
})
import { PgTransaction, type PgTransactionConfig } from "drizzle-orm/pg-core"
import type { ExtractTablesWithRelations } from "drizzle-orm"
import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js"
import { Context } from "../context"
import { memo } from "../util/memo"
export namespace Database {
export type Transaction = PgTransaction<
+11
View File
@@ -0,0 +1,11 @@
export function memo<T>(fn: () => T) {
let value: T | undefined
let loaded = false
return (): T => {
if (loaded) return value as T
loaded = true
value = fn()
return value as T
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/cloud-function",
"version": "0.5.7",
"version": "0.5.13",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+8 -1
View File
@@ -2,11 +2,12 @@ import { Resource } from "sst"
import { z } from "zod"
import { issuer } from "@openauthjs/openauth"
import { createSubjects } from "@openauthjs/openauth/subject"
import { CodeProvider } from "@openauthjs/openauth/provider/code"
import { GithubProvider } from "@openauthjs/openauth/provider/github"
import { GoogleOidcProvider } from "@openauthjs/openauth/provider/google"
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
import { Account } from "@opencode/cloud-core/account.js"
import { Workspace } from "@opencode/cloud-core/workspace.js"
import { Actor } from "@opencode/cloud-core/actor.js"
type Env = {
AuthStorage: KVNamespace
@@ -117,6 +118,12 @@ export default {
email: email!,
})
}
await Actor.provide("account", { accountID, email }, async () => {
const workspaces = await Account.workspaces()
if (workspaces.length === 0) {
await Workspace.create()
}
})
return ctx.subject("account", accountID, { accountID, email })
},
}).fetch(request, env, ctx)
-4
View File
@@ -14,10 +14,6 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
"Console": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
"DATABASE_PASSWORD": {
"type": "sst.sst.Secret"
"value": string
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/cloud-web",
"version": "0.5.7",
"version": "0.5.13",
"private": true,
"description": "",
"type": "module",
+9
View File
@@ -0,0 +1,9 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/// <reference path="../sst-env.d.ts" />
import "sst"
export {}
+2 -2
View File
@@ -25,8 +25,8 @@ export const api = new sst.cloudflare.Worker("Api", {
])
args.migrations = {
// Note: when releasing the next tag, make sure all stages use tag v2
oldTag: $app.stage === "production" ? "" : "v1",
newTag: $app.stage === "production" ? "" : "v1",
oldTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
newTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
//newSqliteClasses: ["SyncServer"],
}
},
+14 -1
View File
@@ -10,7 +10,7 @@ const DATABASE_USERNAME = new sst.Secret("DATABASE_USERNAME")
const DATABASE_PASSWORD = new sst.Secret("DATABASE_PASSWORD")
export const database = new sst.Linkable("Database", {
properties: {
host: "aws-us-east-2-1.pg.psdb.cloud",
host: `aws-us-east-2-${$app.stage === "thdxr" ? "2" : "1"}.pg.psdb.cloud`,
database: "postgres",
username: DATABASE_USERNAME.value,
password: DATABASE_PASSWORD.value,
@@ -106,6 +106,7 @@ export const gateway = new sst.cloudflare.Worker("GatewayApi", {
// CONSOLE
////////////////
/*
export const console = new sst.cloudflare.x.StaticSite("Console", {
domain: `console.${domain}`,
path: "cloud/web",
@@ -119,3 +120,15 @@ export const console = new sst.cloudflare.x.StaticSite("Console", {
VITE_AUTH_URL: auth.url.apply((url) => url!),
},
})
*/
new sst.x.DevCommand("Solid", {
link: [database],
dev: {
directory: "cloud/app",
command: "bun dev",
},
environment: {
VITE_AUTH_URL: auth.url.apply((url) => url!),
},
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/function",
"version": "0.5.7",
"version": "0.5.13",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
-4
View File
@@ -14,10 +14,6 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
"Console": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
"DATABASE_PASSWORD": {
"type": "sst.sst.Secret"
"value": string
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.5.7",
"version": "0.5.13",
"name": "opencode",
"type": "module",
"private": true,
-2
View File
@@ -97,7 +97,6 @@ if (!snapshot) {
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
/* AUR package - commented out as AUR is down
const pkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
@@ -136,7 +135,6 @@ if (!snapshot) {
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
if (!dry) await $`cd ./dist/aur-${pkg} && git push`
}
*/
// Homebrew formula
const homebrewFormula = [
+3 -1
View File
@@ -100,7 +100,7 @@ export namespace Agent {
tools: {},
builtIn: false,
}
const { model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
item.options = {
...item.options,
...extra,
@@ -120,6 +120,8 @@ export namespace Agent {
if (temperature != undefined) item.temperature = temperature
if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode
// just here for consistency & to prevent it from being added as an option
if (name) item.name = name
if (permission ?? cfg.permission) {
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
+1 -1
View File
@@ -8,9 +8,9 @@ import { Snapshot } from "../snapshot"
export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Promise<T>) {
return App.provide(input, async (app) => {
await Plugin.init()
Share.init()
Format.init()
Plugin.init()
ConfigHooks.init()
LSP.init()
Snapshot.init()
+4 -1
View File
@@ -46,7 +46,10 @@ const AgentCreateCommand = cmd({
const spinner = prompts.spinner()
spinner.start("Generating agent configuration...")
const generated = await Agent.generate({ description: query })
const generated = await Agent.generate({ description: query }).catch((error) => {
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
throw new UI.CancelledError()
})
spinner.stop(`Agent ${generated.identifier} generated`)
const availableTools = [
-4
View File
@@ -1,7 +1,6 @@
import { Auth } from "../../auth"
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import open from "open"
import { UI } from "../ui"
import { ModelsDev } from "../../provider/models"
import { map, pipe, sortBy, values } from "remeda"
@@ -159,9 +158,6 @@ export const AuthLoginCommand = cmd({
const authorize = await method.authorize()
if (authorize.url) {
try {
await open(authorize.url)
} catch (e) {}
prompts.log.info("Go to: " + authorize.url)
}
+11 -5
View File
@@ -67,11 +67,17 @@ export const RunCommand = cmd({
await bootstrap({ cwd: process.cwd() }, async () => {
const session = await (async () => {
if (args.continue) {
const list = Session.list()
const first = await list.next()
await list.return()
if (first.done) return
return first.value
const it = Session.list()
try {
for await (const s of it) {
if (s.parentID === undefined) {
return s
}
}
return
} finally {
await it.return()
}
}
if (args.session) return Session.get(args.session)
+11 -5
View File
@@ -82,11 +82,17 @@ export const TuiCommand = cmd({
const result = await bootstrap({ cwd }, async (app) => {
const sessionID = await (async () => {
if (args.continue) {
const list = Session.list()
const first = await list.next()
await list.return()
if (first.done) return
return first.value.id
const it = Session.list()
try {
for await (const s of it) {
if (s.parentID === undefined) {
return s.id
}
}
return
} finally {
await it.return()
}
}
if (args.session) {
return args.session
+1 -1
View File
@@ -45,7 +45,7 @@ export const UpgradeCommand = {
spinner.start("Upgrading...")
const err = await Installation.upgrade(method, target).catch((err) => err)
if (err) {
spinner.stop("Upgrade failed")
spinner.stop("Upgrade failed", 1)
if (err instanceof Installation.UpgradeFailedError) prompts.log.error(err.data.stderr)
else if (err instanceof Error) prompts.log.error(err.message)
prompts.outro("Done")
+1 -1
View File
@@ -64,7 +64,7 @@ export namespace Config {
if (agentFolderPath.includes("/")) {
const relativePath = agentFolderPath.replace(".md", "")
const pathParts = relativePath.split("/")
agentName = pathParts.slice(0, -1).join("/").toUpperCase() + "/" + pathParts[pathParts.length - 1].toUpperCase()
agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1]
}
const config = {
+1 -1
View File
@@ -28,7 +28,7 @@ await Promise.all([
fs.mkdir(Global.Path.bin, { recursive: true }),
])
const CACHE_VERSION = "8"
const CACHE_VERSION = "9"
const version = await Bun.file(path.join(Global.Path.cache, "version"))
.text()
+7 -2
View File
@@ -25,7 +25,7 @@ export namespace Plugin {
}
const plugins = [...(config.plugin ?? [])]
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
plugins.push("opencode-copilot-auth")
plugins.push("opencode-copilot-auth@0.0.2")
plugins.push("opencode-anthropic-auth@0.0.2")
}
for (let plugin of plugins) {
@@ -68,7 +68,12 @@ export namespace Plugin {
return state().then((x) => x.hooks)
}
export function init() {
export async function init() {
const hooks = await state().then((x) => x.hooks)
const config = await Config.get()
for (const hook of hooks) {
hook.config?.(config)
}
Bus.subscribeAll(async (input) => {
const hooks = await state().then((x) => x.hooks)
for (const hook of hooks) {
@@ -36,6 +36,12 @@ export namespace Provider {
},
}
},
async opencode() {
return {
autoload: true,
options: {},
}
},
openai: async () => {
return {
autoload: false,
@@ -276,6 +282,15 @@ export namespace Provider {
}
for (const [providerID, provider] of Object.entries(providers)) {
// Filter out blacklisted models
const filteredModels = Object.fromEntries(
Object.entries(provider.info.models).filter(
([modelID]) =>
modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
),
)
provider.info.models = filteredModels
if (Object.keys(provider.info.models).length === 0) {
delete providers[providerID]
continue
+20
View File
@@ -17,6 +17,7 @@ import {
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
import PROMPT_PLAN from "../session/prompt/plan.txt"
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
import { App } from "../app/app"
import { Bus } from "../bus"
@@ -726,6 +727,18 @@ export namespace Session {
synthetic: true,
})
}
const lastAssistantMsg = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
if (lastAssistantMsg?.mode === "plan" && agent.name === "build") {
msgs.at(-1)?.parts.push({
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
text: BUILD_SWITCH,
synthetic: true,
})
}
let system = SystemPrompt.header(input.providerID)
system.push(
...(() => {
@@ -945,6 +958,13 @@ export namespace Session {
toolName: "invalid",
}
},
headers:
input.providerID === "opencode"
? {
"x-opencode-session": input.sessionID,
"x-opencode-request": userMsg.id,
}
: undefined,
maxRetries: 3,
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
maxOutputTokens: outputLimit,
@@ -0,0 +1 @@
Your operational mode has changed from plan to build. You are no longer in read-only mode. You are permitted to make file changes as necessary and utilize your arsenal of tools as needed.
+1 -1
View File
@@ -86,7 +86,7 @@ export namespace ToolRegistry {
result["webfetch"] = false
}
if (modelID.includes("qwen")) {
if (modelID.toLowerCase().includes("qwen")) {
result["todowrite"] = false
result["todoread"] = false
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.5.7",
"version": "0.5.13",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
+2
View File
@@ -8,6 +8,7 @@ import type {
UserMessage,
Part,
Auth,
Config,
} from "@opencode-ai/sdk"
import type { BunShell } from "./shell"
@@ -20,6 +21,7 @@ export type Plugin = (input: PluginInput) => Promise<Hooks>
export interface Hooks {
event?: (input: { event: Event }) => Promise<void>
config?: (input: Config) => Promise<void>
auth?: {
provider: string
loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.5.7",
"version": "0.5.13",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
+3
View File
@@ -207,6 +207,9 @@ func New(
func (a *App) Keybind(commandName commands.CommandName) string {
command := a.Commands[commandName]
if len(command.Keybindings) == 0 {
return ""
}
kb := command.Keybindings[0]
key := kb.Key
if kb.RequiresLeader {
@@ -382,11 +382,9 @@ func (m *editorComponent) Content() string {
status = "waiting for permission"
}
if m.interruptKeyInDebounce && m.app.CurrentPermission.ID == "" {
bright := t.Accent()
if status == "waiting for permission" {
bright = t.Warning()
}
hint = util.Shimmer(status, t.Background(), t.TextMuted(), bright) + m.spinner.View() + muted(
hint = muted(
status,
) + m.spinner.View() + muted(
" ",
) + base(
keyText+" again",
@@ -394,11 +392,7 @@ func (m *editorComponent) Content() string {
" interrupt",
)
} else {
bright := t.Accent()
if status == "waiting for permission" {
bright = t.Warning()
}
hint = util.Shimmer(status, t.Background(), t.TextMuted(), bright) + m.spinner.View()
hint = muted(status) + m.spinner.View()
if m.app.CurrentPermission.ID == "" {
hint += muted(" ") + base(keyText) + muted(" interrupt")
}
@@ -213,6 +213,7 @@ func renderText(
extra string,
isThinking bool,
isQueued bool,
shimmer bool,
fileParts []opencode.FilePart,
agentParts []opencode.AgentPart,
toolCalls ...opencode.ToolPart,
@@ -234,7 +235,12 @@ func renderText(
}
content = util.ToMarkdown(text, width, backgroundColor)
if isThinking {
label := util.Shimmer("Thinking...", backgroundColor, t.TextMuted(), t.Accent())
var label string
if shimmer {
label = util.Shimmer("Thinking...", backgroundColor, t.TextMuted(), t.Accent())
} else {
label = styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Render("Thinking...")
}
label = styles.NewStyle().Background(backgroundColor).Width(width - 6).Render(label)
content = label + "\n\n" + content
} else if strings.TrimSpace(text) == "Generating..." {
@@ -665,10 +671,22 @@ func renderToolDetails(
body = strings.Join(steps, "\n")
body += "\n\n"
body += baseStyle(app.Keybind(commands.SessionChildCycleCommand)) +
mutedStyle(", ") +
baseStyle(app.Keybind(commands.SessionChildCycleReverseCommand)) +
mutedStyle(" navigate child sessions")
// Build navigation hint with proper spacing
cycleKeybind := app.Keybind(commands.SessionChildCycleCommand)
cycleReverseKeybind := app.Keybind(commands.SessionChildCycleReverseCommand)
var navParts []string
if cycleKeybind != "" {
navParts = append(navParts, baseStyle(cycleKeybind))
}
if cycleReverseKeybind != "" {
navParts = append(navParts, baseStyle(cycleReverseKeybind))
}
if len(navParts) > 0 {
body += strings.Join(navParts, mutedStyle(", ")) + mutedStyle(" navigate child sessions")
}
}
body = defaultStyle(body)
default:
@@ -903,7 +921,9 @@ func renderArgs(args *map[string]any, titleKey string) string {
continue
}
if key == "filePath" || key == "path" {
value = util.Relative(value.(string))
if strValue, ok := value.(string); ok {
value = util.Relative(strValue)
}
}
if key == titleKey {
title = fmt.Sprintf("%s", value)
@@ -336,6 +336,25 @@ func (m *messagesComponent) renderView() tea.Cmd {
width := m.width // always use full width
// Find the last streaming ReasoningPart to only shimmer that one
lastStreamingReasoningID := ""
if m.showThinkingBlocks {
for mi := len(m.app.Messages) - 1; mi >= 0 && lastStreamingReasoningID == ""; mi-- {
if _, ok := m.app.Messages[mi].Info.(opencode.AssistantMessage); !ok {
continue
}
parts := m.app.Messages[mi].Parts
for pi := len(parts) - 1; pi >= 0; pi-- {
if rp, ok := parts[pi].(opencode.ReasoningPart); ok {
if strings.TrimSpace(rp.Text) != "" && rp.Time.End == 0 {
lastStreamingReasoningID = rp.ID
break
}
}
}
}
}
reverted := false
revertedMessageCount := 0
revertedToolCount := 0
@@ -437,6 +456,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
files,
false,
isQueued,
false,
fileParts,
agentParts,
)
@@ -513,6 +533,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
"",
false,
false,
false,
[]opencode.FilePart{},
[]opencode.AgentPart{},
toolCallParts...,
@@ -530,6 +551,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
"",
false,
false,
false,
[]opencode.FilePart{},
[]opencode.AgentPart{},
toolCallParts...,
@@ -600,6 +622,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
}
if part.Text != "" {
text := part.Text
shimmer := part.Time.End == 0 && part.ID == lastStreamingReasoningID
content = renderText(
m.app,
message.Info,
@@ -610,6 +633,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
"",
true,
false,
shimmer,
[]opencode.FilePart{},
[]opencode.AgentPart{},
)
@@ -644,6 +668,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
"",
false,
false,
false,
[]opencode.FilePart{},
[]opencode.AgentPart{},
)
@@ -746,16 +771,18 @@ func (m *messagesComponent) renderView() tea.Cmd {
} else {
for _, part := range response.Parts {
if part.CallID == m.app.CurrentPermission.CallID {
content := renderToolDetails(
m.app,
part.AsUnion().(opencode.ToolPart),
m.app.CurrentPermission,
width,
)
if content != "" {
partCount++
lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
if toolPart, ok := part.AsUnion().(opencode.ToolPart); ok {
content := renderToolDetails(
m.app,
toolPart,
m.app.CurrentPermission,
width,
)
if content != "" {
partCount++
lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
}
}
}
}
+21 -10
View File
@@ -2,12 +2,31 @@ package util
import (
"context"
"fmt"
"log/slog"
"reflect"
"sync"
opencode "github.com/sst/opencode-sdk-go"
)
func sanitizeValue(val any) any {
if val == nil {
return nil
}
if err, ok := val.(error); ok {
return err.Error()
}
v := reflect.ValueOf(val)
if v.Kind() == reflect.Interface && !v.IsNil() {
return fmt.Sprintf("%T", val)
}
return val
}
type APILogHandler struct {
client *opencode.Client
service string
@@ -67,21 +86,13 @@ func (h *APILogHandler) Handle(ctx context.Context, r slog.Record) error {
h.mu.Lock()
for _, attr := range h.attrs {
val := attr.Value.Any()
if err, ok := val.(error); ok {
extra[attr.Key] = err.Error()
} else {
extra[attr.Key] = val
}
extra[attr.Key] = sanitizeValue(val)
}
h.mu.Unlock()
r.Attrs(func(attr slog.Attr) bool {
val := attr.Value.Any()
if err, ok := val.(error); ok {
extra[attr.Key] = err.Error()
} else {
extra[attr.Key] = val
}
extra[attr.Key] = sanitizeValue(val)
return true
})
+14 -5
View File
@@ -9,8 +9,6 @@ import { rehypeHeadingIds } from "@astrojs/markdown-remark"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
import { spawnSync } from "child_process"
const github = "https://github.com/sst/opencode"
// https://astro.build/config
export default defineConfig({
site: config.url,
@@ -49,7 +47,7 @@ export default defineConfig({
},
],
editLink: {
baseUrl: `${github}/edit/dev/packages/web/`,
baseUrl: `${config.github}/edit/dev/packages/web/`,
},
markdown: {
headingLinks: false,
@@ -69,7 +67,14 @@ export default defineConfig({
{
label: "Usage",
items: ["docs/tui", "docs/cli", "docs/ide", "docs/share", "docs/github"],
items: [
"docs/tui",
"docs/cli",
"docs/ide",
"docs/share",
"docs/github",
"docs/gitlab"
],
},
{
@@ -79,7 +84,6 @@ export default defineConfig({
"docs/agents",
"docs/models",
"docs/themes",
"docs/plugins",
"docs/keybinds",
"docs/formatters",
"docs/permissions",
@@ -87,6 +91,11 @@ export default defineConfig({
"docs/mcp-servers",
],
},
{
label: "Develop",
items: ["docs/sdk", "docs/server", "docs/plugins"],
},
],
components: {
Hero: "./src/components/Hero.astro",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode/web",
"type": "module",
"version": "0.5.7",
"version": "0.5.13",
"scripts": {
"dev": "astro dev",
"dev:remote": "sst shell --stage=dev --target=Web astro dev",
@@ -31,7 +31,7 @@
"sharp": "0.32.5",
"shiki": "3.4.2",
"solid-js": "1.9.7",
"toolbeam-docs-theme": "0.4.3"
"toolbeam-docs-theme": "0.4.5"
},
"devDependencies": {
"opencode": "workspace:*",
+1 -1
View File
@@ -176,7 +176,7 @@ opencode run Explain the use of context in Go
### serve
Start a headless opencode server for API access.
Start a headless opencode server for API access. Check out the [server docs](/docs/server) for the full HTTP interface.
```bash
opencode serve
@@ -0,0 +1,151 @@
---
title: GitLab
description: Use opencode in GitLab issues and merge requests
---
opencode integrates with your GitLab workflow.
Mention `@opencode` in a comment, and opencode will execute tasks within your GitLab CI pipeline.
---
## Features
- **Triage issues**: Ask opencode to look into an issue and explain it to you.
- **Fix and implement**: Ask opencode to fix an issue or implement a feature.
It will work create a new branch and raised a merge request with the changes.
- **Secure**: opencode runs on your GitLab runners.
---
## Setup
opencode runs in your GitLab CI/CD pipeline, here's what you'll need to set it up:
:::tip
Check out the [**GitLab docs**](https://docs.gitlab.com/user/duo_agent_platform/agent_assistant/) for up to date instructions.
:::
1. Configure your GitLab environment
2. Set up CI/CD
3. Get an AI model provider API key
4. Create a service account
5. Configure CI/CD variables
6. Create a flow config file, here's an example:
<details>
<summary>Flow configuration</summary>
```yaml
image: node:22-slim
commands:
- echo "Installing opencode"
- npm install --global opencode-ai
- echo "Installing glab"
- export GITLAB_TOKEN=$GITLAB_TOKEN_OPENCODE
- apt-get update --quiet && apt-get install --yes curl wget gpg git && rm --recursive --force /var/lib/apt/lists/*
- curl --silent --show-error --location "https://raw.githubusercontent.com/upciti/wakemeops/main/assets/install_repository" | bash
- apt-get install --yes glab
- echo "Configuring glab"
- echo $GITLAB_HOST
- echo "Creating opencode auth configuration"
- mkdir --parents ~/.local/share/opencode
- |
cat > ~/.local/share/opencode/auth.json << EOF
{
"anthropic": {
"type": "api",
"key": "$ANTHROPIC_API_KEY"
}
}
EOF
- echo "Configuring git"
- git config --global user.email "opencode@gitlab.com"
- git config --global user.name "Opencode"
- echo "Testing glab"
- glab issue list
- echo "Running Opencode"
- |
opencode run "
You are an AI assistant helping with GitLab operations.
Context: $AI_FLOW_CONTEXT
Task: $AI_FLOW_INPUT
Event: $AI_FLOW_EVENT
Please execute the requested task using the available GitLab tools.
Be thorough in your analysis and provide clear explanations.
<important>
Please use the glab CLI to access data from GitLab. The glab CLI has already been authenticated. You can run the corresponding commands.
If you are asked to summarise an MR or issue or asked to provide more information then please post back a note to the MR/Issue so that the user can see it.
You don't need to commit or push up changes, those will be done automatically based on the file changes you make.
</important>
"
- git checkout --branch $CI_WORKLOAD_REF origin/$CI_WORKLOAD_REF
- echo "Checking for git changes and pushing if any exist"
- |
if ! git diff --quiet || ! git diff --cached --quiet || [ --not --zero "$(git ls-files --others --exclude-standard)" ]; then
echo "Git changes detected, adding and pushing..."
git add .
if git diff --cached --quiet; then
echo "No staged changes to commit"
else
echo "Committing changes to branch: $CI_WORKLOAD_REF"
git commit --message "Codex changes"
echo "Pushing changes up to $CI_WORKLOAD_REF"
git push https://gitlab-ci-token:$GITLAB_TOKEN@$GITLAB_HOST/gl-demo-ultimate-dev-ai-epic-17570/test-java-project.git $CI_WORKLOAD_REF
echo "Changes successfully pushed"
fi
else
echo "No git changes detected, skipping push"
fi
variables:
- ANTHROPIC_API_KEY
- GITLAB_TOKEN_OPENCODE
- GITLAB_HOST
```
</details>
You can refer to the [GitLab CLI agents docs](https://docs.gitlab.com/user/duo_agent_platform/agent_assistant/) for detailed instructions.
---
## Examples
Here are some examples of how you can use opencode in GitLab.
:::tip
You can configure to use a different trigger phrase than `@opencode`.
:::
- **Explain an issue**
Add this comment in a GitLab issue.
```
@opencode explain this issue
```
opencode will read the issue and reply with a clear explanation.
- **Fix an issue**
In a GitLab issue, say:
```
@opencode fix this
```
opencode will create a new branch, implement the changes, and open a merge request with the changes.
- **Review merge requests**
Leave the following comment on a GitLab merge request.
```
@opencode review this merge request
```
opencode will review the merge request and provide feedback.
@@ -20,7 +20,6 @@ opencode has a list of keybinds that you can customize through the opencode conf
"session_export": "<leader>x",
"session_new": "<leader>n",
"session_list": "<leader>l",
"session_navigation": "<leader>g",
"session_share": "<leader>s",
"session_unshare": "none",
"session_interrupt": "esc",
+302
View File
@@ -0,0 +1,302 @@
---
title: SDK
description: JS SDK for the opencode server.
---
import config from "../../../../config.mjs"
export const typesUrl = `${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts`
The opencode [JS/TS SDK](https://www.npmjs.com/package/@opencode-ai/sdk) provides a type-safe client for interacting with the opencode server. You can use it to build custom integrations and control opencode programmatically.
[Learn more](/docs/server) about how the opencode server works.
---
## Install
Install the SDK from npm:
```bash
npm install @opencode-ai/sdk
```
---
## Create client
Create a client instance to connect to your opencode server:
```javascript
import { createOpencodeClient } from "@opencode-ai/sdk"
const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
})
```
#### Options
| Option | Type | Description | Default |
| --------- | ---------- | --------------------------- | ----------------------- |
| `baseUrl` | `string` | URL of the opencode server | `http://localhost:4096` |
| `fetch` | `function` | Custom fetch implementation | `globalThis.fetch` |
---
## Start server
You can also programmatically start an opencode server:
```javascript
import { createOpencodeServer } from "@opencode-ai/sdk"
const server = await createOpencodeServer({
host: "127.0.0.1",
port: 4096,
})
console.log(`Server running at ${server.url}`)
server.close()
```
---
## Types
The SDK includes TypeScript definitions for all API types. Import them directly:
```typescript
import type { Session, Message, Part } from "@opencode-ai/sdk"
```
All types are generated from the server's OpenAPI specification and available in the <a href={typesUrl}>types file</a>.
---
## Errors
The SDK throws typed errors that you can catch and handle:
```typescript
try {
const session = await client.session.get({ id: "invalid-id" })
} catch (error) {
console.error("Failed to get session:", error.message)
}
```
---
## APIs
The SDK exposes all server APIs through a type-safe client interface.
---
### App
| Method | Description | Response |
| ------------ | ------------------ | --------------------------------------- |
| `app.get()` | Get app info | <a href={typesUrl}><code>App</code></a> |
| `app.init()` | Initialize the app | `boolean` |
---
#### Examples
```javascript
const app = await client.app.get()
await client.app.init()
```
---
### Config
| Method | Description | Response |
| -------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `config.get()` | Get config info | <a href={typesUrl}><code>Config</code></a> |
| `config.providers()` | List providers and default models | `{ providers: `<a href={typesUrl}><code>Provider[]</code></a>`, default: { [key: string]: string } }` |
---
#### Examples
```javascript
const config = await client.config.get()
const { providers, default: defaults } = await client.config.providers()
```
---
### Sessions
| Method | Description | Notes |
| ------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `session.list()` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.get({ id })` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.children({ id })` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.create({ parentID?, title? })` | Create session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.delete({ id })` | Delete session | Returns `boolean` |
| `session.update({ id, title? })` | Update session properties | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.init({ id, messageID, providerID, modelID })` | Analyze app and create `AGENTS.md` | Returns `boolean` |
| `session.abort({ id })` | Abort a running session | Returns `boolean` |
| `session.share({ id })` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unshare({ id })` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.summarize({ id, providerID, modelID })` | Summarize session | Returns `boolean` |
| `session.messages({ id })` | List messages in a session | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ id, messageID })` | Get message details | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.chat({ id, ...chatInput })` | Send chat message | Returns <a href={typesUrl}><code>Message</code></a> |
| `session.shell({ id, agent, command })` | Run a shell command | Returns <a href={typesUrl}><code>Message</code></a> |
| `session.revert({ id, messageID, partID? })` | Revert a message | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ id })` | Restore reverted messages | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.permissions.respond({ id, permissionID, response })` | Respond to a permission request | Returns `boolean` |
---
#### Examples
```javascript
// Create and manage sessions
const session = await client.session.create({ title: "My session" })
const sessions = await client.session.list()
// Send messages
const message = await client.session.chat({
id: session.id,
providerID: "anthropic",
modelID: "claude-3-5-sonnet-20241022",
parts: [{ type: "text", text: "Hello!" }]
})
```
---
### Files
| Method | Description | Response |
| ------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------- |
| `find.text({ pattern })` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `find.files({ query })` | Find files by name | `string[]` (file paths) |
| `find.symbols({ query })` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `file.read({ path })` | Read a file | `{ type: "raw" \| "patch", content: string }` |
| `file.status()` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
---
#### Examples
```javascript
// Search and read files
const textResults = await client.find.text({ pattern: "function.*opencode" })
const files = await client.find.files({ query: "*.ts" })
const content = await client.file.read({ path: "src/index.ts" })
```
---
### Logging
| Method | Description | Response |
| ------------------------------------------------ | --------------- | --------- |
| `log.write({ service, level, message, extra? })` | Write log entry | `boolean` |
---
#### Examples
```javascript
await client.log.write({
service: "my-app",
level: "info",
message: "Operation completed"
})
```
---
### Agents
| Method | Description | Response |
| -------------- | ------------------------- | ------------------------------------------- |
| `agent.list()` | List all available agents | <a href={typesUrl}><code>Agent[]</code></a> |
---
#### Examples
```javascript
const agents = await client.agent.list()
```
---
### TUI
| Method | Description | Response |
| --------------------------------------------- | --------------------------------- | ---------------------- |
| `tui.appendPrompt({ text })` | Append text to the prompt | `boolean` |
| `tui.openHelp()` | Open the help dialog | `boolean` |
| `tui.openSessions()` | Open the session selector | `boolean` |
| `tui.openThemes()` | Open the theme selector | `boolean` |
| `tui.openModels()` | Open the model selector | `boolean` |
| `tui.submitPrompt()` | Submit the current prompt | `boolean` |
| `tui.clearPrompt()` | Clear the prompt | `boolean` |
| `tui.executeCommand({ command })` | Execute a command | `boolean` |
| `tui.showToast({ title?, message, variant })` | Show toast notification | `boolean` |
| `tui.control.next()` | Wait for the next control request | Control request object |
| `tui.control.response({ body })` | Respond to a control request | `boolean` |
---
#### Examples
```javascript
// Control TUI interface
await client.tui.appendPrompt({ text: "Add this to prompt" })
await client.tui.showToast({
message: "Task completed",
variant: "success"
})
```
---
### Auth
| Method | Description | Response |
| ------------------------------- | ------------------------------ | --------- |
| `auth.set({ id, ...authData })` | Set authentication credentials | `boolean` |
---
#### Examples
```javascript
await client.auth.set({
id: "anthropic",
type: "api",
key: "your-api-key"
})
```
---
### Events
| Method | Description | Response |
| ------------------- | ------------------------- | ------------------------- |
| `event.subscribe()` | Server-sent events stream | Server-sent events stream |
---
#### Examples
```javascript
// Listen to real-time events
const eventStream = await client.event.subscribe()
for await (const event of eventStream) {
console.log("Event:", event.type, event.properties)
}
```
@@ -0,0 +1,180 @@
---
title: Server
description: Interact with opencode server over HTTP.
---
import config from "../../../../config.mjs"
export const typesUrl = `${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts`
The `opencode serve` command runs a headless HTTP server that exposes an OpenAPI endpoint that an opencode client can use.
---
### Usage
```bash
opencode serve [--port <number>] [--hostname <string>]
```
#### Options
| Flag | Short | Description | Default |
| ------------ | ----- | --------------------- | ----------- |
| `--port` | `-p` | Port to listen on | `4096` |
| `--hostname` | `-h` | Hostname to listen on | `127.0.0.1` |
---
### How it works
When you run `opencode` it starts a TUI and a server. Where the TUI is the
client that talks to the server. The server exposes an OpenAPI 3.1 spec
endpoint. This endpoint is also used to generate an [SDK](/docs/sdk).
:::tip
Use the opencode server to interact with opencode programmatically.
:::
This architecture lets opencode support multiple clients and allows you to interact with opencode programmatically.
You can run `opencode serve` to start a standalone server. If you have the
opencode TUI running, `opencode serve` will start a new server.
---
#### Connect to an existing server
When you start the TUI it randomly assigns a port and hostname. You can instead pass in the `--hostname` and `--port` [flags](/docs/cli). Then use this to connect to its server.
The [`/tui`](#tui) endpoint can be used to drive the TUI through the server. For example, you can prefill or run a prompt. This setup is used by the opencode [IDE](/docs/ide) plugins.
---
## Spec
The server publishes an OpenAPI 3.1 spec that can be viewed at:
```
http://<hostname>:<port>/doc
```
For example, `http://localhost:4096/doc`. Use the spec to generate clients or inspect request and response types. Or view it in a Swagger explorer.
---
## APIs
The opencode server exposes the following APIs.
---
### App
| Method | Path | Description | Response |
| ------ | ----------- | ------------------ | --------------------------------------- |
| `GET` | `/app` | Get app info | <a href={typesUrl}><code>App</code></a> |
| `POST` | `/app/init` | Initialize the app | `boolean` |
---
### Config
| Method | Path | Description | Response |
| ------ | ------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `GET` | `/config` | Get config info | <a href={typesUrl}><code>Config</code></a> |
| `GET` | `/config/providers` | List providers and default models | `{ providers: `<a href={typesUrl}>Provider[]</a>`, default: { [key: string]: string } }` |
---
### Sessions
| Method | Path | Description | Notes |
| -------- | ---------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/session` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `GET` | `/session/:id` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/children` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `POST` | `/session` | Create session | body: `{ parentID?, title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id` | Delete session | |
| `PATCH` | `/session/:id` | Update session properties | body: `{ title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `POST` | `/session/:id/init` | Analyze app and create `AGENTS.md` | body: `{ messageID, providerID, modelID }` |
| `POST` | `/session/:id/abort` | Abort a running session | |
| `POST` | `/session/:id/share` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id/share` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
| `POST` | `/session/:id/summarize` | Summarize session | |
| `GET` | `/session/:id/message` | List messages in a session | Returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}[]` |
| `GET` | `/session/:id/message/:messageID` | Get message details | Returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `POST` | `/session/:id/message` | Send chat message | body matches [`ChatInput`](https://github.com/sst/opencode/blob/main/packages/opencode/src/session/index.ts#L358), returns <a href={typesUrl}><code>Message</code></a> |
| `POST` | `/session/:id/shell` | Run a shell command | body matches [`CommandInput`](https://github.com/sst/opencode/blob/main/packages/opencode/src/session/index.ts#L1007), returns <a href={typesUrl}><code>Message</code></a> |
| `POST` | `/session/:id/revert` | Revert a message | body: `{ messageID }` |
| `POST` | `/session/:id/unrevert` | Restore reverted messages | |
| `POST` | `/session/:id/permissions/:permissionID` | Respond to a permission request | body: `{ response }` |
---
### Files
| Method | Path | Description | Response |
| ------ | ------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------- |
| `GET` | `/find?pattern=<pat>` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `GET` | `/find/file?query=<q>` | Find files by name | `string[]` (file paths) |
| `GET` | `/find/symbol?query=<q>` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `GET` | `/file?path=<path>` | Read a file | `{ type: "raw" \| "patch", content: string }` |
| `GET` | `/file/status` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
---
### Logging
| Method | Path | Description | Response |
| ------ | ------ | ------------------------------------------------------------ | --------- |
| `POST` | `/log` | Write log entry. Body: `{ service, level, message, extra? }` | `boolean` |
---
### Agents
| Method | Path | Description | Response |
| ------ | -------- | ------------------------- | ------------------------------------------- |
| `GET` | `/agent` | List all available agents | <a href={typesUrl}><code>Agent[]</code></a> |
---
### TUI
| Method | Path | Description | Response |
| ------ | ----------------------- | ------------------------------------------- | ---------------------- |
| `POST` | `/tui/append-prompt` | Append text to the prompt | `boolean` |
| `POST` | `/tui/open-help` | Open the help dialog | `boolean` |
| `POST` | `/tui/open-sessions` | Open the session selector | `boolean` |
| `POST` | `/tui/open-themes` | Open the theme selector | `boolean` |
| `POST` | `/tui/open-models` | Open the model selector | `boolean` |
| `POST` | `/tui/submit-prompt` | Submit the current prompt | `boolean` |
| `POST` | `/tui/clear-prompt` | Clear the prompt | `boolean` |
| `POST` | `/tui/execute-command` | Execute a command (`{ command }`) | `boolean` |
| `POST` | `/tui/show-toast` | Show toast (`{ title?, message, variant }`) | `boolean` |
| `GET` | `/tui/control/next` | Wait for the next control request | Control request object |
| `POST` | `/tui/control/response` | Respond to a control request (`{ body }`) | `boolean` |
---
### Auth
| Method | Path | Description | Response |
| ------ | ----------- | --------------------------------------------------------------- | --------- |
| `PUT` | `/auth/:id` | Set authentication credentials. Body must match provider schema | `boolean` |
---
### Events
| Method | Path | Description | Response |
| ------ | -------- | ----------------------------------------------------------------------------- | ------------------------- |
| `GET` | `/event` | Server-sent events stream. First event is `server.connected`, then bus events | Server-sent events stream |
---
### Docs
| Method | Path | Description | Response |
| ------ | ------ | -------------------------------------- | ------------------------------------------ |
| `GET` | `/doc` | OpenAPI 3.1 specification | HTML page with OpenAPI spec |
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "0.5.7",
"version": "0.5.13",
"publisher": "sst-dev",
"repository": {
"type": "git",
-4
View File
@@ -27,10 +27,6 @@ declare module "sst" {
"Bucket": {
"type": "sst.cloudflare.Bucket"
}
"Console": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
"DATABASE_PASSWORD": {
"type": "sst.sst.Secret"
"value": string