2025-12-10 03:32:04 +08:00
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { GlobalBus } from "@/bus/global"
2025-06-01 02:41:00 +08:00
import { Log } from "../util/log"
2025-11-08 09:59:02 +08:00
import { describeRoute , generateSpecs , validator , resolver , openAPIRouteHandler } from "hono-openapi"
2025-06-01 02:41:00 +08:00
import { Hono } from "hono"
2025-09-15 15:28:08 +08:00
import { cors } from "hono/cors"
2025-10-24 21:46:28 +08:00
import { stream , streamSSE } from "hono/streaming"
2025-12-22 20:05:10 +08:00
import { proxy } from "hono/proxy"
2026-01-13 04:23:12 +08:00
import { basicAuth } from "hono/basic-auth"
2025-06-05 05:38:15 +08:00
import { Session } from "../session"
2025-10-27 03:50:41 +08:00
import z from "zod"
2025-06-01 02:41:00 +08:00
import { Provider } from "../provider/provider"
2025-12-11 04:59:12 +08:00
import { filter , mapValues , sortBy , pipe } from "remeda"
2025-11-25 01:56:00 +08:00
import { NamedError } from "@opencode-ai/util/error"
2025-06-13 11:10:03 +08:00
import { ModelsDev } from "../provider/models"
2025-07-01 02:46:18 +08:00
import { Ripgrep } from "../file/ripgrep"
2025-06-19 10:20:03 +08:00
import { Config } from "../config/config"
2025-07-03 05:08:06 +08:00
import { File } from "../file"
import { LSP } from "../lsp"
2025-11-01 23:14:39 +08:00
import { Format } from "../format"
2025-07-08 03:53:43 +08:00
import { MessageV2 } from "../session/message-v2"
2025-11-01 03:07:36 +08:00
import { TuiRoute } from "./tui"
2025-09-02 05:15:49 +08:00
import { Instance } from "../project/instance"
2026-01-03 10:17:02 +08:00
import { Project } from "../project/project"
2025-11-26 11:39:20 +08:00
import { Vcs } from "../project/vcs"
2025-08-08 04:32:12 +08:00
import { Agent } from "../agent/agent"
2025-08-15 04:24:46 +08:00
import { Auth } from "../auth"
2026-01-13 04:23:12 +08:00
import { Flag } from "../flag/flag"
2025-08-23 05:04:28 +08:00
import { Command } from "../command"
2025-11-21 13:21:06 +08:00
import { ProviderAuth } from "../provider/auth"
2025-09-02 05:15:49 +08:00
import { Global } from "../global"
import { ProjectRoute } from "./project"
2025-09-09 04:25:04 +08:00
import { ToolRegistry } from "../tool/registry"
import { zodToJsonSchema } from "zod-to-json-schema"
2025-09-13 17:46:14 +08:00
import { SessionPrompt } from "../session/prompt"
import { SessionCompaction } from "../session/compaction"
import { SessionRevert } from "../session/revert"
2025-09-17 13:16:49 +08:00
import { lazy } from "../util/lazy"
2025-10-07 06:51:57 +08:00
import { Todo } from "../session/todo"
2025-09-19 17:11:29 +08:00
import { InstanceBootstrap } from "../project/bootstrap"
2025-10-07 16:04:19 +08:00
import { MCP } from "../mcp"
2025-10-15 23:53:09 +08:00
import { Storage } from "../storage/storage"
import type { ContentfulStatusCode } from "hono/utils/http-status"
2025-11-01 03:07:36 +08:00
import { TuiEvent } from "@/cli/cmd/tui/event"
2025-10-21 05:58:48 +08:00
import { Snapshot } from "@/snapshot"
2025-10-24 04:28:20 +08:00
import { SessionSummary } from "@/session/summary"
2025-11-17 23:57:18 +08:00
import { SessionStatus } from "@/session/status"
2025-12-05 10:32:08 +08:00
import { upgradeWebSocket , websocket } from "hono/bun"
2026-01-13 04:23:12 +08:00
import { HTTPException } from "hono/http-exception"
2025-12-05 10:32:08 +08:00
import { errors } from "./error"
import { Pty } from "@/pty"
2026-01-02 06:54:11 +08:00
import { PermissionNext } from "@/permission/next"
2026-01-08 11:29:42 +08:00
import { QuestionRoute } from "./question"
2025-12-23 09:16:50 +08:00
import { Installation } from "@/installation"
2025-12-27 04:24:44 +08:00
import { MDNS } from "./mdns"
2026-01-03 10:17:02 +08:00
import { Worktree } from "../worktree"
2025-06-10 02:01:11 +08:00
2025-11-20 22:42:24 +08:00
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis . AI_SDK_LOG_WARNINGS = false
2025-05-19 02:13:04 +08:00
export namespace Server {
2025-06-01 02:41:00 +08:00
const log = Log . create ( { service : "server" } )
2025-05-19 02:13:04 +08:00
2025-12-30 11:05:08 +08:00
let _url : URL | undefined
2025-12-31 17:50:29 +08:00
let _corsWhitelist : string [ ] = [ ]
2025-12-30 11:05:08 +08:00
export function url ( ) : URL {
return _url ? ? new URL ( "http://localhost:4096" )
}
2025-07-29 10:58:12 +08:00
export const Event = {
2025-12-10 03:32:04 +08:00
Connected : BusEvent.define ( "server.connected" , z . object ( { } ) ) ,
2025-12-11 11:22:00 +08:00
Disposed : BusEvent.define ( "global.disposed" , z . object ( { } ) ) ,
2025-07-29 10:58:12 +08:00
}
2025-09-02 05:15:49 +08:00
const app = new Hono ( )
2026-01-08 11:29:42 +08:00
export const App : ( ) = > Hono = lazy (
( ) = >
2026-01-10 07:47:37 +08:00
// TODO: Break server.ts into smaller route files to fix type inference
2026-01-08 11:29:42 +08:00
app
. onError ( ( err , c ) = > {
log . error ( "failed" , {
error : err ,
} )
if ( err instanceof NamedError ) {
let status : ContentfulStatusCode
if ( err instanceof Storage . NotFoundError ) status = 404
else if ( err instanceof Provider . ModelNotFoundError ) status = 400
else if ( err . name . startsWith ( "Worktree" ) ) status = 400
else status = 500
return c . json ( err . toObject ( ) , { status } )
}
2026-01-13 04:43:48 +08:00
if ( err instanceof HTTPException ) return err . getResponse ( )
2026-01-08 11:29:42 +08:00
const message = err instanceof Error && err . stack ? err.stack : err.toString ( )
return c . json ( new NamedError . Unknown ( { message } ) . toObject ( ) , {
status : 500 ,
} )
2025-07-08 03:53:43 +08:00
} )
2026-01-13 04:23:12 +08:00
. use ( ( c , next ) = > {
const password = Flag . OPENCODE_PASSWORD
if ( ! password ) return next ( )
return basicAuth ( { username : "opencode" , password } ) ( c , next )
} )
2026-01-08 11:29:42 +08:00
. use ( async ( c , next ) = > {
const skipLogging = c . req . path === "/log"
if ( ! skipLogging ) {
log . info ( "request" , {
method : c.req.method ,
path : c.req.path ,
} )
}
const timer = log . time ( "request" , {
2025-09-17 13:16:49 +08:00
method : c.req.method ,
path : c.req.path ,
} )
2026-01-08 11:29:42 +08:00
await next ( )
if ( ! skipLogging ) {
timer . stop ( )
}
2025-11-09 09:17:46 +08:00
} )
2026-01-08 11:29:42 +08:00
. use (
cors ( {
origin ( input ) {
if ( ! input ) return
2025-12-31 02:04:18 +08:00
2026-01-08 11:29:42 +08:00
if ( input . startsWith ( "http://localhost:" ) ) return input
if ( input . startsWith ( "http://127.0.0.1:" ) ) return input
if ( input === "tauri://localhost" || input === "http://tauri.localhost" ) return input
2025-12-31 02:04:18 +08:00
2026-01-08 11:29:42 +08:00
// *.opencode.ai (https only, adjust if needed)
if ( /^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/ . test ( input ) ) {
return input
}
if ( _corsWhitelist . includes ( input ) ) {
return input
}
2025-12-31 17:50:29 +08:00
2026-01-08 11:29:42 +08:00
return
} ,
} ) ,
)
. get (
"/global/health" ,
describeRoute ( {
summary : "Get health" ,
description : "Get health information about the OpenCode server." ,
operationId : "global.health" ,
responses : {
200 : {
description : "Health information" ,
content : {
"application/json" : {
schema : resolver ( z . object ( { healthy : z.literal ( true ) , version : z.string ( ) } ) ) ,
} ,
2025-12-23 09:16:50 +08:00
} ,
} ,
} ,
2026-01-08 11:29:42 +08:00
} ) ,
async ( c ) = > {
return c . json ( { healthy : true , version : Installation.VERSION } )
} ,
)
. get (
"/global/event" ,
describeRoute ( {
summary : "Get global events" ,
description : "Subscribe to global events from the OpenCode system using server-sent events." ,
operationId : "global.event" ,
responses : {
200 : {
description : "Event stream" ,
content : {
"text/event-stream" : {
schema : resolver (
z
. object ( {
directory : z.string ( ) ,
payload : BusEvent.payloads ( ) ,
} )
. meta ( {
ref : "GlobalEvent" ,
} ) ,
) ,
} ,
2025-11-15 01:32:43 +08:00
} ,
} ,
} ,
2026-01-08 11:29:42 +08:00
} ) ,
async ( c ) = > {
log . info ( "global event connected" )
return streamSSE ( c , async ( stream ) = > {
2025-12-17 03:45:03 +08:00
stream . writeSSE ( {
data : JSON.stringify ( {
payload : {
2026-01-08 11:29:42 +08:00
type : "server.connected" ,
2025-12-17 03:45:03 +08:00
properties : { } ,
} ,
} ) ,
} )
2026-01-08 11:29:42 +08:00
async function handler ( event : any ) {
await stream . writeSSE ( {
data : JSON.stringify ( event ) ,
} )
}
GlobalBus . on ( "event" , handler )
// Send heartbeat every 30s to prevent WKWebView timeout (60s default)
const heartbeat = setInterval ( ( ) = > {
stream . writeSSE ( {
data : JSON.stringify ( {
payload : {
type : "server.heartbeat" ,
properties : { } ,
} ,
} ) ,
} )
} , 30000 )
2025-12-17 03:45:03 +08:00
2026-01-08 11:29:42 +08:00
await new Promise < void > ( ( resolve ) = > {
stream . onAbort ( ( ) = > {
clearInterval ( heartbeat )
GlobalBus . off ( "event" , handler )
resolve ( )
log . info ( "global event disconnected" )
} )
2025-11-15 01:32:43 +08:00
} )
} )
2026-01-08 11:29:42 +08:00
} ,
)
. post (
"/global/dispose" ,
describeRoute ( {
summary : "Dispose instance" ,
description : "Clean up and dispose all OpenCode instances, releasing all resources." ,
operationId : "global.dispose" ,
responses : {
200 : {
description : "Global disposed" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
await Instance . disposeAll ( )
GlobalBus . emit ( "event" , {
directory : "global" ,
payload : {
type : Event . Disposed . type ,
properties : { } ,
} ,
} )
return c . json ( true )
} ,
)
. use ( async ( c , next ) = > {
let directory = c . req . query ( "directory" ) || c . req . header ( "x-opencode-directory" ) || process . cwd ( )
try {
directory = decodeURIComponent ( directory )
} catch {
// fallback to original value
}
return Instance . provide ( {
directory ,
init : InstanceBootstrap ,
async fn() {
return next ( )
2025-12-11 11:22:00 +08:00
} ,
} )
2025-09-02 05:15:49 +08:00
} )
2026-01-08 11:29:42 +08:00
. get (
"/doc" ,
openAPIRouteHandler ( app , {
documentation : {
info : {
title : "opencode" ,
version : "0.0.3" ,
description : "opencode api" ,
} ,
openapi : "3.1.1" ,
} ,
} ) ,
)
. use ( validator ( "query" , z . object ( { directory : z.string ( ) . optional ( ) } ) ) )
2025-12-05 10:32:08 +08:00
2026-01-08 11:29:42 +08:00
. route ( "/project" , ProjectRoute )
2025-12-05 10:32:08 +08:00
2026-01-08 11:29:42 +08:00
. get (
"/pty" ,
describeRoute ( {
summary : "List PTY sessions" ,
description : "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode." ,
operationId : "pty.list" ,
responses : {
200 : {
description : "List of sessions" ,
content : {
"application/json" : {
schema : resolver ( Pty . Info . array ( ) ) ,
} ,
2025-12-05 10:32:08 +08:00
} ,
} ,
} ,
2026-01-08 11:29:42 +08:00
} ) ,
async ( c ) = > {
return c . json ( Pty . list ( ) )
} ,
)
. post (
"/pty" ,
describeRoute ( {
summary : "Create PTY session" ,
description : "Create a new pseudo-terminal (PTY) session for running shell commands and processes." ,
operationId : "pty.create" ,
responses : {
200 : {
description : "Created session" ,
content : {
"application/json" : {
schema : resolver ( Pty . Info ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
2025-12-05 10:32:08 +08:00
} ,
2026-01-08 11:29:42 +08:00
} ) ,
validator ( "json" , Pty . CreateInput ) ,
async ( c ) = > {
const info = await Pty . create ( c . req . valid ( "json" ) )
return c . json ( info )
} ,
)
. get (
"/pty/:ptyID" ,
describeRoute ( {
summary : "Get PTY session" ,
description : "Retrieve detailed information about a specific pseudo-terminal (PTY) session." ,
operationId : "pty.get" ,
responses : {
200 : {
description : "Session info" ,
content : {
"application/json" : {
schema : resolver ( Pty . Info ) ,
} ,
} ,
} ,
. . . errors ( 404 ) ,
} ,
} ) ,
validator ( "param" , z . object ( { ptyID : z.string ( ) } ) ) ,
async ( c ) = > {
const info = Pty . get ( c . req . valid ( "param" ) . ptyID )
if ( ! info ) {
throw new Storage . NotFoundError ( { message : "Session not found" } )
}
return c . json ( info )
} ,
)
. put (
"/pty/:ptyID" ,
describeRoute ( {
summary : "Update PTY session" ,
description : "Update properties of an existing pseudo-terminal (PTY) session." ,
operationId : "pty.update" ,
responses : {
200 : {
description : "Updated session" ,
content : {
"application/json" : {
schema : resolver ( Pty . Info ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
validator ( "param" , z . object ( { ptyID : z.string ( ) } ) ) ,
validator ( "json" , Pty . UpdateInput ) ,
async ( c ) = > {
const info = await Pty . update ( c . req . valid ( "param" ) . ptyID , c . req . valid ( "json" ) )
return c . json ( info )
} ,
)
. delete (
"/pty/:ptyID" ,
describeRoute ( {
summary : "Remove PTY session" ,
description : "Remove and terminate a specific pseudo-terminal (PTY) session." ,
operationId : "pty.remove" ,
responses : {
200 : {
description : "Session removed" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 404 ) ,
} ,
} ) ,
validator ( "param" , z . object ( { ptyID : z.string ( ) } ) ) ,
async ( c ) = > {
await Pty . remove ( c . req . valid ( "param" ) . ptyID )
return c . json ( true )
} ,
)
. get (
"/pty/:ptyID/connect" ,
describeRoute ( {
summary : "Connect to PTY session" ,
description :
"Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time." ,
operationId : "pty.connect" ,
responses : {
200 : {
description : "Connected session" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 404 ) ,
} ,
} ) ,
validator ( "param" , z . object ( { ptyID : z.string ( ) } ) ) ,
upgradeWebSocket ( ( c ) = > {
const id = c . req . param ( "ptyID" )
let handler : ReturnType < typeof Pty.connect >
if ( ! Pty . get ( id ) ) throw new Error ( "Session not found" )
return {
onOpen ( _event , ws ) {
handler = Pty . connect ( id , ws )
} ,
onMessage ( event ) {
handler ? . onMessage ( String ( event . data ) )
} ,
onClose() {
handler ? . onClose ( )
} ,
}
} ) ,
)
2025-12-05 10:32:08 +08:00
2026-01-08 11:29:42 +08:00
. get (
"/config" ,
describeRoute ( {
summary : "Get configuration" ,
description : "Retrieve the current OpenCode configuration settings and preferences." ,
operationId : "config.get" ,
responses : {
200 : {
description : "Get config info" ,
content : {
"application/json" : {
schema : resolver ( Config . Info ) ,
} ,
2025-09-17 13:16:49 +08:00
} ,
2025-06-03 23:59:03 +08:00
} ,
} ,
2026-01-08 11:29:42 +08:00
} ) ,
async ( c ) = > {
return c . json ( await Config . get ( ) )
2025-06-03 23:59:03 +08:00
} ,
2026-01-08 11:29:42 +08:00
)
2025-11-12 10:30:38 +08:00
2026-01-08 11:29:42 +08:00
. patch (
"/config" ,
describeRoute ( {
summary : "Update configuration" ,
description : "Update OpenCode configuration settings and preferences." ,
operationId : "config.update" ,
responses : {
200 : {
description : "Successfully updated config" ,
content : {
"application/json" : {
schema : resolver ( Config . Info ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
validator ( "json" , Config . Info ) ,
async ( c ) = > {
const config = c . req . valid ( "json" )
await Config . update ( config )
return c . json ( config )
} ,
)
. get (
"/experimental/tool/ids" ,
describeRoute ( {
summary : "List tool IDs" ,
description :
"Get a list of all available tool IDs, including both built-in tools and dynamically registered tools." ,
operationId : "tool.ids" ,
responses : {
200 : {
description : "Tool IDs" ,
content : {
"application/json" : {
schema : resolver ( z . array ( z . string ( ) ) . meta ( { ref : "ToolIDs" } ) ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
async ( c ) = > {
return c . json ( await ToolRegistry . ids ( ) )
} ,
)
. get (
"/experimental/tool" ,
describeRoute ( {
summary : "List tools" ,
description :
"Get a list of available tools with their JSON schema parameters for a specific provider and model combination." ,
operationId : "tool.list" ,
responses : {
200 : {
description : "Tools" ,
content : {
"application/json" : {
schema : resolver (
z
. array (
z
. object ( {
id : z.string ( ) ,
description : z.string ( ) ,
parameters : z.any ( ) ,
} )
. meta ( { ref : "ToolListItem" } ) ,
)
. meta ( { ref : "ToolList" } ) ,
) ,
} ,
2025-09-17 13:16:49 +08:00
} ,
2025-06-04 02:24:45 +08:00
} ,
2026-01-08 11:29:42 +08:00
. . . errors ( 400 ) ,
2025-06-04 02:24:45 +08:00
} ,
2026-01-08 11:29:42 +08:00
} ) ,
validator (
"query" ,
z . object ( {
provider : z.string ( ) ,
model : z.string ( ) ,
} ) ,
) ,
async ( c ) = > {
const { provider } = c . req . valid ( "query" )
const tools = await ToolRegistry . tools ( provider )
return c . json (
tools . map ( ( t ) = > ( {
id : t.id ,
description : t.description ,
// Handle both Zod schemas and plain JSON schemas
parameters : ( t . parameters as any ) ? . _def ? zodToJsonSchema ( t . parameters as any ) : t . parameters ,
} ) ) ,
)
2025-06-04 02:24:45 +08:00
} ,
2026-01-08 11:29:42 +08:00
)
. post (
"/instance/dispose" ,
describeRoute ( {
summary : "Dispose instance" ,
description : "Clean up and dispose the current OpenCode instance, releasing all resources." ,
operationId : "instance.dispose" ,
responses : {
200 : {
description : "Instance disposed" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
} ,
2025-09-17 13:16:49 +08:00
} ) ,
2026-01-08 11:29:42 +08:00
async ( c ) = > {
await Instance . dispose ( )
return c . json ( true )
} ,
)
. get (
"/path" ,
describeRoute ( {
summary : "Get paths" ,
description :
"Retrieve the current working directory and related path information for the OpenCode instance." ,
operationId : "path.get" ,
responses : {
200 : {
description : "Path" ,
content : {
"application/json" : {
schema : resolver (
z
. object ( {
home : z.string ( ) ,
state : z.string ( ) ,
config : z.string ( ) ,
worktree : z.string ( ) ,
directory : z.string ( ) ,
} )
. meta ( {
ref : "Path" ,
} ) ,
) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
return c . json ( {
home : Global.Path.home ,
state : Global.Path.state ,
config : Global.Path.config ,
worktree : Instance.worktree ,
directory : Instance.directory ,
} )
} ,
)
. post (
"/experimental/worktree" ,
describeRoute ( {
summary : "Create worktree" ,
description : "Create a new git worktree for the current project." ,
operationId : "worktree.create" ,
responses : {
200 : {
description : "Worktree created" ,
content : {
"application/json" : {
schema : resolver ( Worktree . Info ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
validator ( "json" , Worktree . create . schema ) ,
async ( c ) = > {
const body = c . req . valid ( "json" )
const worktree = await Worktree . create ( body )
return c . json ( worktree )
} ,
)
. get (
"/experimental/worktree" ,
describeRoute ( {
summary : "List worktrees" ,
description : "List all sandbox worktrees for the current project." ,
operationId : "worktree.list" ,
responses : {
200 : {
description : "List of worktree directories" ,
content : {
"application/json" : {
schema : resolver ( z . array ( z . string ( ) ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
const sandboxes = await Project . sandboxes ( Instance . project . id )
return c . json ( sandboxes )
} ,
)
. get (
"/vcs" ,
describeRoute ( {
summary : "Get VCS info" ,
description :
"Retrieve version control system (VCS) information for the current project, such as git branch." ,
operationId : "vcs.get" ,
responses : {
200 : {
description : "VCS info" ,
content : {
"application/json" : {
schema : resolver ( Vcs . Info ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
const branch = await Vcs . branch ( )
return c . json ( {
branch ,
} )
} ,
)
. get (
"/session" ,
describeRoute ( {
summary : "List sessions" ,
description : "Get a list of all OpenCode sessions, sorted by most recently updated." ,
operationId : "session.list" ,
responses : {
200 : {
description : "List of sessions" ,
content : {
"application/json" : {
schema : resolver ( Session . Info . array ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
validator (
"query" ,
z . object ( {
start : z.coerce
. number ( )
. optional ( )
. meta ( { description : "Filter sessions updated on or after this timestamp (milliseconds since epoch)" } ) ,
search : z.string ( ) . optional ( ) . meta ( { description : "Filter sessions by title (case-insensitive)" } ) ,
limit : z.coerce.number ( ) . optional ( ) . meta ( { description : "Maximum number of sessions to return" } ) ,
} ) ,
) ,
async ( c ) = > {
const query = c . req . valid ( "query" )
const term = query . search ? . toLowerCase ( )
const sessions : Session.Info [ ] = [ ]
for await ( const session of Session . list ( ) ) {
if ( query . start !== undefined && session . time . updated < query . start ) continue
if ( term !== undefined && ! session . title . toLowerCase ( ) . includes ( term ) ) continue
sessions . push ( session )
if ( query . limit !== undefined && sessions . length >= query . limit ) break
2025-11-21 16:13:10 +08:00
}
2026-01-08 11:29:42 +08:00
return c . json ( sessions )
} ,
)
. get (
"/session/status" ,
describeRoute ( {
summary : "Get session status" ,
description : "Retrieve the current status of all sessions, including active, idle, and completed states." ,
operationId : "session.status" ,
responses : {
200 : {
description : "Get session status" ,
content : {
"application/json" : {
schema : resolver ( z . record ( z . string ( ) , SessionStatus . Info ) ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
async ( c ) = > {
const result = SessionStatus . list ( )
return c . json ( result )
} ,
)
. get (
"/session/:sessionID" ,
describeRoute ( {
summary : "Get session" ,
description : "Retrieve detailed information about a specific OpenCode session." ,
tags : [ "Session" ] ,
operationId : "session.get" ,
responses : {
200 : {
description : "Get session" ,
content : {
"application/json" : {
schema : resolver ( Session . Info ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : Session.get.schema ,
} ) ,
) ,
async ( c ) = > {
2025-12-08 08:04:14 +08:00
const sessionID = c . req . valid ( "param" ) . sessionID
2026-01-08 11:29:42 +08:00
log . info ( "SEARCH" , { url : c.req.url } )
const session = await Session . get ( sessionID )
return c . json ( session )
} ,
)
. get (
"/session/:sessionID/children" ,
describeRoute ( {
summary : "Get session children" ,
tags : [ "Session" ] ,
description : "Retrieve all child sessions that were forked from the specified parent session." ,
operationId : "session.children" ,
responses : {
200 : {
description : "List of children" ,
content : {
"application/json" : {
schema : resolver ( Session . Info . array ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : Session.children.schema ,
} ) ,
) ,
async ( c ) = > {
2025-12-08 08:04:14 +08:00
const sessionID = c . req . valid ( "param" ) . sessionID
2026-01-08 11:29:42 +08:00
const session = await Session . children ( sessionID )
return c . json ( session )
} ,
)
. get (
"/session/:sessionID/todo" ,
describeRoute ( {
summary : "Get session todos" ,
description : "Retrieve the todo list associated with a specific session, showing tasks and action items." ,
operationId : "session.todo" ,
responses : {
200 : {
description : "Todo list" ,
content : {
"application/json" : {
schema : resolver ( Todo . Info . array ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) . meta ( { description : "Session ID" } ) ,
} ) ,
) ,
async ( c ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
const todos = await Todo . get ( sessionID )
return c . json ( todos )
} ,
)
. post (
"/session" ,
describeRoute ( {
summary : "Create session" ,
description : "Create a new OpenCode session for interacting with AI assistants and managing conversations." ,
operationId : "session.create" ,
responses : {
. . . errors ( 400 ) ,
200 : {
description : "Successfully created session" ,
content : {
"application/json" : {
schema : resolver ( Session . Info ) ,
} ,
} ,
} ,
} ,
} ) ,
validator ( "json" , Session . create . schema . optional ( ) ) ,
async ( c ) = > {
const body = c . req . valid ( "json" ) ? ? { }
const session = await Session . create ( body )
return c . json ( session )
} ,
)
. delete (
"/session/:sessionID" ,
describeRoute ( {
summary : "Delete session" ,
description : "Delete a session and permanently remove all associated data, including messages and history." ,
operationId : "session.delete" ,
responses : {
200 : {
description : "Successfully deleted session" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : Session.remove.schema ,
} ) ,
) ,
async ( c ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
await Session . remove ( sessionID )
return c . json ( true )
} ,
)
. patch (
"/session/:sessionID" ,
describeRoute ( {
summary : "Update session" ,
description : "Update properties of an existing session, such as title or other metadata." ,
operationId : "session.update" ,
responses : {
200 : {
description : "Successfully updated session" ,
content : {
"application/json" : {
schema : resolver ( Session . Info ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) ,
} ) ,
) ,
validator (
"json" ,
z . object ( {
title : z.string ( ) . optional ( ) ,
time : z
. object ( {
archived : z.number ( ) . optional ( ) ,
} )
. optional ( ) ,
} ) ,
) ,
async ( c ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
const updates = c . req . valid ( "json" )
const updatedSession = await Session . update ( sessionID , ( session ) = > {
if ( updates . title !== undefined ) {
session . title = updates . title
}
if ( updates . time ? . archived !== undefined ) session . time . archived = updates . time . archived
} )
return c . json ( updatedSession )
} ,
)
. post (
"/session/:sessionID/init" ,
describeRoute ( {
summary : "Initialize session" ,
description :
"Analyze the current application and create an AGENTS.md file with project-specific agent configurations." ,
operationId : "session.init" ,
responses : {
200 : {
description : "200" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) . meta ( { description : "Session ID" } ) ,
} ) ,
) ,
validator ( "json" , Session . initialize . schema . omit ( { sessionID : true } ) ) ,
async ( c ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
const body = c . req . valid ( "json" )
await Session . initialize ( { . . . body , sessionID } )
return c . json ( true )
} ,
)
. post (
"/session/:sessionID/fork" ,
describeRoute ( {
summary : "Fork session" ,
description : "Create a new session by forking an existing session at a specific message point." ,
operationId : "session.fork" ,
responses : {
200 : {
description : "200" ,
content : {
"application/json" : {
schema : resolver ( Session . Info ) ,
} ,
} ,
} ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : Session.fork.schema.shape.sessionID ,
} ) ,
) ,
validator ( "json" , Session . fork . schema . omit ( { sessionID : true } ) ) ,
async ( c ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
const body = c . req . valid ( "json" )
const result = await Session . fork ( { . . . body , sessionID } )
return c . json ( result )
} ,
)
. post (
"/session/:sessionID/abort" ,
describeRoute ( {
summary : "Abort session" ,
description : "Abort an active session and stop any ongoing AI processing or command execution." ,
operationId : "session.abort" ,
responses : {
200 : {
description : "Aborted session" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) ,
} ) ,
) ,
async ( c ) = > {
SessionPrompt . cancel ( c . req . valid ( "param" ) . sessionID )
return c . json ( true )
} ,
)
. post (
"/session/:sessionID/share" ,
describeRoute ( {
summary : "Share session" ,
description : "Create a shareable link for a session, allowing others to view the conversation." ,
operationId : "session.share" ,
responses : {
200 : {
description : "Successfully shared session" ,
content : {
"application/json" : {
schema : resolver ( Session . Info ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) ,
} ) ,
) ,
async ( c ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
await Session . share ( sessionID )
const session = await Session . get ( sessionID )
return c . json ( session )
} ,
)
. get (
"/session/:sessionID/diff" ,
describeRoute ( {
summary : "Get message diff" ,
description : "Get the file changes (diff) that resulted from a specific user message in the session." ,
operationId : "session.diff" ,
responses : {
200 : {
description : "Successfully retrieved diff" ,
content : {
"application/json" : {
schema : resolver ( Snapshot . FileDiff . array ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : SessionSummary.diff.schema.shape.sessionID ,
} ) ,
) ,
validator (
"query" ,
z . object ( {
messageID : SessionSummary.diff.schema.shape.messageID ,
} ) ,
) ,
async ( c ) = > {
const query = c . req . valid ( "query" )
const params = c . req . valid ( "param" )
const result = await SessionSummary . diff ( {
sessionID : params.sessionID ,
messageID : query.messageID ,
} )
return c . json ( result )
} ,
)
. delete (
"/session/:sessionID/share" ,
describeRoute ( {
summary : "Unshare session" ,
description : "Remove the shareable link for a session, making it private again." ,
operationId : "session.unshare" ,
responses : {
200 : {
description : "Successfully unshared session" ,
content : {
"application/json" : {
schema : resolver ( Session . Info ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : Session.unshare.schema ,
} ) ,
) ,
async ( c ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
await Session . unshare ( sessionID )
const session = await Session . get ( sessionID )
return c . json ( session )
} ,
)
. post (
"/session/:sessionID/summarize" ,
describeRoute ( {
summary : "Summarize session" ,
description : "Generate a concise summary of the session using AI compaction to preserve key information." ,
operationId : "session.summarize" ,
responses : {
200 : {
description : "Summarized session" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) . meta ( { description : "Session ID" } ) ,
} ) ,
) ,
validator (
"json" ,
z . object ( {
providerID : z.string ( ) ,
modelID : z.string ( ) ,
auto : z.boolean ( ) . optional ( ) . default ( false ) ,
} ) ,
) ,
async ( c ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
const body = c . req . valid ( "json" )
const session = await Session . get ( sessionID )
await SessionRevert . cleanup ( session )
const msgs = await Session . messages ( { sessionID } )
let currentAgent = await Agent . defaultAgent ( )
for ( let i = msgs . length - 1 ; i >= 0 ; i -- ) {
const info = msgs [ i ] . info
if ( info . role === "user" ) {
currentAgent = info . agent || ( await Agent . defaultAgent ( ) )
break
}
}
await SessionCompaction . create ( {
sessionID ,
agent : currentAgent ,
model : {
providerID : body.providerID ,
modelID : body.modelID ,
} ,
auto : body.auto ,
} )
await SessionPrompt . loop ( sessionID )
return c . json ( true )
} ,
)
. get (
"/session/:sessionID/message" ,
describeRoute ( {
summary : "Get session messages" ,
description : "Retrieve all messages in a session, including user prompts and AI responses." ,
operationId : "session.messages" ,
responses : {
200 : {
description : "List of messages" ,
content : {
"application/json" : {
schema : resolver ( MessageV2 . WithParts . array ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) . meta ( { description : "Session ID" } ) ,
} ) ,
) ,
validator (
"query" ,
z . object ( {
limit : z.coerce.number ( ) . optional ( ) ,
} ) ,
) ,
async ( c ) = > {
const query = c . req . valid ( "query" )
const messages = await Session . messages ( {
sessionID : c.req.valid ( "param" ) . sessionID ,
limit : query.limit ,
} )
return c . json ( messages )
} ,
)
. get (
"/session/:sessionID/diff" ,
describeRoute ( {
summary : "Get session diff" ,
description : "Get all file changes (diffs) made during this session." ,
operationId : "session.diff" ,
responses : {
200 : {
description : "List of diffs" ,
content : {
"application/json" : {
schema : resolver ( Snapshot . FileDiff . array ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) . meta ( { description : "Session ID" } ) ,
} ) ,
) ,
async ( c ) = > {
const diff = await Session . diff ( c . req . valid ( "param" ) . sessionID )
return c . json ( diff )
} ,
)
. get (
"/session/:sessionID/message/:messageID" ,
describeRoute ( {
summary : "Get message" ,
description : "Retrieve a specific message from a session by its message ID." ,
operationId : "session.message" ,
responses : {
200 : {
description : "Message" ,
content : {
"application/json" : {
schema : resolver (
z . object ( {
info : MessageV2.Info ,
parts : MessageV2.Part.array ( ) ,
} ) ,
) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) . meta ( { description : "Session ID" } ) ,
messageID : z.string ( ) . meta ( { description : "Message ID" } ) ,
} ) ,
) ,
async ( c ) = > {
const params = c . req . valid ( "param" )
const message = await MessageV2 . get ( {
sessionID : params.sessionID ,
messageID : params.messageID ,
} )
return c . json ( message )
} ,
)
. delete (
"/session/:sessionID/message/:messageID/part/:partID" ,
describeRoute ( {
description : "Delete a part from a message" ,
operationId : "part.delete" ,
responses : {
200 : {
description : "Successfully deleted part" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) . meta ( { description : "Session ID" } ) ,
messageID : z.string ( ) . meta ( { description : "Message ID" } ) ,
partID : z.string ( ) . meta ( { description : "Part ID" } ) ,
} ) ,
) ,
async ( c ) = > {
const params = c . req . valid ( "param" )
await Session . removePart ( {
sessionID : params.sessionID ,
messageID : params.messageID ,
partID : params.partID ,
} )
return c . json ( true )
} ,
)
. patch (
"/session/:sessionID/message/:messageID/part/:partID" ,
describeRoute ( {
description : "Update a part in a message" ,
operationId : "part.update" ,
responses : {
200 : {
description : "Successfully updated part" ,
content : {
"application/json" : {
schema : resolver ( MessageV2 . Part ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) . meta ( { description : "Session ID" } ) ,
messageID : z.string ( ) . meta ( { description : "Message ID" } ) ,
partID : z.string ( ) . meta ( { description : "Part ID" } ) ,
} ) ,
) ,
validator ( "json" , MessageV2 . Part ) ,
async ( c ) = > {
const params = c . req . valid ( "param" )
const body = c . req . valid ( "json" )
if (
body . id !== params . partID ||
body . messageID !== params . messageID ||
body . sessionID !== params . sessionID
) {
throw new Error (
` Part mismatch: body.id=' ${ body . id } ' vs partID=' ${ params . partID } ', body.messageID=' ${ body . messageID } ' vs messageID=' ${ params . messageID } ', body.sessionID=' ${ body . sessionID } ' vs sessionID=' ${ params . sessionID } ' ` ,
)
}
const part = await Session . updatePart ( body )
return c . json ( part )
} ,
)
. post (
"/session/:sessionID/message" ,
describeRoute ( {
summary : "Send message" ,
description : "Create and send a new message to a session, streaming the AI response." ,
operationId : "session.prompt" ,
responses : {
200 : {
description : "Created message" ,
content : {
"application/json" : {
schema : resolver (
z . object ( {
info : MessageV2.Assistant ,
parts : MessageV2.Part.array ( ) ,
} ) ,
) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) . meta ( { description : "Session ID" } ) ,
} ) ,
) ,
validator ( "json" , SessionPrompt . PromptInput . omit ( { sessionID : true } ) ) ,
async ( c ) = > {
c . status ( 200 )
c . header ( "Content-Type" , "application/json" )
return stream ( c , async ( stream ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
const body = c . req . valid ( "json" )
const msg = await SessionPrompt . prompt ( { . . . body , sessionID } )
stream . write ( JSON . stringify ( msg ) )
} )
} ,
)
. post (
"/session/:sessionID/prompt_async" ,
describeRoute ( {
summary : "Send async message" ,
description :
"Create and send a new message to a session asynchronously, starting the session if needed and returning immediately." ,
operationId : "session.prompt_async" ,
responses : {
204 : {
description : "Prompt accepted" ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) . meta ( { description : "Session ID" } ) ,
} ) ,
) ,
validator ( "json" , SessionPrompt . PromptInput . omit ( { sessionID : true } ) ) ,
async ( c ) = > {
c . status ( 204 )
c . header ( "Content-Type" , "application/json" )
return stream ( c , async ( ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
const body = c . req . valid ( "json" )
SessionPrompt . prompt ( { . . . body , sessionID } )
} )
} ,
)
. post (
"/session/:sessionID/command" ,
describeRoute ( {
summary : "Send command" ,
description : "Send a new command to a session for execution by the AI assistant." ,
operationId : "session.command" ,
responses : {
200 : {
description : "Created message" ,
content : {
"application/json" : {
schema : resolver (
z . object ( {
info : MessageV2.Assistant ,
parts : MessageV2.Part.array ( ) ,
} ) ,
) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) . meta ( { description : "Session ID" } ) ,
} ) ,
) ,
validator ( "json" , SessionPrompt . CommandInput . omit ( { sessionID : true } ) ) ,
async ( c ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
const body = c . req . valid ( "json" )
const msg = await SessionPrompt . command ( { . . . body , sessionID } )
return c . json ( msg )
} ,
)
. post (
"/session/:sessionID/shell" ,
describeRoute ( {
summary : "Run shell command" ,
description : "Execute a shell command within the session context and return the AI's response." ,
operationId : "session.shell" ,
responses : {
200 : {
description : "Created message" ,
content : {
"application/json" : {
schema : resolver ( MessageV2 . Assistant ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) . meta ( { description : "Session ID" } ) ,
} ) ,
) ,
validator ( "json" , SessionPrompt . ShellInput . omit ( { sessionID : true } ) ) ,
async ( c ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
const body = c . req . valid ( "json" )
const msg = await SessionPrompt . shell ( { . . . body , sessionID } )
return c . json ( msg )
} ,
)
. post (
"/session/:sessionID/revert" ,
describeRoute ( {
summary : "Revert message" ,
description :
"Revert a specific message in a session, undoing its effects and restoring the previous state." ,
operationId : "session.revert" ,
responses : {
200 : {
description : "Updated session" ,
content : {
"application/json" : {
schema : resolver ( Session . Info ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) ,
} ) ,
) ,
validator ( "json" , SessionRevert . RevertInput . omit ( { sessionID : true } ) ) ,
async ( c ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
log . info ( "revert" , c . req . valid ( "json" ) )
const session = await SessionRevert . revert ( {
sessionID ,
. . . c . req . valid ( "json" ) ,
} )
return c . json ( session )
} ,
)
. post (
"/session/:sessionID/unrevert" ,
describeRoute ( {
summary : "Restore reverted messages" ,
description : "Restore all previously reverted messages in a session." ,
operationId : "session.unrevert" ,
responses : {
200 : {
description : "Updated session" ,
content : {
"application/json" : {
schema : resolver ( Session . Info ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) ,
} ) ,
) ,
async ( c ) = > {
const sessionID = c . req . valid ( "param" ) . sessionID
const session = await SessionRevert . unrevert ( { sessionID } )
return c . json ( session )
} ,
)
. post (
"/session/:sessionID/permissions/:permissionID" ,
describeRoute ( {
summary : "Respond to permission" ,
deprecated : true ,
description : "Approve or deny a permission request from the AI assistant." ,
operationId : "permission.respond" ,
responses : {
200 : {
description : "Permission processed successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
sessionID : z.string ( ) ,
permissionID : z.string ( ) ,
} ) ,
) ,
validator ( "json" , z . object ( { response : PermissionNext.Reply } ) ) ,
async ( c ) = > {
const params = c . req . valid ( "param" )
PermissionNext . reply ( {
requestID : params.permissionID ,
reply : c.req.valid ( "json" ) . response ,
} )
return c . json ( true )
} ,
)
. post (
"/permission/:requestID/reply" ,
describeRoute ( {
summary : "Respond to permission request" ,
description : "Approve or deny a permission request from the AI assistant." ,
operationId : "permission.reply" ,
responses : {
200 : {
description : "Permission processed successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
requestID : z.string ( ) ,
} ) ,
) ,
validator ( "json" , z . object ( { reply : PermissionNext.Reply , message : z.string ( ) . optional ( ) } ) ) ,
async ( c ) = > {
const params = c . req . valid ( "param" )
const json = c . req . valid ( "json" )
await PermissionNext . reply ( {
requestID : params.requestID ,
reply : json.reply ,
message : json.message ,
} )
return c . json ( true )
} ,
)
. get (
"/permission" ,
describeRoute ( {
summary : "List pending permissions" ,
description : "Get all pending permission requests across all sessions." ,
operationId : "permission.list" ,
responses : {
200 : {
description : "List of pending permissions" ,
content : {
"application/json" : {
schema : resolver ( PermissionNext . Request . array ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
const permissions = await PermissionNext . list ( )
return c . json ( permissions )
} ,
)
. route ( "/question" , QuestionRoute )
. get (
"/command" ,
describeRoute ( {
summary : "List commands" ,
description : "Get a list of all available commands in the OpenCode system." ,
operationId : "command.list" ,
responses : {
200 : {
description : "List of commands" ,
content : {
"application/json" : {
schema : resolver ( Command . Info . array ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
const commands = await Command . list ( )
return c . json ( commands )
} ,
)
. get (
"/config/providers" ,
describeRoute ( {
summary : "List config providers" ,
description : "Get a list of all configured AI providers and their default models." ,
operationId : "config.providers" ,
responses : {
200 : {
description : "List of providers" ,
content : {
"application/json" : {
schema : resolver (
z . object ( {
providers : Provider.Info.array ( ) ,
default : z . record ( z . string ( ) , z . string ( ) ) ,
} ) ,
) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
using _ = log . time ( "providers" )
const providers = await Provider . list ( ) . then ( ( x ) = > mapValues ( x , ( item ) = > item ) )
return c . json ( {
providers : Object.values ( providers ) ,
default : mapValues ( providers , ( item ) = > Provider . sort ( Object . values ( item . models ) ) [ 0 ] . id ) ,
} )
} ,
)
. get (
"/provider" ,
describeRoute ( {
summary : "List providers" ,
description : "Get a list of all available AI providers, including both available and connected ones." ,
operationId : "provider.list" ,
responses : {
200 : {
description : "List of providers" ,
content : {
"application/json" : {
schema : resolver (
z . object ( {
all : ModelsDev.Provider.array ( ) ,
default : z . record ( z . string ( ) , z . string ( ) ) ,
connected : z.array ( z . string ( ) ) ,
} ) ,
) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
const config = await Config . get ( )
const disabled = new Set ( config . disabled_providers ? ? [ ] )
const enabled = config . enabled_providers ? new Set ( config . enabled_providers ) : undefined
const allProviders = await ModelsDev . get ( )
const filteredProviders : Record < string , ( typeof allProviders ) [ string ] > = { }
for ( const [ key , value ] of Object . entries ( allProviders ) ) {
if ( ( enabled ? enabled . has ( key ) : true ) && ! disabled . has ( key ) ) {
filteredProviders [ key ] = value
}
}
const connected = await Provider . list ( )
const providers = Object . assign (
mapValues ( filteredProviders , ( x ) = > Provider . fromModelsDevProvider ( x ) ) ,
connected ,
)
return c . json ( {
all : Object.values ( providers ) ,
default : mapValues ( providers , ( item ) = > Provider . sort ( Object . values ( item . models ) ) [ 0 ] . id ) ,
connected : Object.keys ( connected ) ,
} )
} ,
)
. get (
"/provider/auth" ,
describeRoute ( {
summary : "Get provider auth methods" ,
description : "Retrieve available authentication methods for all AI providers." ,
operationId : "provider.auth" ,
responses : {
200 : {
description : "Provider auth methods" ,
content : {
"application/json" : {
schema : resolver ( z . record ( z . string ( ) , z . array ( ProviderAuth . Method ) ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
return c . json ( await ProviderAuth . methods ( ) )
} ,
)
. post (
"/provider/:providerID/oauth/authorize" ,
describeRoute ( {
summary : "OAuth authorize" ,
description : "Initiate OAuth authorization for a specific AI provider to get an authorization URL." ,
operationId : "provider.oauth.authorize" ,
responses : {
200 : {
description : "Authorization URL and method" ,
content : {
"application/json" : {
schema : resolver ( ProviderAuth . Authorization . optional ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
providerID : z.string ( ) . meta ( { description : "Provider ID" } ) ,
} ) ,
) ,
validator (
"json" ,
z . object ( {
method : z.number ( ) . meta ( { description : "Auth method index" } ) ,
} ) ,
) ,
async ( c ) = > {
const providerID = c . req . valid ( "param" ) . providerID
const { method } = c . req . valid ( "json" )
const result = await ProviderAuth . authorize ( {
providerID ,
method ,
} )
return c . json ( result )
} ,
)
. post (
"/provider/:providerID/oauth/callback" ,
describeRoute ( {
summary : "OAuth callback" ,
description : "Handle the OAuth callback from a provider after user authorization." ,
operationId : "provider.oauth.callback" ,
responses : {
200 : {
description : "OAuth callback processed successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
providerID : z.string ( ) . meta ( { description : "Provider ID" } ) ,
} ) ,
) ,
validator (
"json" ,
z . object ( {
method : z.number ( ) . meta ( { description : "Auth method index" } ) ,
code : z.string ( ) . optional ( ) . meta ( { description : "OAuth authorization code" } ) ,
} ) ,
) ,
async ( c ) = > {
const providerID = c . req . valid ( "param" ) . providerID
const { method , code } = c . req . valid ( "json" )
await ProviderAuth . callback ( {
providerID ,
method ,
code ,
} )
return c . json ( true )
} ,
)
. get (
"/find" ,
describeRoute ( {
summary : "Find text" ,
description : "Search for text patterns across files in the project using ripgrep." ,
operationId : "find.text" ,
responses : {
200 : {
description : "Matches" ,
content : {
"application/json" : {
schema : resolver ( Ripgrep . Match . shape . data . array ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
validator (
"query" ,
z . object ( {
pattern : z.string ( ) ,
} ) ,
) ,
async ( c ) = > {
const pattern = c . req . valid ( "query" ) . pattern
const result = await Ripgrep . search ( {
cwd : Instance.directory ,
pattern ,
limit : 10 ,
} )
return c . json ( result )
} ,
)
. get (
"/find/file" ,
describeRoute ( {
summary : "Find files" ,
description : "Search for files or directories by name or pattern in the project directory." ,
operationId : "find.files" ,
responses : {
200 : {
description : "File paths" ,
content : {
"application/json" : {
schema : resolver ( z . string ( ) . array ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
validator (
"query" ,
z . object ( {
query : z.string ( ) ,
dirs : z.enum ( [ "true" , "false" ] ) . optional ( ) ,
type : z . enum ( [ "file" , "directory" ] ) . optional ( ) ,
limit : z.coerce.number ( ) . int ( ) . min ( 1 ) . max ( 200 ) . optional ( ) ,
} ) ,
) ,
async ( c ) = > {
const query = c . req . valid ( "query" ) . query
const dirs = c . req . valid ( "query" ) . dirs
const type = c . req . valid ( "query" ) . type
const limit = c . req . valid ( "query" ) . limit
const results = await File . search ( {
query ,
limit : limit ? ? 10 ,
dirs : dirs !== "false" ,
type ,
} )
return c . json ( results )
} ,
)
. get (
"/find/symbol" ,
describeRoute ( {
summary : "Find symbols" ,
description : "Search for workspace symbols like functions, classes, and variables using LSP." ,
operationId : "find.symbols" ,
responses : {
200 : {
description : "Symbols" ,
content : {
"application/json" : {
schema : resolver ( LSP . Symbol . array ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
validator (
"query" ,
z . object ( {
query : z.string ( ) ,
} ) ,
) ,
async ( c ) = > {
/ *
const query = c . req . valid ( "query" ) . query
const result = await LSP . workspaceSymbol ( query )
return c . json ( result )
* /
return c . json ( [ ] )
} ,
)
. get (
"/file" ,
describeRoute ( {
summary : "List files" ,
description : "List files and directories in a specified path." ,
operationId : "file.list" ,
responses : {
200 : {
description : "Files and directories" ,
content : {
"application/json" : {
schema : resolver ( File . Node . array ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
validator (
"query" ,
z . object ( {
path : z.string ( ) ,
} ) ,
) ,
async ( c ) = > {
const path = c . req . valid ( "query" ) . path
const content = await File . list ( path )
return c . json ( content )
} ,
)
. get (
"/file/content" ,
describeRoute ( {
summary : "Read file" ,
description : "Read the content of a specified file." ,
operationId : "file.read" ,
responses : {
200 : {
description : "File content" ,
content : {
"application/json" : {
schema : resolver ( File . Content ) ,
} ,
} ,
} ,
} ,
} ) ,
validator (
"query" ,
z . object ( {
path : z.string ( ) ,
} ) ,
) ,
async ( c ) = > {
const path = c . req . valid ( "query" ) . path
const content = await File . read ( path )
return c . json ( content )
} ,
)
. get (
"/file/status" ,
describeRoute ( {
summary : "Get file status" ,
description : "Get the git status of all files in the project." ,
operationId : "file.status" ,
responses : {
200 : {
description : "File status" ,
content : {
"application/json" : {
schema : resolver ( File . Info . array ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
const content = await File . status ( )
return c . json ( content )
} ,
)
. post (
"/log" ,
describeRoute ( {
summary : "Write log" ,
description : "Write a log entry to the server logs with specified level and metadata." ,
operationId : "app.log" ,
responses : {
200 : {
description : "Log entry written successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
validator (
"json" ,
z . object ( {
service : z.string ( ) . meta ( { description : "Service name for the log entry" } ) ,
level : z.enum ( [ "debug" , "info" , "error" , "warn" ] ) . meta ( { description : "Log level" } ) ,
message : z.string ( ) . meta ( { description : "Log message" } ) ,
extra : z
. record ( z . string ( ) , z . any ( ) )
. optional ( )
. meta ( { description : "Additional metadata for the log entry" } ) ,
} ) ,
) ,
async ( c ) = > {
const { service , level , message , extra } = c . req . valid ( "json" )
const logger = Log . create ( { service } )
2025-07-09 21:16:10 +08:00
2026-01-08 11:29:42 +08:00
switch ( level ) {
case "debug" :
logger . debug ( message , extra )
break
case "info" :
logger . info ( message , extra )
break
case "error" :
logger . error ( message , extra )
break
case "warn" :
logger . warn ( message , extra )
break
}
2025-07-09 21:16:10 +08:00
2026-01-08 11:29:42 +08:00
return c . json ( true )
} ,
)
. get (
"/agent" ,
describeRoute ( {
summary : "List agents" ,
description : "Get a list of all available AI agents in the OpenCode system." ,
operationId : "app.agents" ,
responses : {
200 : {
description : "List of agents" ,
content : {
"application/json" : {
schema : resolver ( Agent . Info . array ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
const modes = await Agent . list ( )
return c . json ( modes )
} ,
)
. get (
"/mcp" ,
describeRoute ( {
summary : "Get MCP status" ,
description : "Get the status of all Model Context Protocol (MCP) servers." ,
operationId : "mcp.status" ,
responses : {
200 : {
description : "MCP server status" ,
content : {
"application/json" : {
schema : resolver ( z . record ( z . string ( ) , MCP . Status ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
return c . json ( await MCP . status ( ) )
} ,
)
. post (
"/mcp" ,
describeRoute ( {
summary : "Add MCP server" ,
description : "Dynamically add a new Model Context Protocol (MCP) server to the system." ,
operationId : "mcp.add" ,
responses : {
200 : {
description : "MCP server added successfully" ,
content : {
"application/json" : {
schema : resolver ( z . record ( z . string ( ) , MCP . Status ) ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
validator (
"json" ,
z . object ( {
name : z.string ( ) ,
config : Config.Mcp ,
2025-11-01 03:07:36 +08:00
} ) ,
) ,
2026-01-08 11:29:42 +08:00
async ( c ) = > {
const { name , config } = c . req . valid ( "json" )
const result = await MCP . add ( name , config )
return c . json ( result . status )
} ,
)
. post (
"/mcp/:name/auth" ,
describeRoute ( {
summary : "Start MCP OAuth" ,
description : "Start OAuth authentication flow for a Model Context Protocol (MCP) server." ,
operationId : "mcp.auth.start" ,
responses : {
200 : {
description : "OAuth flow started" ,
content : {
"application/json" : {
schema : resolver (
z . object ( {
authorizationUrl : z.string ( ) . describe ( "URL to open in browser for authorization" ) ,
} ) ,
) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
async ( c ) = > {
const name = c . req . param ( "name" )
const supportsOAuth = await MCP . supportsOAuth ( name )
if ( ! supportsOAuth ) {
return c . json ( { error : ` MCP server ${ name } does not support OAuth ` } , 400 )
}
const result = await MCP . startAuth ( name )
return c . json ( result )
} ,
)
. post (
"/mcp/:name/auth/callback" ,
describeRoute ( {
summary : "Complete MCP OAuth" ,
description :
"Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code." ,
operationId : "mcp.auth.callback" ,
responses : {
200 : {
description : "OAuth authentication completed" ,
content : {
"application/json" : {
schema : resolver ( MCP . Status ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator (
"json" ,
z . object ( {
code : z.string ( ) . describe ( "Authorization code from OAuth callback" ) ,
} ) ,
) ,
async ( c ) = > {
const name = c . req . param ( "name" )
const { code } = c . req . valid ( "json" )
const status = await MCP . finishAuth ( name , code )
return c . json ( status )
} ,
)
. post (
"/mcp/:name/auth/authenticate" ,
describeRoute ( {
summary : "Authenticate MCP OAuth" ,
description : "Start OAuth flow and wait for callback (opens browser)" ,
operationId : "mcp.auth.authenticate" ,
responses : {
200 : {
description : "OAuth authentication completed" ,
content : {
"application/json" : {
schema : resolver ( MCP . Status ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
async ( c ) = > {
const name = c . req . param ( "name" )
const supportsOAuth = await MCP . supportsOAuth ( name )
if ( ! supportsOAuth ) {
return c . json ( { error : ` MCP server ${ name } does not support OAuth ` } , 400 )
}
const status = await MCP . authenticate ( name )
return c . json ( status )
} ,
)
. delete (
"/mcp/:name/auth" ,
describeRoute ( {
summary : "Remove MCP OAuth" ,
description : "Remove OAuth credentials for an MCP server" ,
operationId : "mcp.auth.remove" ,
responses : {
200 : {
description : "OAuth credentials removed" ,
content : {
"application/json" : {
schema : resolver ( z . object ( { success : z.literal ( true ) } ) ) ,
} ,
} ,
} ,
. . . errors ( 404 ) ,
} ,
} ) ,
async ( c ) = > {
const name = c . req . param ( "name" )
await MCP . removeAuth ( name )
return c . json ( { success : true as const } )
} ,
)
. post (
"/mcp/:name/connect" ,
describeRoute ( {
description : "Connect an MCP server" ,
operationId : "mcp.connect" ,
responses : {
200 : {
description : "MCP server connected successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
validator ( "param" , z . object ( { name : z.string ( ) } ) ) ,
async ( c ) = > {
const { name } = c . req . valid ( "param" )
await MCP . connect ( name )
return c . json ( true )
} ,
)
. post (
"/mcp/:name/disconnect" ,
describeRoute ( {
description : "Disconnect an MCP server" ,
operationId : "mcp.disconnect" ,
responses : {
200 : {
description : "MCP server disconnected successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
validator ( "param" , z . object ( { name : z.string ( ) } ) ) ,
async ( c ) = > {
const { name } = c . req . valid ( "param" )
await MCP . disconnect ( name )
return c . json ( true )
} ,
)
. get (
"/experimental/resource" ,
describeRoute ( {
summary : "Get MCP resources" ,
description : "Get all available MCP resources from connected servers. Optionally filter by name." ,
operationId : "experimental.resource.list" ,
responses : {
200 : {
description : "MCP resources" ,
content : {
"application/json" : {
schema : resolver ( z . record ( z . string ( ) , MCP . Resource ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
return c . json ( await MCP . resources ( ) )
} ,
)
. get (
"/lsp" ,
describeRoute ( {
summary : "Get LSP status" ,
description : "Get LSP server status" ,
operationId : "lsp.status" ,
responses : {
200 : {
description : "LSP server status" ,
content : {
"application/json" : {
schema : resolver ( LSP . Status . array ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
return c . json ( await LSP . status ( ) )
} ,
)
. get (
"/formatter" ,
describeRoute ( {
summary : "Get formatter status" ,
description : "Get formatter status" ,
operationId : "formatter.status" ,
responses : {
200 : {
description : "Formatter status" ,
content : {
"application/json" : {
schema : resolver ( Format . Status . array ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
return c . json ( await Format . status ( ) )
} ,
)
. post (
"/tui/append-prompt" ,
describeRoute ( {
summary : "Append TUI prompt" ,
description : "Append prompt to the TUI" ,
operationId : "tui.appendPrompt" ,
responses : {
200 : {
description : "Prompt processed successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
validator ( "json" , TuiEvent . PromptAppend . properties ) ,
async ( c ) = > {
await Bus . publish ( TuiEvent . PromptAppend , c . req . valid ( "json" ) )
return c . json ( true )
} ,
)
. post (
"/tui/open-help" ,
describeRoute ( {
summary : "Open help dialog" ,
description : "Open the help dialog in the TUI to display user assistance information." ,
operationId : "tui.openHelp" ,
responses : {
200 : {
description : "Help dialog opened successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
// TODO: open dialog
return c . json ( true )
} ,
)
. post (
"/tui/open-sessions" ,
describeRoute ( {
summary : "Open sessions dialog" ,
description : "Open the session dialog" ,
operationId : "tui.openSessions" ,
responses : {
200 : {
description : "Session dialog opened successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
await Bus . publish ( TuiEvent . CommandExecute , {
command : "session.list" ,
2025-09-17 13:16:49 +08:00
} )
2026-01-08 11:29:42 +08:00
return c . json ( true )
} ,
)
. post (
"/tui/open-themes" ,
describeRoute ( {
summary : "Open themes dialog" ,
description : "Open the theme dialog" ,
operationId : "tui.openThemes" ,
responses : {
200 : {
description : "Theme dialog opened successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
await Bus . publish ( TuiEvent . CommandExecute , {
command : "session.list" ,
2025-09-17 13:16:49 +08:00
} )
2026-01-08 11:29:42 +08:00
return c . json ( true )
} ,
)
. post (
"/tui/open-models" ,
describeRoute ( {
summary : "Open models dialog" ,
description : "Open the model dialog" ,
operationId : "tui.openModels" ,
responses : {
200 : {
description : "Model dialog opened successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
await Bus . publish ( TuiEvent . CommandExecute , {
command : "model.list" ,
} )
return c . json ( true )
} ,
)
. post (
"/tui/submit-prompt" ,
describeRoute ( {
summary : "Submit TUI prompt" ,
description : "Submit the prompt" ,
operationId : "tui.submitPrompt" ,
responses : {
200 : {
description : "Prompt submitted successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
await Bus . publish ( TuiEvent . CommandExecute , {
command : "prompt.submit" ,
} )
return c . json ( true )
} ,
)
. post (
"/tui/clear-prompt" ,
describeRoute ( {
summary : "Clear TUI prompt" ,
description : "Clear the prompt" ,
operationId : "tui.clearPrompt" ,
responses : {
200 : {
description : "Prompt cleared successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
await Bus . publish ( TuiEvent . CommandExecute , {
command : "prompt.clear" ,
} )
return c . json ( true )
} ,
)
. post (
"/tui/execute-command" ,
describeRoute ( {
summary : "Execute TUI command" ,
description : "Execute a TUI command (e.g. agent_cycle)" ,
operationId : "tui.executeCommand" ,
responses : {
200 : {
description : "Command executed successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
validator ( "json" , z . object ( { command : z.string ( ) } ) ) ,
async ( c ) = > {
const command = c . req . valid ( "json" ) . command
await Bus . publish ( TuiEvent . CommandExecute , {
// @ts-expect-error
command : {
session_new : "session.new" ,
session_share : "session.share" ,
session_interrupt : "session.interrupt" ,
session_compact : "session.compact" ,
messages_page_up : "session.page.up" ,
messages_page_down : "session.page.down" ,
messages_half_page_up : "session.half.page.up" ,
messages_half_page_down : "session.half.page.down" ,
messages_first : "session.first" ,
messages_last : "session.last" ,
agent_cycle : "agent.cycle" ,
} [ command ] ,
} )
return c . json ( true )
} ,
)
. post (
"/tui/show-toast" ,
describeRoute ( {
summary : "Show TUI toast" ,
description : "Show a toast notification in the TUI" ,
operationId : "tui.showToast" ,
responses : {
200 : {
description : "Toast notification shown successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
validator ( "json" , TuiEvent . ToastShow . properties ) ,
async ( c ) = > {
await Bus . publish ( TuiEvent . ToastShow , c . req . valid ( "json" ) )
return c . json ( true )
} ,
)
. post (
"/tui/publish" ,
describeRoute ( {
summary : "Publish TUI event" ,
description : "Publish a TUI event" ,
operationId : "tui.publish" ,
responses : {
200 : {
description : "Event published successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
validator (
"json" ,
z . union (
Object . values ( TuiEvent ) . map ( ( def ) = > {
return z
. object ( {
type : z . literal ( def . type ) ,
properties : def.properties ,
} )
. meta ( {
ref : "Event" + "." + def . type ,
} )
} ) ,
) ,
) ,
async ( c ) = > {
const evt = c . req . valid ( "json" )
await Bus . publish ( Object . values ( TuiEvent ) . find ( ( def ) = > def . type === evt . type ) ! , evt . properties )
return c . json ( true )
} ,
)
. post (
"/tui/select-session" ,
describeRoute ( {
summary : "Select session" ,
description : "Navigate the TUI to display the specified session." ,
operationId : "tui.selectSession" ,
responses : {
200 : {
description : "Session selected successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 , 404 ) ,
} ,
} ) ,
validator ( "json" , TuiEvent . SessionSelect . properties ) ,
async ( c ) = > {
const { sessionID } = c . req . valid ( "json" )
await Session . get ( sessionID )
await Bus . publish ( TuiEvent . SessionSelect , { sessionID } )
return c . json ( true )
} ,
)
. route ( "/tui/control" , TuiRoute )
. put (
"/auth/:providerID" ,
describeRoute ( {
summary : "Set auth credentials" ,
description : "Set authentication credentials" ,
operationId : "auth.set" ,
responses : {
200 : {
description : "Successfully set authentication credentials" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
} ,
} ,
} ,
. . . errors ( 400 ) ,
} ,
} ) ,
validator (
"param" ,
z . object ( {
providerID : z.string ( ) ,
} ) ,
) ,
validator ( "json" , Auth . Info ) ,
async ( c ) = > {
const providerID = c . req . valid ( "param" ) . providerID
const info = c . req . valid ( "json" )
await Auth . set ( providerID , info )
return c . json ( true )
} ,
)
. get (
"/event" ,
describeRoute ( {
summary : "Subscribe to events" ,
description : "Get events" ,
operationId : "event.subscribe" ,
responses : {
200 : {
description : "Event stream" ,
content : {
"text/event-stream" : {
schema : resolver ( BusEvent . payloads ( ) ) ,
} ,
} ,
} ,
} ,
} ) ,
async ( c ) = > {
log . info ( "event connected" )
return streamSSE ( c , async ( stream ) = > {
2025-12-17 03:45:03 +08:00
stream . writeSSE ( {
data : JSON.stringify ( {
2026-01-08 11:29:42 +08:00
type : "server.connected" ,
2025-12-17 03:45:03 +08:00
properties : { } ,
} ) ,
} )
2026-01-08 11:29:42 +08:00
const unsub = Bus . subscribeAll ( async ( event ) = > {
await stream . writeSSE ( {
data : JSON.stringify ( event ) ,
} )
if ( event . type === Bus . InstanceDisposed . type ) {
stream . close ( )
}
} )
// Send heartbeat every 30s to prevent WKWebView timeout (60s default)
const heartbeat = setInterval ( ( ) = > {
stream . writeSSE ( {
data : JSON.stringify ( {
type : "server.heartbeat" ,
properties : { } ,
} ) ,
} )
} , 30000 )
2025-12-17 03:45:03 +08:00
2026-01-08 11:29:42 +08:00
await new Promise < void > ( ( resolve ) = > {
stream . onAbort ( ( ) = > {
clearInterval ( heartbeat )
unsub ( )
resolve ( )
log . info ( "event disconnected" )
} )
2025-09-17 13:16:49 +08:00
} )
} )
2025-11-05 01:33:03 +08:00
} ,
2026-01-08 11:29:42 +08:00
)
. all ( "/*" , async ( c ) = > {
const path = c . req . path
const response = await proxy ( ` https://app.opencode.ai ${ path } ` , {
. . . c . req ,
headers : {
. . . c . req . raw . headers ,
host : "app.opencode.ai" ,
} ,
} )
2026-01-10 05:33:46 +08:00
response . headers . set (
"Content-Security-Policy" ,
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'" ,
)
2026-01-08 11:29:42 +08:00
return response
} ) as unknown as Hono ,
2025-09-17 13:16:49 +08:00
)
2025-05-19 10:30:41 +08:00
export async function openapi() {
2026-01-08 11:29:42 +08:00
// Cast to break excessive type recursion from long route chains
const result = await generateSpecs ( App ( ) as Hono , {
2025-05-19 10:30:41 +08:00
documentation : {
info : {
title : "opencode" ,
version : "1.0.0" ,
description : "opencode api" ,
} ,
2025-08-15 00:10:32 +08:00
openapi : "3.1.1" ,
2025-05-19 10:30:41 +08:00
} ,
2025-06-01 02:41:00 +08:00
} )
return result
2025-05-19 02:28:08 +08:00
}
2025-05-19 02:13:04 +08:00
2025-12-31 17:50:29 +08:00
export function listen ( opts : { port : number ; hostname : string ; mdns? : boolean ; cors? : string [ ] } ) {
_corsWhitelist = opts . cors ? ? [ ]
2025-12-22 03:57:55 +08:00
const args = {
2025-06-25 08:52:09 +08:00
hostname : opts.hostname ,
2025-05-19 02:13:04 +08:00
idleTimeout : 0 ,
2025-09-17 13:16:49 +08:00
fetch : App ( ) . fetch ,
2025-12-05 10:32:08 +08:00
websocket : websocket ,
2025-12-22 20:05:10 +08:00
} as const
2025-12-27 04:24:44 +08:00
const tryServe = ( port : number ) = > {
2025-12-22 20:05:10 +08:00
try {
2025-12-27 04:24:44 +08:00
return Bun . serve ( { . . . args , port } )
2025-12-22 20:05:10 +08:00
} catch {
2025-12-27 04:24:44 +08:00
return undefined
2025-12-22 20:05:10 +08:00
}
}
2025-12-27 04:24:44 +08:00
const server = opts . port === 0 ? ( tryServe ( 4096 ) ? ? tryServe ( 0 ) ) : tryServe ( opts . port )
if ( ! server ) throw new Error ( ` Failed to start server on port ${ opts . port } ` )
2025-12-30 11:05:08 +08:00
_url = server . url
2025-12-27 04:24:44 +08:00
const shouldPublishMDNS =
opts . mdns &&
server . port &&
opts . hostname !== "127.0.0.1" &&
opts . hostname !== "localhost" &&
opts . hostname !== "::1"
if ( shouldPublishMDNS ) {
2025-12-31 21:33:49 +08:00
MDNS . publish ( server . port ! , ` opencode- ${ server . port ! } ` )
2025-12-27 04:24:44 +08:00
} else if ( opts . mdns ) {
log . warn ( "mDNS enabled but hostname is loopback; skipping mDNS publish" )
}
const originalStop = server . stop . bind ( server )
server . stop = async ( closeActiveConnections? : boolean ) = > {
if ( shouldPublishMDNS ) MDNS . unpublish ( )
return originalStop ( closeActiveConnections )
}
return server
2025-05-18 09:31:42 +08:00
}
}