| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029 |
- import { Log } from "../util/log"
- import { Bus } from "../bus"
- 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 { mapValues } from "remeda"
- import { NamedError } from "../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 { Permission } from "../permission"
- import { Instance } from "../project/instance"
- 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 { GlobalBus } from "@/bus/global"
- import { SessionStatus } from "@/session/status"
- // @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
- const ERRORS = {
- 400: {
- description: "Bad request",
- content: {
- "application/json": {
- schema: resolver(
- z
- .object({
- data: z.any(),
- errors: z.array(z.record(z.string(), z.any())),
- success: z.literal(false),
- })
- .meta({
- ref: "BadRequestError",
- }),
- ),
- },
- },
- },
- 404: {
- description: "Not found",
- content: {
- "application/json": {
- schema: resolver(Storage.NotFoundError.Schema),
- },
- },
- },
- } as const
- function errors(...codes: number[]) {
- return Object.fromEntries(codes.map((code) => [code, ERRORS[code as keyof typeof ERRORS]]))
- }
- export namespace Server {
- const log = Log.create({ service: "server" })
- export const Event = {
- Connected: Bus.event("server.connected", 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 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())
- .get(
- "/global/event",
- describeRoute({
- description: "Get events",
- operationId: "global.event",
- responses: {
- 200: {
- description: "Event stream",
- content: {
- "text/event-stream": {
- schema: resolver(
- z
- .object({
- directory: z.string(),
- payload: Bus.payloads(),
- })
- .meta({
- ref: "GlobalEvent",
- }),
- ),
- },
- },
- },
- },
- }),
- async (c) => {
- log.info("global event connected")
- return streamSSE(c, async (stream) => {
- async function handler(event: any) {
- await stream.writeSSE({
- data: JSON.stringify(event),
- })
- }
- GlobalBus.on("event", handler)
- await new Promise<void>((resolve) => {
- stream.onAbort(() => {
- GlobalBus.off("event", handler)
- resolve()
- log.info("global event disconnected")
- })
- })
- })
- },
- )
- .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(
- "/config",
- describeRoute({
- description: "Get config info",
- 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({
- description: "Update config",
- 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({
- description: "List all tool IDs (including built-in and dynamically registered)",
- 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({
- description: "List tools with JSON schema parameters for a provider/model",
- 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, model } = c.req.valid("query")
- const tools = await ToolRegistry.tools(provider, model)
- 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({
- description: "Dispose the current instance",
- 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({
- description: "Get the current path",
- operationId: "path.get",
- responses: {
- 200: {
- description: "Path",
- content: {
- "application/json": {
- schema: resolver(
- z
- .object({
- state: z.string(),
- config: z.string(),
- worktree: z.string(),
- directory: z.string(),
- })
- .meta({
- ref: "Path",
- }),
- ),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json({
- state: Global.Path.state,
- config: Global.Path.config,
- worktree: Instance.worktree,
- directory: Instance.directory,
- })
- },
- )
- .get(
- "/session",
- describeRoute({
- description: "List all sessions",
- operationId: "session.list",
- responses: {
- 200: {
- description: "List of sessions",
- content: {
- "application/json": {
- schema: resolver(Session.Info.array()),
- },
- },
- },
- },
- }),
- async (c) => {
- const sessions = await Array.fromAsync(Session.list())
- sessions.sort((a, b) => b.time.updated - a.time.updated)
- return c.json(sessions)
- },
- )
- .get(
- "/session/status",
- describeRoute({
- description: "Get session status",
- 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/:id",
- describeRoute({
- description: "Get session",
- operationId: "session.get",
- responses: {
- 200: {
- description: "Get session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: Session.get.schema,
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").id
- const session = await Session.get(sessionID)
- return c.json(session)
- },
- )
- .get(
- "/session/:id/children",
- describeRoute({
- description: "Get a session's children",
- operationId: "session.children",
- responses: {
- 200: {
- description: "List of children",
- content: {
- "application/json": {
- schema: resolver(Session.Info.array()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: Session.children.schema,
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").id
- const session = await Session.children(sessionID)
- return c.json(session)
- },
- )
- .get(
- "/session/:id/todo",
- describeRoute({
- description: "Get the todo list for a session",
- operationId: "session.todo",
- responses: {
- 200: {
- description: "Todo list",
- content: {
- "application/json": {
- schema: resolver(Todo.Info.array()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: z.string().meta({ description: "Session ID" }),
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").id
- const todos = await Todo.get(sessionID)
- return c.json(todos)
- },
- )
- .post(
- "/session",
- describeRoute({
- description: "Create a new session",
- 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/:id",
- describeRoute({
- description: "Delete a session and all its data",
- operationId: "session.delete",
- responses: {
- 200: {
- description: "Successfully deleted session",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: Session.remove.schema,
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").id
- await Session.remove(sessionID)
- await Bus.publish(TuiEvent.CommandExecute, {
- command: "session.list",
- })
- return c.json(true)
- },
- )
- .patch(
- "/session/:id",
- describeRoute({
- description: "Update session properties",
- operationId: "session.update",
- responses: {
- 200: {
- description: "Successfully updated session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: z.string(),
- }),
- ),
- validator(
- "json",
- z.object({
- title: z.string().optional(),
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").id
- const updates = c.req.valid("json")
- const updatedSession = await Session.update(sessionID, (session) => {
- if (updates.title !== undefined) {
- session.title = updates.title
- }
- })
- return c.json(updatedSession)
- },
- )
- .post(
- "/session/:id/init",
- describeRoute({
- description: "Analyze the app and create an AGENTS.md file",
- operationId: "session.init",
- responses: {
- 200: {
- description: "200",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: z.string().meta({ description: "Session ID" }),
- }),
- ),
- validator("json", Session.initialize.schema.omit({ sessionID: true })),
- async (c) => {
- const sessionID = c.req.valid("param").id
- const body = c.req.valid("json")
- await Session.initialize({ ...body, sessionID })
- return c.json(true)
- },
- )
- .post(
- "/session/:id/fork",
- describeRoute({
- description: "Fork an existing session at a specific message",
- operationId: "session.fork",
- responses: {
- 200: {
- description: "200",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- },
- }),
- validator(
- "param",
- z.object({
- id: Session.fork.schema.shape.sessionID,
- }),
- ),
- validator("json", Session.fork.schema.omit({ sessionID: true })),
- async (c) => {
- const sessionID = c.req.valid("param").id
- const body = c.req.valid("json")
- const result = await Session.fork({ ...body, sessionID })
- return c.json(result)
- },
- )
- .post(
- "/session/:id/abort",
- describeRoute({
- description: "Abort a session",
- operationId: "session.abort",
- responses: {
- 200: {
- description: "Aborted session",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: z.string(),
- }),
- ),
- async (c) => {
- SessionPrompt.cancel(c.req.valid("param").id)
- return c.json(true)
- },
- )
- .post(
- "/session/:id/share",
- describeRoute({
- description: "Share a session",
- operationId: "session.share",
- responses: {
- 200: {
- description: "Successfully shared session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: z.string(),
- }),
- ),
- async (c) => {
- const id = c.req.valid("param").id
- await Session.share(id)
- const session = await Session.get(id)
- return c.json(session)
- },
- )
- .get(
- "/session/:id/diff",
- describeRoute({
- description: "Get the diff that resulted from this user message",
- operationId: "session.diff",
- responses: {
- 200: {
- description: "Successfully retrieved diff",
- content: {
- "application/json": {
- schema: resolver(Snapshot.FileDiff.array()),
- },
- },
- },
- },
- }),
- validator(
- "param",
- z.object({
- id: 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.id,
- messageID: query.messageID,
- })
- return c.json(result)
- },
- )
- .delete(
- "/session/:id/share",
- describeRoute({
- description: "Unshare the session",
- operationId: "session.unshare",
- responses: {
- 200: {
- description: "Successfully unshared session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: Session.unshare.schema,
- }),
- ),
- async (c) => {
- const id = c.req.valid("param").id
- await Session.unshare(id)
- const session = await Session.get(id)
- return c.json(session)
- },
- )
- .post(
- "/session/:id/summarize",
- describeRoute({
- description: "Summarize the session",
- operationId: "session.summarize",
- responses: {
- 200: {
- description: "Summarized session",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: z.string().meta({ description: "Session ID" }),
- }),
- ),
- validator(
- "json",
- z.object({
- providerID: z.string(),
- modelID: z.string(),
- }),
- ),
- async (c) => {
- const id = c.req.valid("param").id
- const body = c.req.valid("json")
- const msgs = await Session.messages({ sessionID: id })
- let currentAgent = "build"
- for (let i = msgs.length - 1; i >= 0; i--) {
- const info = msgs[i].info
- if (info.role === "user") {
- currentAgent = info.agent || "build"
- break
- }
- }
- await SessionCompaction.create({
- sessionID: id,
- agent: currentAgent,
- model: {
- providerID: body.providerID,
- modelID: body.modelID,
- },
- })
- await SessionPrompt.loop(id)
- return c.json(true)
- },
- )
- .get(
- "/session/:id/message",
- describeRoute({
- description: "List messages for a session",
- operationId: "session.messages",
- responses: {
- 200: {
- description: "List of messages",
- content: {
- "application/json": {
- schema: resolver(MessageV2.WithParts.array()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: 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").id,
- limit: query.limit,
- })
- return c.json(messages)
- },
- )
- .get(
- "/session/:id/diff",
- describeRoute({
- description: "Get the diff for 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({
- id: z.string().meta({ description: "Session ID" }),
- }),
- ),
- async (c) => {
- const diff = await Session.diff(c.req.valid("param").id)
- return c.json(diff)
- },
- )
- .get(
- "/session/:id/message/:messageID",
- describeRoute({
- description: "Get a message from a session",
- 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({
- id: 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.id,
- messageID: params.messageID,
- })
- return c.json(message)
- },
- )
- .post(
- "/session/:id/message",
- describeRoute({
- description: "Create and send a new message to a session",
- 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({
- id: 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").id
- const body = c.req.valid("json")
- const msg = await SessionPrompt.prompt({ ...body, sessionID })
- stream.write(JSON.stringify(msg))
- })
- },
- )
- .post(
- "/session/:id/command",
- describeRoute({
- description: "Send a new command to a session",
- 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({
- id: z.string().meta({ description: "Session ID" }),
- }),
- ),
- validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
- async (c) => {
- const sessionID = c.req.valid("param").id
- const body = c.req.valid("json")
- const msg = await SessionPrompt.command({ ...body, sessionID })
- return c.json(msg)
- },
- )
- .post(
- "/session/:id/shell",
- describeRoute({
- description: "Run a shell command",
- operationId: "session.shell",
- responses: {
- 200: {
- description: "Created message",
- content: {
- "application/json": {
- schema: resolver(MessageV2.Assistant),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: z.string().meta({ description: "Session ID" }),
- }),
- ),
- validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
- async (c) => {
- const sessionID = c.req.valid("param").id
- const body = c.req.valid("json")
- const msg = await SessionPrompt.shell({ ...body, sessionID })
- return c.json(msg)
- },
- )
- .post(
- "/session/:id/revert",
- describeRoute({
- description: "Revert a message",
- operationId: "session.revert",
- responses: {
- 200: {
- description: "Updated session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: z.string(),
- }),
- ),
- validator("json", SessionRevert.RevertInput.omit({ sessionID: true })),
- async (c) => {
- const id = c.req.valid("param").id
- log.info("revert", c.req.valid("json"))
- const session = await SessionRevert.revert({
- sessionID: id,
- ...c.req.valid("json"),
- })
- return c.json(session)
- },
- )
- .post(
- "/session/:id/unrevert",
- describeRoute({
- description: "Restore all reverted messages",
- operationId: "session.unrevert",
- responses: {
- 200: {
- description: "Updated session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: z.string(),
- }),
- ),
- async (c) => {
- const id = c.req.valid("param").id
- const session = await SessionRevert.unrevert({ sessionID: id })
- return c.json(session)
- },
- )
- .post(
- "/session/:id/permissions/:permissionID",
- describeRoute({
- description: "Respond to a permission request",
- responses: {
- 200: {
- description: "Permission processed successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400, 404),
- },
- }),
- validator(
- "param",
- z.object({
- id: z.string(),
- permissionID: z.string(),
- }),
- ),
- validator("json", z.object({ response: Permission.Response })),
- async (c) => {
- const params = c.req.valid("param")
- const id = params.id
- const permissionID = params.permissionID
- Permission.respond({
- sessionID: id,
- permissionID,
- response: c.req.valid("json").response,
- })
- return c.json(true)
- },
- )
- .get(
- "/command",
- describeRoute({
- description: "List all commands",
- 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({
- description: "List all providers",
- operationId: "config.providers",
- responses: {
- 200: {
- description: "List of providers",
- content: {
- "application/json": {
- schema: resolver(
- z.object({
- providers: ModelsDev.Provider.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.info))
- return c.json({
- providers: Object.values(providers),
- default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
- })
- },
- )
- .get(
- "/provider",
- describeRoute({
- description: "List all providers",
- 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 providers = await ModelsDev.get()
- const connected = await Provider.list().then((x) => Object.keys(x))
- return c.json({
- all: Object.values(providers),
- default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
- connected,
- })
- },
- )
- .get(
- "/provider/auth",
- describeRoute({
- description: "Get provider authentication methods",
- 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/:id/oauth/authorize",
- describeRoute({
- description: "Authorize a provider using OAuth",
- 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({
- id: z.string().meta({ description: "Provider ID" }),
- }),
- ),
- validator(
- "json",
- z.object({
- method: z.number().meta({ description: "Auth method index" }),
- }),
- ),
- async (c) => {
- const id = c.req.valid("param").id
- const { method } = c.req.valid("json")
- const result = await ProviderAuth.authorize({
- providerID: id,
- method,
- })
- return c.json(result)
- },
- )
- .post(
- "/provider/:id/oauth/callback",
- describeRoute({
- description: "Handle OAuth callback for a provider",
- operationId: "provider.oauth.callback",
- responses: {
- 200: {
- description: "OAuth callback processed successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- ...errors(400),
- },
- }),
- validator(
- "param",
- z.object({
- id: 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 id = c.req.valid("param").id
- const { method, code } = c.req.valid("json")
- await ProviderAuth.callback({
- providerID: id,
- method,
- code,
- })
- return c.json(true)
- },
- )
- .get(
- "/find",
- describeRoute({
- description: "Find text in files",
- 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({
- description: "Find files",
- 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(),
- }),
- ),
- async (c) => {
- const query = c.req.valid("query").query
- const dirs = c.req.valid("query").dirs
- const results = await File.search({
- query,
- limit: 10,
- dirs: dirs !== "false",
- })
- return c.json(results)
- },
- )
- .get(
- "/find/symbol",
- describeRoute({
- description: "Find workspace symbols",
- 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({
- description: "List files and directories",
- 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({
- description: "Read a 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({
- description: "Get file status",
- 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({
- description: "Write a log entry to the server logs",
- 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({
- description: "List all agents",
- 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({
- description: "Get MCP server status",
- 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({
- description: "Add MCP server dynamically",
- 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)
- },
- )
- .get(
- "/lsp",
- describeRoute({
- 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({
- 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({
- 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({
- description: "Open the help dialog",
- 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({
- 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({
- 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({
- 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({
- 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({
- 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({
- 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({
- 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({
- 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)
- },
- )
- .route("/tui/control", TuiRoute)
- .put(
- "/auth/:id",
- describeRoute({
- 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({
- id: z.string(),
- }),
- ),
- validator("json", Auth.Info),
- async (c) => {
- const id = c.req.valid("param").id
- const info = c.req.valid("json")
- await Auth.set(id, info)
- return c.json(true)
- },
- )
- .get(
- "/event",
- describeRoute({
- description: "Get events",
- operationId: "event.subscribe",
- responses: {
- 200: {
- description: "Event stream",
- content: {
- "text/event-stream": {
- schema: resolver(Bus.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),
- })
- })
- await new Promise<void>((resolve) => {
- stream.onAbort(() => {
- unsub()
- resolve()
- log.info("event disconnected")
- })
- })
- })
- },
- )
- .all("/*", async (c) => {
- return proxy(`https://desktop.dev.opencode.ai${c.req.path}`, {
- ...c.req,
- headers: {
- host: "desktop.dev.opencode.ai",
- },
- })
- }),
- )
- 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 }) {
- const server = Bun.serve({
- port: opts.port,
- hostname: opts.hostname,
- idleTimeout: 0,
- fetch: App().fetch,
- })
- return server
- }
- }
|