| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873 |
- import { BusEvent } from "@/bus/bus-event"
- import { Bus } from "@/bus"
- import { GlobalBus } from "@/bus/global"
- import { Log } from "../util/log"
- import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
- import { Hono } from "hono"
- import { cors } from "hono/cors"
- import { stream, streamSSE } from "hono/streaming"
- import { proxy } from "hono/proxy"
- import { Session } from "../session"
- import z from "zod"
- import { Provider } from "../provider/provider"
- import { filter, mapValues, sortBy, pipe } from "remeda"
- import { NamedError } from "@opencode-ai/util/error"
- import { ModelsDev } from "../provider/models"
- import { Ripgrep } from "../file/ripgrep"
- import { Config } from "../config/config"
- import { File } from "../file"
- import { LSP } from "../lsp"
- import { Format } from "../format"
- import { MessageV2 } from "../session/message-v2"
- import { TuiRoute } from "./tui"
- import { Instance } from "../project/instance"
- import { Project } from "../project/project"
- import { Vcs } from "../project/vcs"
- import { Agent } from "../agent/agent"
- import { Auth } from "../auth"
- import { Command } from "../command"
- import { ProviderAuth } from "../provider/auth"
- import { Global } from "../global"
- import { ProjectRoute } from "./project"
- import { ToolRegistry } from "../tool/registry"
- import { zodToJsonSchema } from "zod-to-json-schema"
- import { SessionPrompt } from "../session/prompt"
- import { SessionCompaction } from "../session/compaction"
- import { SessionRevert } from "../session/revert"
- import { lazy } from "../util/lazy"
- import { Todo } from "../session/todo"
- import { InstanceBootstrap } from "../project/bootstrap"
- import { MCP } from "../mcp"
- import { Storage } from "../storage/storage"
- import type { ContentfulStatusCode } from "hono/utils/http-status"
- import { TuiEvent } from "@/cli/cmd/tui/event"
- import { Snapshot } from "@/snapshot"
- import { SessionSummary } from "@/session/summary"
- import { SessionStatus } from "@/session/status"
- import { upgradeWebSocket, websocket } from "hono/bun"
- import { errors } from "./error"
- import { Pty } from "@/pty"
- import { PermissionNext } from "@/permission/next"
- import { Installation } from "@/installation"
- import { MDNS } from "./mdns"
- import { Worktree } from "../worktree"
- // @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
- export namespace Server {
- const log = Log.create({ service: "server" })
- let _url: URL | undefined
- let _corsWhitelist: string[] = []
- export function url(): URL {
- return _url ?? new URL("http://localhost:4096")
- }
- export const Event = {
- Connected: BusEvent.define("server.connected", z.object({})),
- Disposed: BusEvent.define("global.disposed", z.object({})),
- }
- const app = new Hono()
- export const App = lazy(() =>
- 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 })
- }
- const message = err instanceof Error && err.stack ? err.stack : err.toString()
- return c.json(new NamedError.Unknown({ message }).toObject(), {
- status: 500,
- })
- })
- .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", {
- method: c.req.method,
- path: c.req.path,
- })
- await next()
- if (!skipLogging) {
- timer.stop()
- }
- })
- .use(
- cors({
- origin(input) {
- if (!input) return
- 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
- // *.opencode.ai (https only, adjust if needed)
- if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
- return input
- }
- if (_corsWhitelist.includes(input)) {
- return input
- }
- 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() })),
- },
- },
- },
- },
- }),
- 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",
- }),
- ),
- },
- },
- },
- },
- }),
- async (c) => {
- log.info("global event connected")
- return streamSSE(c, async (stream) => {
- stream.writeSSE({
- data: JSON.stringify({
- payload: {
- type: "server.connected",
- properties: {},
- },
- }),
- })
- 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)
- await new Promise<void>((resolve) => {
- stream.onAbort(() => {
- clearInterval(heartbeat)
- GlobalBus.off("event", handler)
- resolve()
- log.info("global event disconnected")
- })
- })
- })
- },
- )
- .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) => {
- const directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
- return Instance.provide({
- directory,
- init: InstanceBootstrap,
- async fn() {
- return next()
- },
- })
- })
- .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() })))
- .route("/project", ProjectRoute)
- .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()),
- },
- },
- },
- },
- }),
- 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),
- },
- }),
- 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()
- },
- }
- }),
- )
- .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),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json(await Config.get())
- },
- )
- .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" }),
- ),
- },
- },
- },
- ...errors(400),
- },
- }),
- 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,
- })),
- )
- },
- )
- .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()),
- },
- },
- },
- },
- }),
- 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
- }
- 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) => {
- const sessionID = c.req.valid("param").sessionID
- 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) => {
- const sessionID = c.req.valid("param").sessionID
- 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)
- },
- )
- .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 })
- 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
- }
- 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,
- }),
- ),
- 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",
- })
- 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",
- })
- 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) => {
- stream.writeSSE({
- data: JSON.stringify({
- type: "server.connected",
- properties: {},
- }),
- })
- 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)
- await new Promise<void>((resolve) => {
- stream.onAbort(() => {
- clearInterval(heartbeat)
- unsub()
- resolve()
- log.info("event disconnected")
- })
- })
- })
- },
- )
- .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",
- },
- })
- return response
- }),
- )
- export async function openapi() {
- const result = await generateSpecs(App(), {
- documentation: {
- info: {
- title: "opencode",
- version: "1.0.0",
- description: "opencode api",
- },
- openapi: "3.1.1",
- },
- })
- return result
- }
- export function listen(opts: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
- _corsWhitelist = opts.cors ?? []
- const args = {
- hostname: opts.hostname,
- idleTimeout: 0,
- fetch: App().fetch,
- websocket: websocket,
- } as const
- const tryServe = (port: number) => {
- try {
- return Bun.serve({ ...args, port })
- } catch {
- return undefined
- }
- }
- 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}`)
- _url = server.url
- const shouldPublishMDNS =
- opts.mdns &&
- server.port &&
- opts.hostname !== "127.0.0.1" &&
- opts.hostname !== "localhost" &&
- opts.hostname !== "::1"
- if (shouldPublishMDNS) {
- MDNS.publish(server.port!, `opencode-${server.port!}`)
- } 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
- }
- }
|