server.ts 78 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620
  1. import { BusEvent } from "@/bus/bus-event"
  2. import { Bus } from "@/bus"
  3. import { GlobalBus } from "@/bus/global"
  4. import { Log } from "../util/log"
  5. import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
  6. import { Hono } from "hono"
  7. import { cors } from "hono/cors"
  8. import { stream, streamSSE } from "hono/streaming"
  9. import { proxy } from "hono/proxy"
  10. import { Session } from "../session"
  11. import z from "zod"
  12. import { Provider } from "../provider/provider"
  13. import { filter, mapValues, sortBy, pipe } from "remeda"
  14. import { NamedError } from "@opencode-ai/util/error"
  15. import { ModelsDev } from "../provider/models"
  16. import { Ripgrep } from "../file/ripgrep"
  17. import { Config } from "../config/config"
  18. import { File } from "../file"
  19. import { LSP } from "../lsp"
  20. import { Format } from "../format"
  21. import { MessageV2 } from "../session/message-v2"
  22. import { TuiRoute } from "./tui"
  23. import { Permission } from "../permission"
  24. import { Instance } from "../project/instance"
  25. import { Vcs } from "../project/vcs"
  26. import { Agent } from "../agent/agent"
  27. import { Auth } from "../auth"
  28. import { Command } from "../command"
  29. import { ProviderAuth } from "../provider/auth"
  30. import { Global } from "../global"
  31. import { ProjectRoute } from "./project"
  32. import { ToolRegistry } from "../tool/registry"
  33. import { zodToJsonSchema } from "zod-to-json-schema"
  34. import { SessionPrompt } from "../session/prompt"
  35. import { SessionCompaction } from "../session/compaction"
  36. import { SessionRevert } from "../session/revert"
  37. import { lazy } from "../util/lazy"
  38. import { Todo } from "../session/todo"
  39. import { InstanceBootstrap } from "../project/bootstrap"
  40. import { MCP } from "../mcp"
  41. import { Storage } from "../storage/storage"
  42. import type { ContentfulStatusCode } from "hono/utils/http-status"
  43. import { TuiEvent } from "@/cli/cmd/tui/event"
  44. import { Snapshot } from "@/snapshot"
  45. import { SessionSummary } from "@/session/summary"
  46. import { SessionStatus } from "@/session/status"
  47. import { upgradeWebSocket, websocket } from "hono/bun"
  48. import { errors } from "./error"
  49. import { Pty } from "@/pty"
  50. // @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
  51. globalThis.AI_SDK_LOG_WARNINGS = false
  52. export namespace Server {
  53. const log = Log.create({ service: "server" })
  54. export const Event = {
  55. Connected: BusEvent.define("server.connected", z.object({})),
  56. Disposed: BusEvent.define("global.disposed", z.object({})),
  57. }
  58. const app = new Hono()
  59. export const App = lazy(() =>
  60. app
  61. .onError((err, c) => {
  62. log.error("failed", {
  63. error: err,
  64. })
  65. if (err instanceof NamedError) {
  66. let status: ContentfulStatusCode
  67. if (err instanceof Storage.NotFoundError) status = 404
  68. else if (err instanceof Provider.ModelNotFoundError) status = 400
  69. else status = 500
  70. return c.json(err.toObject(), { status })
  71. }
  72. const message = err instanceof Error && err.stack ? err.stack : err.toString()
  73. return c.json(new NamedError.Unknown({ message }).toObject(), {
  74. status: 500,
  75. })
  76. })
  77. .use(async (c, next) => {
  78. const skipLogging = c.req.path === "/log"
  79. if (!skipLogging) {
  80. log.info("request", {
  81. method: c.req.method,
  82. path: c.req.path,
  83. })
  84. }
  85. const timer = log.time("request", {
  86. method: c.req.method,
  87. path: c.req.path,
  88. })
  89. await next()
  90. if (!skipLogging) {
  91. timer.stop()
  92. }
  93. })
  94. .use(cors())
  95. .get(
  96. "/global/event",
  97. describeRoute({
  98. summary: "Get global events",
  99. description: "Subscribe to global events from the OpenCode system using server-sent events.",
  100. operationId: "global.event",
  101. responses: {
  102. 200: {
  103. description: "Event stream",
  104. content: {
  105. "text/event-stream": {
  106. schema: resolver(
  107. z
  108. .object({
  109. directory: z.string(),
  110. payload: BusEvent.payloads(),
  111. })
  112. .meta({
  113. ref: "GlobalEvent",
  114. }),
  115. ),
  116. },
  117. },
  118. },
  119. },
  120. }),
  121. async (c) => {
  122. log.info("global event connected")
  123. return streamSSE(c, async (stream) => {
  124. stream.writeSSE({
  125. data: JSON.stringify({
  126. payload: {
  127. type: "server.connected",
  128. properties: {},
  129. },
  130. }),
  131. })
  132. async function handler(event: any) {
  133. await stream.writeSSE({
  134. data: JSON.stringify(event),
  135. })
  136. }
  137. GlobalBus.on("event", handler)
  138. // Send heartbeat every 30s to prevent WKWebView timeout (60s default)
  139. const heartbeat = setInterval(() => {
  140. stream.writeSSE({
  141. data: JSON.stringify({
  142. payload: {
  143. type: "server.heartbeat",
  144. properties: {},
  145. },
  146. }),
  147. })
  148. }, 30000)
  149. await new Promise<void>((resolve) => {
  150. stream.onAbort(() => {
  151. clearInterval(heartbeat)
  152. GlobalBus.off("event", handler)
  153. resolve()
  154. log.info("global event disconnected")
  155. })
  156. })
  157. })
  158. },
  159. )
  160. .post(
  161. "/global/dispose",
  162. describeRoute({
  163. summary: "Dispose instance",
  164. description: "Clean up and dispose all OpenCode instances, releasing all resources.",
  165. operationId: "global.dispose",
  166. responses: {
  167. 200: {
  168. description: "Global disposed",
  169. content: {
  170. "application/json": {
  171. schema: resolver(z.boolean()),
  172. },
  173. },
  174. },
  175. },
  176. }),
  177. async (c) => {
  178. await Instance.disposeAll()
  179. GlobalBus.emit("event", {
  180. directory: "global",
  181. payload: {
  182. type: Event.Disposed.type,
  183. properties: {},
  184. },
  185. })
  186. return c.json(true)
  187. },
  188. )
  189. .use(async (c, next) => {
  190. const directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
  191. return Instance.provide({
  192. directory,
  193. init: InstanceBootstrap,
  194. async fn() {
  195. return next()
  196. },
  197. })
  198. })
  199. .get(
  200. "/doc",
  201. openAPIRouteHandler(app, {
  202. documentation: {
  203. info: {
  204. title: "opencode",
  205. version: "0.0.3",
  206. description: "opencode api",
  207. },
  208. openapi: "3.1.1",
  209. },
  210. }),
  211. )
  212. .use(validator("query", z.object({ directory: z.string().optional() })))
  213. .route("/project", ProjectRoute)
  214. .get(
  215. "/pty",
  216. describeRoute({
  217. summary: "List PTY sessions",
  218. description: "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.",
  219. operationId: "pty.list",
  220. responses: {
  221. 200: {
  222. description: "List of sessions",
  223. content: {
  224. "application/json": {
  225. schema: resolver(Pty.Info.array()),
  226. },
  227. },
  228. },
  229. },
  230. }),
  231. async (c) => {
  232. return c.json(Pty.list())
  233. },
  234. )
  235. .post(
  236. "/pty",
  237. describeRoute({
  238. summary: "Create PTY session",
  239. description: "Create a new pseudo-terminal (PTY) session for running shell commands and processes.",
  240. operationId: "pty.create",
  241. responses: {
  242. 200: {
  243. description: "Created session",
  244. content: {
  245. "application/json": {
  246. schema: resolver(Pty.Info),
  247. },
  248. },
  249. },
  250. ...errors(400),
  251. },
  252. }),
  253. validator("json", Pty.CreateInput),
  254. async (c) => {
  255. const info = await Pty.create(c.req.valid("json"))
  256. return c.json(info)
  257. },
  258. )
  259. .get(
  260. "/pty/:ptyID",
  261. describeRoute({
  262. summary: "Get PTY session",
  263. description: "Retrieve detailed information about a specific pseudo-terminal (PTY) session.",
  264. operationId: "pty.get",
  265. responses: {
  266. 200: {
  267. description: "Session info",
  268. content: {
  269. "application/json": {
  270. schema: resolver(Pty.Info),
  271. },
  272. },
  273. },
  274. ...errors(404),
  275. },
  276. }),
  277. validator("param", z.object({ ptyID: z.string() })),
  278. async (c) => {
  279. const info = Pty.get(c.req.valid("param").ptyID)
  280. if (!info) {
  281. throw new Storage.NotFoundError({ message: "Session not found" })
  282. }
  283. return c.json(info)
  284. },
  285. )
  286. .put(
  287. "/pty/:ptyID",
  288. describeRoute({
  289. summary: "Update PTY session",
  290. description: "Update properties of an existing pseudo-terminal (PTY) session.",
  291. operationId: "pty.update",
  292. responses: {
  293. 200: {
  294. description: "Updated session",
  295. content: {
  296. "application/json": {
  297. schema: resolver(Pty.Info),
  298. },
  299. },
  300. },
  301. ...errors(400),
  302. },
  303. }),
  304. validator("param", z.object({ ptyID: z.string() })),
  305. validator("json", Pty.UpdateInput),
  306. async (c) => {
  307. const info = await Pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
  308. return c.json(info)
  309. },
  310. )
  311. .delete(
  312. "/pty/:ptyID",
  313. describeRoute({
  314. summary: "Remove PTY session",
  315. description: "Remove and terminate a specific pseudo-terminal (PTY) session.",
  316. operationId: "pty.remove",
  317. responses: {
  318. 200: {
  319. description: "Session removed",
  320. content: {
  321. "application/json": {
  322. schema: resolver(z.boolean()),
  323. },
  324. },
  325. },
  326. ...errors(404),
  327. },
  328. }),
  329. validator("param", z.object({ ptyID: z.string() })),
  330. async (c) => {
  331. await Pty.remove(c.req.valid("param").ptyID)
  332. return c.json(true)
  333. },
  334. )
  335. .get(
  336. "/pty/:ptyID/connect",
  337. describeRoute({
  338. summary: "Connect to PTY session",
  339. description:
  340. "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.",
  341. operationId: "pty.connect",
  342. responses: {
  343. 200: {
  344. description: "Connected session",
  345. content: {
  346. "application/json": {
  347. schema: resolver(z.boolean()),
  348. },
  349. },
  350. },
  351. ...errors(404),
  352. },
  353. }),
  354. validator("param", z.object({ ptyID: z.string() })),
  355. upgradeWebSocket((c) => {
  356. const id = c.req.param("ptyID")
  357. let handler: ReturnType<typeof Pty.connect>
  358. if (!Pty.get(id)) throw new Error("Session not found")
  359. return {
  360. onOpen(_event, ws) {
  361. handler = Pty.connect(id, ws)
  362. },
  363. onMessage(event) {
  364. handler?.onMessage(String(event.data))
  365. },
  366. onClose() {
  367. handler?.onClose()
  368. },
  369. }
  370. }),
  371. )
  372. .get(
  373. "/config",
  374. describeRoute({
  375. summary: "Get configuration",
  376. description: "Retrieve the current OpenCode configuration settings and preferences.",
  377. operationId: "config.get",
  378. responses: {
  379. 200: {
  380. description: "Get config info",
  381. content: {
  382. "application/json": {
  383. schema: resolver(Config.Info),
  384. },
  385. },
  386. },
  387. },
  388. }),
  389. async (c) => {
  390. return c.json(await Config.get())
  391. },
  392. )
  393. .patch(
  394. "/config",
  395. describeRoute({
  396. summary: "Update configuration",
  397. description: "Update OpenCode configuration settings and preferences.",
  398. operationId: "config.update",
  399. responses: {
  400. 200: {
  401. description: "Successfully updated config",
  402. content: {
  403. "application/json": {
  404. schema: resolver(Config.Info),
  405. },
  406. },
  407. },
  408. ...errors(400),
  409. },
  410. }),
  411. validator("json", Config.Info),
  412. async (c) => {
  413. const config = c.req.valid("json")
  414. await Config.update(config)
  415. return c.json(config)
  416. },
  417. )
  418. .get(
  419. "/experimental/tool/ids",
  420. describeRoute({
  421. summary: "List tool IDs",
  422. description:
  423. "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
  424. operationId: "tool.ids",
  425. responses: {
  426. 200: {
  427. description: "Tool IDs",
  428. content: {
  429. "application/json": {
  430. schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })),
  431. },
  432. },
  433. },
  434. ...errors(400),
  435. },
  436. }),
  437. async (c) => {
  438. return c.json(await ToolRegistry.ids())
  439. },
  440. )
  441. .get(
  442. "/experimental/tool",
  443. describeRoute({
  444. summary: "List tools",
  445. description:
  446. "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
  447. operationId: "tool.list",
  448. responses: {
  449. 200: {
  450. description: "Tools",
  451. content: {
  452. "application/json": {
  453. schema: resolver(
  454. z
  455. .array(
  456. z
  457. .object({
  458. id: z.string(),
  459. description: z.string(),
  460. parameters: z.any(),
  461. })
  462. .meta({ ref: "ToolListItem" }),
  463. )
  464. .meta({ ref: "ToolList" }),
  465. ),
  466. },
  467. },
  468. },
  469. ...errors(400),
  470. },
  471. }),
  472. validator(
  473. "query",
  474. z.object({
  475. provider: z.string(),
  476. model: z.string(),
  477. }),
  478. ),
  479. async (c) => {
  480. const { provider } = c.req.valid("query")
  481. const tools = await ToolRegistry.tools(provider)
  482. return c.json(
  483. tools.map((t) => ({
  484. id: t.id,
  485. description: t.description,
  486. // Handle both Zod schemas and plain JSON schemas
  487. parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters,
  488. })),
  489. )
  490. },
  491. )
  492. .post(
  493. "/instance/dispose",
  494. describeRoute({
  495. summary: "Dispose instance",
  496. description: "Clean up and dispose the current OpenCode instance, releasing all resources.",
  497. operationId: "instance.dispose",
  498. responses: {
  499. 200: {
  500. description: "Instance disposed",
  501. content: {
  502. "application/json": {
  503. schema: resolver(z.boolean()),
  504. },
  505. },
  506. },
  507. },
  508. }),
  509. async (c) => {
  510. await Instance.dispose()
  511. return c.json(true)
  512. },
  513. )
  514. .get(
  515. "/path",
  516. describeRoute({
  517. summary: "Get paths",
  518. description: "Retrieve the current working directory and related path information for the OpenCode instance.",
  519. operationId: "path.get",
  520. responses: {
  521. 200: {
  522. description: "Path",
  523. content: {
  524. "application/json": {
  525. schema: resolver(
  526. z
  527. .object({
  528. home: z.string(),
  529. state: z.string(),
  530. config: z.string(),
  531. worktree: z.string(),
  532. directory: z.string(),
  533. })
  534. .meta({
  535. ref: "Path",
  536. }),
  537. ),
  538. },
  539. },
  540. },
  541. },
  542. }),
  543. async (c) => {
  544. return c.json({
  545. home: Global.Path.home,
  546. state: Global.Path.state,
  547. config: Global.Path.config,
  548. worktree: Instance.worktree,
  549. directory: Instance.directory,
  550. })
  551. },
  552. )
  553. .get(
  554. "/vcs",
  555. describeRoute({
  556. summary: "Get VCS info",
  557. description: "Retrieve version control system (VCS) information for the current project, such as git branch.",
  558. operationId: "vcs.get",
  559. responses: {
  560. 200: {
  561. description: "VCS info",
  562. content: {
  563. "application/json": {
  564. schema: resolver(Vcs.Info),
  565. },
  566. },
  567. },
  568. },
  569. }),
  570. async (c) => {
  571. const branch = await Vcs.branch()
  572. return c.json({
  573. branch,
  574. })
  575. },
  576. )
  577. .get(
  578. "/session",
  579. describeRoute({
  580. summary: "List sessions",
  581. description: "Get a list of all OpenCode sessions, sorted by most recently updated.",
  582. operationId: "session.list",
  583. responses: {
  584. 200: {
  585. description: "List of sessions",
  586. content: {
  587. "application/json": {
  588. schema: resolver(Session.Info.array()),
  589. },
  590. },
  591. },
  592. },
  593. }),
  594. async (c) => {
  595. const sessions = await Array.fromAsync(Session.list())
  596. pipe(
  597. await Array.fromAsync(Session.list()),
  598. filter((s) => !s.time.archived),
  599. sortBy((s) => s.time.updated),
  600. )
  601. return c.json(sessions)
  602. },
  603. )
  604. .get(
  605. "/session/status",
  606. describeRoute({
  607. summary: "Get session status",
  608. description: "Retrieve the current status of all sessions, including active, idle, and completed states.",
  609. operationId: "session.status",
  610. responses: {
  611. 200: {
  612. description: "Get session status",
  613. content: {
  614. "application/json": {
  615. schema: resolver(z.record(z.string(), SessionStatus.Info)),
  616. },
  617. },
  618. },
  619. ...errors(400),
  620. },
  621. }),
  622. async (c) => {
  623. const result = SessionStatus.list()
  624. return c.json(result)
  625. },
  626. )
  627. .get(
  628. "/session/:sessionID",
  629. describeRoute({
  630. summary: "Get session",
  631. description: "Retrieve detailed information about a specific OpenCode session.",
  632. tags: ["Session"],
  633. operationId: "session.get",
  634. responses: {
  635. 200: {
  636. description: "Get session",
  637. content: {
  638. "application/json": {
  639. schema: resolver(Session.Info),
  640. },
  641. },
  642. },
  643. ...errors(400, 404),
  644. },
  645. }),
  646. validator(
  647. "param",
  648. z.object({
  649. sessionID: Session.get.schema,
  650. }),
  651. ),
  652. async (c) => {
  653. const sessionID = c.req.valid("param").sessionID
  654. log.info("SEARCH", { url: c.req.url })
  655. const session = await Session.get(sessionID)
  656. return c.json(session)
  657. },
  658. )
  659. .get(
  660. "/session/:sessionID/children",
  661. describeRoute({
  662. summary: "Get session children",
  663. tags: ["Session"],
  664. description: "Retrieve all child sessions that were forked from the specified parent session.",
  665. operationId: "session.children",
  666. responses: {
  667. 200: {
  668. description: "List of children",
  669. content: {
  670. "application/json": {
  671. schema: resolver(Session.Info.array()),
  672. },
  673. },
  674. },
  675. ...errors(400, 404),
  676. },
  677. }),
  678. validator(
  679. "param",
  680. z.object({
  681. sessionID: Session.children.schema,
  682. }),
  683. ),
  684. async (c) => {
  685. const sessionID = c.req.valid("param").sessionID
  686. const session = await Session.children(sessionID)
  687. return c.json(session)
  688. },
  689. )
  690. .get(
  691. "/session/:sessionID/todo",
  692. describeRoute({
  693. summary: "Get session todos",
  694. description: "Retrieve the todo list associated with a specific session, showing tasks and action items.",
  695. operationId: "session.todo",
  696. responses: {
  697. 200: {
  698. description: "Todo list",
  699. content: {
  700. "application/json": {
  701. schema: resolver(Todo.Info.array()),
  702. },
  703. },
  704. },
  705. ...errors(400, 404),
  706. },
  707. }),
  708. validator(
  709. "param",
  710. z.object({
  711. sessionID: z.string().meta({ description: "Session ID" }),
  712. }),
  713. ),
  714. async (c) => {
  715. const sessionID = c.req.valid("param").sessionID
  716. const todos = await Todo.get(sessionID)
  717. return c.json(todos)
  718. },
  719. )
  720. .post(
  721. "/session",
  722. describeRoute({
  723. summary: "Create session",
  724. description: "Create a new OpenCode session for interacting with AI assistants and managing conversations.",
  725. operationId: "session.create",
  726. responses: {
  727. ...errors(400),
  728. 200: {
  729. description: "Successfully created session",
  730. content: {
  731. "application/json": {
  732. schema: resolver(Session.Info),
  733. },
  734. },
  735. },
  736. },
  737. }),
  738. validator("json", Session.create.schema.optional()),
  739. async (c) => {
  740. const body = c.req.valid("json") ?? {}
  741. const session = await Session.create(body)
  742. return c.json(session)
  743. },
  744. )
  745. .delete(
  746. "/session/:sessionID",
  747. describeRoute({
  748. summary: "Delete session",
  749. description: "Delete a session and permanently remove all associated data, including messages and history.",
  750. operationId: "session.delete",
  751. responses: {
  752. 200: {
  753. description: "Successfully deleted session",
  754. content: {
  755. "application/json": {
  756. schema: resolver(z.boolean()),
  757. },
  758. },
  759. },
  760. ...errors(400, 404),
  761. },
  762. }),
  763. validator(
  764. "param",
  765. z.object({
  766. sessionID: Session.remove.schema,
  767. }),
  768. ),
  769. async (c) => {
  770. const sessionID = c.req.valid("param").sessionID
  771. await Session.remove(sessionID)
  772. return c.json(true)
  773. },
  774. )
  775. .patch(
  776. "/session/:sessionID",
  777. describeRoute({
  778. summary: "Update session",
  779. description: "Update properties of an existing session, such as title or other metadata.",
  780. operationId: "session.update",
  781. responses: {
  782. 200: {
  783. description: "Successfully updated session",
  784. content: {
  785. "application/json": {
  786. schema: resolver(Session.Info),
  787. },
  788. },
  789. },
  790. ...errors(400, 404),
  791. },
  792. }),
  793. validator(
  794. "param",
  795. z.object({
  796. sessionID: z.string(),
  797. }),
  798. ),
  799. validator(
  800. "json",
  801. z.object({
  802. title: z.string().optional(),
  803. time: z
  804. .object({
  805. archived: z.number().optional(),
  806. })
  807. .optional(),
  808. }),
  809. ),
  810. async (c) => {
  811. const sessionID = c.req.valid("param").sessionID
  812. const updates = c.req.valid("json")
  813. const updatedSession = await Session.update(sessionID, (session) => {
  814. if (updates.title !== undefined) {
  815. session.title = updates.title
  816. }
  817. if (updates.time?.archived !== undefined) session.time.archived = updates.time.archived
  818. })
  819. return c.json(updatedSession)
  820. },
  821. )
  822. .post(
  823. "/session/:sessionID/init",
  824. describeRoute({
  825. summary: "Initialize session",
  826. description:
  827. "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.",
  828. operationId: "session.init",
  829. responses: {
  830. 200: {
  831. description: "200",
  832. content: {
  833. "application/json": {
  834. schema: resolver(z.boolean()),
  835. },
  836. },
  837. },
  838. ...errors(400, 404),
  839. },
  840. }),
  841. validator(
  842. "param",
  843. z.object({
  844. sessionID: z.string().meta({ description: "Session ID" }),
  845. }),
  846. ),
  847. validator("json", Session.initialize.schema.omit({ sessionID: true })),
  848. async (c) => {
  849. const sessionID = c.req.valid("param").sessionID
  850. const body = c.req.valid("json")
  851. await Session.initialize({ ...body, sessionID })
  852. return c.json(true)
  853. },
  854. )
  855. .post(
  856. "/session/:sessionID/fork",
  857. describeRoute({
  858. summary: "Fork session",
  859. description: "Create a new session by forking an existing session at a specific message point.",
  860. operationId: "session.fork",
  861. responses: {
  862. 200: {
  863. description: "200",
  864. content: {
  865. "application/json": {
  866. schema: resolver(Session.Info),
  867. },
  868. },
  869. },
  870. },
  871. }),
  872. validator(
  873. "param",
  874. z.object({
  875. sessionID: Session.fork.schema.shape.sessionID,
  876. }),
  877. ),
  878. validator("json", Session.fork.schema.omit({ sessionID: true })),
  879. async (c) => {
  880. const sessionID = c.req.valid("param").sessionID
  881. const body = c.req.valid("json")
  882. const result = await Session.fork({ ...body, sessionID })
  883. return c.json(result)
  884. },
  885. )
  886. .post(
  887. "/session/:sessionID/abort",
  888. describeRoute({
  889. summary: "Abort session",
  890. description: "Abort an active session and stop any ongoing AI processing or command execution.",
  891. operationId: "session.abort",
  892. responses: {
  893. 200: {
  894. description: "Aborted session",
  895. content: {
  896. "application/json": {
  897. schema: resolver(z.boolean()),
  898. },
  899. },
  900. },
  901. ...errors(400, 404),
  902. },
  903. }),
  904. validator(
  905. "param",
  906. z.object({
  907. sessionID: z.string(),
  908. }),
  909. ),
  910. async (c) => {
  911. SessionPrompt.cancel(c.req.valid("param").sessionID)
  912. return c.json(true)
  913. },
  914. )
  915. .post(
  916. "/session/:sessionID/share",
  917. describeRoute({
  918. summary: "Share session",
  919. description: "Create a shareable link for a session, allowing others to view the conversation.",
  920. operationId: "session.share",
  921. responses: {
  922. 200: {
  923. description: "Successfully shared session",
  924. content: {
  925. "application/json": {
  926. schema: resolver(Session.Info),
  927. },
  928. },
  929. },
  930. ...errors(400, 404),
  931. },
  932. }),
  933. validator(
  934. "param",
  935. z.object({
  936. sessionID: z.string(),
  937. }),
  938. ),
  939. async (c) => {
  940. const sessionID = c.req.valid("param").sessionID
  941. await Session.share(sessionID)
  942. const session = await Session.get(sessionID)
  943. return c.json(session)
  944. },
  945. )
  946. .get(
  947. "/session/:sessionID/diff",
  948. describeRoute({
  949. summary: "Get message diff",
  950. description: "Get the file changes (diff) that resulted from a specific user message in the session.",
  951. operationId: "session.diff",
  952. responses: {
  953. 200: {
  954. description: "Successfully retrieved diff",
  955. content: {
  956. "application/json": {
  957. schema: resolver(Snapshot.FileDiff.array()),
  958. },
  959. },
  960. },
  961. },
  962. }),
  963. validator(
  964. "param",
  965. z.object({
  966. sessionID: SessionSummary.diff.schema.shape.sessionID,
  967. }),
  968. ),
  969. validator(
  970. "query",
  971. z.object({
  972. messageID: SessionSummary.diff.schema.shape.messageID,
  973. }),
  974. ),
  975. async (c) => {
  976. const query = c.req.valid("query")
  977. const params = c.req.valid("param")
  978. const result = await SessionSummary.diff({
  979. sessionID: params.sessionID,
  980. messageID: query.messageID,
  981. })
  982. return c.json(result)
  983. },
  984. )
  985. .delete(
  986. "/session/:sessionID/share",
  987. describeRoute({
  988. summary: "Unshare session",
  989. description: "Remove the shareable link for a session, making it private again.",
  990. operationId: "session.unshare",
  991. responses: {
  992. 200: {
  993. description: "Successfully unshared session",
  994. content: {
  995. "application/json": {
  996. schema: resolver(Session.Info),
  997. },
  998. },
  999. },
  1000. ...errors(400, 404),
  1001. },
  1002. }),
  1003. validator(
  1004. "param",
  1005. z.object({
  1006. sessionID: Session.unshare.schema,
  1007. }),
  1008. ),
  1009. async (c) => {
  1010. const sessionID = c.req.valid("param").sessionID
  1011. await Session.unshare(sessionID)
  1012. const session = await Session.get(sessionID)
  1013. return c.json(session)
  1014. },
  1015. )
  1016. .post(
  1017. "/session/:sessionID/summarize",
  1018. describeRoute({
  1019. summary: "Summarize session",
  1020. description: "Generate a concise summary of the session using AI compaction to preserve key information.",
  1021. operationId: "session.summarize",
  1022. responses: {
  1023. 200: {
  1024. description: "Summarized session",
  1025. content: {
  1026. "application/json": {
  1027. schema: resolver(z.boolean()),
  1028. },
  1029. },
  1030. },
  1031. ...errors(400, 404),
  1032. },
  1033. }),
  1034. validator(
  1035. "param",
  1036. z.object({
  1037. sessionID: z.string().meta({ description: "Session ID" }),
  1038. }),
  1039. ),
  1040. validator(
  1041. "json",
  1042. z.object({
  1043. providerID: z.string(),
  1044. modelID: z.string(),
  1045. auto: z.boolean().optional().default(false),
  1046. }),
  1047. ),
  1048. async (c) => {
  1049. const sessionID = c.req.valid("param").sessionID
  1050. const body = c.req.valid("json")
  1051. const msgs = await Session.messages({ sessionID })
  1052. let currentAgent = await Agent.defaultAgent()
  1053. for (let i = msgs.length - 1; i >= 0; i--) {
  1054. const info = msgs[i].info
  1055. if (info.role === "user") {
  1056. currentAgent = info.agent || (await Agent.defaultAgent())
  1057. break
  1058. }
  1059. }
  1060. await SessionCompaction.create({
  1061. sessionID,
  1062. agent: currentAgent,
  1063. model: {
  1064. providerID: body.providerID,
  1065. modelID: body.modelID,
  1066. },
  1067. auto: body.auto,
  1068. })
  1069. await SessionPrompt.loop(sessionID)
  1070. return c.json(true)
  1071. },
  1072. )
  1073. .get(
  1074. "/session/:sessionID/message",
  1075. describeRoute({
  1076. summary: "Get session messages",
  1077. description: "Retrieve all messages in a session, including user prompts and AI responses.",
  1078. operationId: "session.messages",
  1079. responses: {
  1080. 200: {
  1081. description: "List of messages",
  1082. content: {
  1083. "application/json": {
  1084. schema: resolver(MessageV2.WithParts.array()),
  1085. },
  1086. },
  1087. },
  1088. ...errors(400, 404),
  1089. },
  1090. }),
  1091. validator(
  1092. "param",
  1093. z.object({
  1094. sessionID: z.string().meta({ description: "Session ID" }),
  1095. }),
  1096. ),
  1097. validator(
  1098. "query",
  1099. z.object({
  1100. limit: z.coerce.number().optional(),
  1101. }),
  1102. ),
  1103. async (c) => {
  1104. const query = c.req.valid("query")
  1105. const messages = await Session.messages({
  1106. sessionID: c.req.valid("param").sessionID,
  1107. limit: query.limit,
  1108. })
  1109. return c.json(messages)
  1110. },
  1111. )
  1112. .get(
  1113. "/session/:sessionID/diff",
  1114. describeRoute({
  1115. summary: "Get session diff",
  1116. description: "Get all file changes (diffs) made during this session.",
  1117. operationId: "session.diff",
  1118. responses: {
  1119. 200: {
  1120. description: "List of diffs",
  1121. content: {
  1122. "application/json": {
  1123. schema: resolver(Snapshot.FileDiff.array()),
  1124. },
  1125. },
  1126. },
  1127. ...errors(400, 404),
  1128. },
  1129. }),
  1130. validator(
  1131. "param",
  1132. z.object({
  1133. sessionID: z.string().meta({ description: "Session ID" }),
  1134. }),
  1135. ),
  1136. async (c) => {
  1137. const diff = await Session.diff(c.req.valid("param").sessionID)
  1138. return c.json(diff)
  1139. },
  1140. )
  1141. .get(
  1142. "/session/:sessionID/message/:messageID",
  1143. describeRoute({
  1144. summary: "Get message",
  1145. description: "Retrieve a specific message from a session by its message ID.",
  1146. operationId: "session.message",
  1147. responses: {
  1148. 200: {
  1149. description: "Message",
  1150. content: {
  1151. "application/json": {
  1152. schema: resolver(
  1153. z.object({
  1154. info: MessageV2.Info,
  1155. parts: MessageV2.Part.array(),
  1156. }),
  1157. ),
  1158. },
  1159. },
  1160. },
  1161. ...errors(400, 404),
  1162. },
  1163. }),
  1164. validator(
  1165. "param",
  1166. z.object({
  1167. sessionID: z.string().meta({ description: "Session ID" }),
  1168. messageID: z.string().meta({ description: "Message ID" }),
  1169. }),
  1170. ),
  1171. async (c) => {
  1172. const params = c.req.valid("param")
  1173. const message = await MessageV2.get({
  1174. sessionID: params.sessionID,
  1175. messageID: params.messageID,
  1176. })
  1177. return c.json(message)
  1178. },
  1179. )
  1180. .delete(
  1181. "/session/:sessionID/message/:messageID/part/:partID",
  1182. describeRoute({
  1183. description: "Delete a part from a message",
  1184. operationId: "part.delete",
  1185. responses: {
  1186. 200: {
  1187. description: "Successfully deleted part",
  1188. content: {
  1189. "application/json": {
  1190. schema: resolver(z.boolean()),
  1191. },
  1192. },
  1193. },
  1194. ...errors(400, 404),
  1195. },
  1196. }),
  1197. validator(
  1198. "param",
  1199. z.object({
  1200. sessionID: z.string().meta({ description: "Session ID" }),
  1201. messageID: z.string().meta({ description: "Message ID" }),
  1202. partID: z.string().meta({ description: "Part ID" }),
  1203. }),
  1204. ),
  1205. async (c) => {
  1206. const params = c.req.valid("param")
  1207. await Session.removePart({
  1208. sessionID: params.sessionID,
  1209. messageID: params.messageID,
  1210. partID: params.partID,
  1211. })
  1212. return c.json(true)
  1213. },
  1214. )
  1215. .patch(
  1216. "/session/:sessionID/message/:messageID/part/:partID",
  1217. describeRoute({
  1218. description: "Update a part in a message",
  1219. operationId: "part.update",
  1220. responses: {
  1221. 200: {
  1222. description: "Successfully updated part",
  1223. content: {
  1224. "application/json": {
  1225. schema: resolver(MessageV2.Part),
  1226. },
  1227. },
  1228. },
  1229. ...errors(400, 404),
  1230. },
  1231. }),
  1232. validator(
  1233. "param",
  1234. z.object({
  1235. sessionID: z.string().meta({ description: "Session ID" }),
  1236. messageID: z.string().meta({ description: "Message ID" }),
  1237. partID: z.string().meta({ description: "Part ID" }),
  1238. }),
  1239. ),
  1240. validator("json", MessageV2.Part),
  1241. async (c) => {
  1242. const params = c.req.valid("param")
  1243. const body = c.req.valid("json")
  1244. if (body.id !== params.partID || body.messageID !== params.messageID || body.sessionID !== params.sessionID) {
  1245. throw new Error(
  1246. `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}'`,
  1247. )
  1248. }
  1249. const part = await Session.updatePart(body)
  1250. return c.json(part)
  1251. },
  1252. )
  1253. .post(
  1254. "/session/:sessionID/message",
  1255. describeRoute({
  1256. summary: "Send message",
  1257. description: "Create and send a new message to a session, streaming the AI response.",
  1258. operationId: "session.prompt",
  1259. responses: {
  1260. 200: {
  1261. description: "Created message",
  1262. content: {
  1263. "application/json": {
  1264. schema: resolver(
  1265. z.object({
  1266. info: MessageV2.Assistant,
  1267. parts: MessageV2.Part.array(),
  1268. }),
  1269. ),
  1270. },
  1271. },
  1272. },
  1273. ...errors(400, 404),
  1274. },
  1275. }),
  1276. validator(
  1277. "param",
  1278. z.object({
  1279. sessionID: z.string().meta({ description: "Session ID" }),
  1280. }),
  1281. ),
  1282. validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
  1283. async (c) => {
  1284. c.status(200)
  1285. c.header("Content-Type", "application/json")
  1286. return stream(c, async (stream) => {
  1287. const sessionID = c.req.valid("param").sessionID
  1288. const body = c.req.valid("json")
  1289. const msg = await SessionPrompt.prompt({ ...body, sessionID })
  1290. stream.write(JSON.stringify(msg))
  1291. })
  1292. },
  1293. )
  1294. .post(
  1295. "/session/:sessionID/prompt_async",
  1296. describeRoute({
  1297. summary: "Send async message",
  1298. description:
  1299. "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.",
  1300. operationId: "session.prompt_async",
  1301. responses: {
  1302. 204: {
  1303. description: "Prompt accepted",
  1304. },
  1305. ...errors(400, 404),
  1306. },
  1307. }),
  1308. validator(
  1309. "param",
  1310. z.object({
  1311. sessionID: z.string().meta({ description: "Session ID" }),
  1312. }),
  1313. ),
  1314. validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
  1315. async (c) => {
  1316. c.status(204)
  1317. c.header("Content-Type", "application/json")
  1318. return stream(c, async () => {
  1319. const sessionID = c.req.valid("param").sessionID
  1320. const body = c.req.valid("json")
  1321. SessionPrompt.prompt({ ...body, sessionID })
  1322. })
  1323. },
  1324. )
  1325. .post(
  1326. "/session/:sessionID/command",
  1327. describeRoute({
  1328. summary: "Send command",
  1329. description: "Send a new command to a session for execution by the AI assistant.",
  1330. operationId: "session.command",
  1331. responses: {
  1332. 200: {
  1333. description: "Created message",
  1334. content: {
  1335. "application/json": {
  1336. schema: resolver(
  1337. z.object({
  1338. info: MessageV2.Assistant,
  1339. parts: MessageV2.Part.array(),
  1340. }),
  1341. ),
  1342. },
  1343. },
  1344. },
  1345. ...errors(400, 404),
  1346. },
  1347. }),
  1348. validator(
  1349. "param",
  1350. z.object({
  1351. sessionID: z.string().meta({ description: "Session ID" }),
  1352. }),
  1353. ),
  1354. validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
  1355. async (c) => {
  1356. const sessionID = c.req.valid("param").sessionID
  1357. const body = c.req.valid("json")
  1358. const msg = await SessionPrompt.command({ ...body, sessionID })
  1359. return c.json(msg)
  1360. },
  1361. )
  1362. .post(
  1363. "/session/:sessionID/shell",
  1364. describeRoute({
  1365. summary: "Run shell command",
  1366. description: "Execute a shell command within the session context and return the AI's response.",
  1367. operationId: "session.shell",
  1368. responses: {
  1369. 200: {
  1370. description: "Created message",
  1371. content: {
  1372. "application/json": {
  1373. schema: resolver(MessageV2.Assistant),
  1374. },
  1375. },
  1376. },
  1377. ...errors(400, 404),
  1378. },
  1379. }),
  1380. validator(
  1381. "param",
  1382. z.object({
  1383. sessionID: z.string().meta({ description: "Session ID" }),
  1384. }),
  1385. ),
  1386. validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
  1387. async (c) => {
  1388. const sessionID = c.req.valid("param").sessionID
  1389. const body = c.req.valid("json")
  1390. const msg = await SessionPrompt.shell({ ...body, sessionID })
  1391. return c.json(msg)
  1392. },
  1393. )
  1394. .post(
  1395. "/session/:sessionID/revert",
  1396. describeRoute({
  1397. summary: "Revert message",
  1398. description: "Revert a specific message in a session, undoing its effects and restoring the previous state.",
  1399. operationId: "session.revert",
  1400. responses: {
  1401. 200: {
  1402. description: "Updated session",
  1403. content: {
  1404. "application/json": {
  1405. schema: resolver(Session.Info),
  1406. },
  1407. },
  1408. },
  1409. ...errors(400, 404),
  1410. },
  1411. }),
  1412. validator(
  1413. "param",
  1414. z.object({
  1415. sessionID: z.string(),
  1416. }),
  1417. ),
  1418. validator("json", SessionRevert.RevertInput.omit({ sessionID: true })),
  1419. async (c) => {
  1420. const sessionID = c.req.valid("param").sessionID
  1421. log.info("revert", c.req.valid("json"))
  1422. const session = await SessionRevert.revert({
  1423. sessionID,
  1424. ...c.req.valid("json"),
  1425. })
  1426. return c.json(session)
  1427. },
  1428. )
  1429. .post(
  1430. "/session/:sessionID/unrevert",
  1431. describeRoute({
  1432. summary: "Restore reverted messages",
  1433. description: "Restore all previously reverted messages in a session.",
  1434. operationId: "session.unrevert",
  1435. responses: {
  1436. 200: {
  1437. description: "Updated session",
  1438. content: {
  1439. "application/json": {
  1440. schema: resolver(Session.Info),
  1441. },
  1442. },
  1443. },
  1444. ...errors(400, 404),
  1445. },
  1446. }),
  1447. validator(
  1448. "param",
  1449. z.object({
  1450. sessionID: z.string(),
  1451. }),
  1452. ),
  1453. async (c) => {
  1454. const sessionID = c.req.valid("param").sessionID
  1455. const session = await SessionRevert.unrevert({ sessionID })
  1456. return c.json(session)
  1457. },
  1458. )
  1459. .post(
  1460. "/session/:sessionID/permissions/:permissionID",
  1461. describeRoute({
  1462. summary: "Respond to permission",
  1463. description: "Approve or deny a permission request from the AI assistant.",
  1464. operationId: "permission.respond",
  1465. responses: {
  1466. 200: {
  1467. description: "Permission processed successfully",
  1468. content: {
  1469. "application/json": {
  1470. schema: resolver(z.boolean()),
  1471. },
  1472. },
  1473. },
  1474. ...errors(400, 404),
  1475. },
  1476. }),
  1477. validator(
  1478. "param",
  1479. z.object({
  1480. sessionID: z.string(),
  1481. permissionID: z.string(),
  1482. }),
  1483. ),
  1484. validator("json", z.object({ response: Permission.Response })),
  1485. async (c) => {
  1486. const params = c.req.valid("param")
  1487. const sessionID = params.sessionID
  1488. const permissionID = params.permissionID
  1489. Permission.respond({
  1490. sessionID,
  1491. permissionID,
  1492. response: c.req.valid("json").response,
  1493. })
  1494. return c.json(true)
  1495. },
  1496. )
  1497. .get(
  1498. "/command",
  1499. describeRoute({
  1500. summary: "List commands",
  1501. description: "Get a list of all available commands in the OpenCode system.",
  1502. operationId: "command.list",
  1503. responses: {
  1504. 200: {
  1505. description: "List of commands",
  1506. content: {
  1507. "application/json": {
  1508. schema: resolver(Command.Info.array()),
  1509. },
  1510. },
  1511. },
  1512. },
  1513. }),
  1514. async (c) => {
  1515. const commands = await Command.list()
  1516. return c.json(commands)
  1517. },
  1518. )
  1519. .get(
  1520. "/config/providers",
  1521. describeRoute({
  1522. summary: "List config providers",
  1523. description: "Get a list of all configured AI providers and their default models.",
  1524. operationId: "config.providers",
  1525. responses: {
  1526. 200: {
  1527. description: "List of providers",
  1528. content: {
  1529. "application/json": {
  1530. schema: resolver(
  1531. z.object({
  1532. providers: Provider.Info.array(),
  1533. default: z.record(z.string(), z.string()),
  1534. }),
  1535. ),
  1536. },
  1537. },
  1538. },
  1539. },
  1540. }),
  1541. async (c) => {
  1542. using _ = log.time("providers")
  1543. const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
  1544. return c.json({
  1545. providers: Object.values(providers),
  1546. default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
  1547. })
  1548. },
  1549. )
  1550. .get(
  1551. "/provider",
  1552. describeRoute({
  1553. summary: "List providers",
  1554. description: "Get a list of all available AI providers, including both available and connected ones.",
  1555. operationId: "provider.list",
  1556. responses: {
  1557. 200: {
  1558. description: "List of providers",
  1559. content: {
  1560. "application/json": {
  1561. schema: resolver(
  1562. z.object({
  1563. all: ModelsDev.Provider.array(),
  1564. default: z.record(z.string(), z.string()),
  1565. connected: z.array(z.string()),
  1566. }),
  1567. ),
  1568. },
  1569. },
  1570. },
  1571. },
  1572. }),
  1573. async (c) => {
  1574. const config = await Config.get()
  1575. const disabled = new Set(config.disabled_providers ?? [])
  1576. const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
  1577. const allProviders = await ModelsDev.get()
  1578. const filteredProviders: Record<string, (typeof allProviders)[string]> = {}
  1579. for (const [key, value] of Object.entries(allProviders)) {
  1580. if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
  1581. filteredProviders[key] = value
  1582. }
  1583. }
  1584. const connected = await Provider.list()
  1585. const providers = Object.assign(
  1586. mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)),
  1587. connected,
  1588. )
  1589. return c.json({
  1590. all: Object.values(providers),
  1591. default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
  1592. connected: Object.keys(connected),
  1593. })
  1594. },
  1595. )
  1596. .get(
  1597. "/provider/auth",
  1598. describeRoute({
  1599. summary: "Get provider auth methods",
  1600. description: "Retrieve available authentication methods for all AI providers.",
  1601. operationId: "provider.auth",
  1602. responses: {
  1603. 200: {
  1604. description: "Provider auth methods",
  1605. content: {
  1606. "application/json": {
  1607. schema: resolver(z.record(z.string(), z.array(ProviderAuth.Method))),
  1608. },
  1609. },
  1610. },
  1611. },
  1612. }),
  1613. async (c) => {
  1614. return c.json(await ProviderAuth.methods())
  1615. },
  1616. )
  1617. .post(
  1618. "/provider/:providerID/oauth/authorize",
  1619. describeRoute({
  1620. summary: "OAuth authorize",
  1621. description: "Initiate OAuth authorization for a specific AI provider to get an authorization URL.",
  1622. operationId: "provider.oauth.authorize",
  1623. responses: {
  1624. 200: {
  1625. description: "Authorization URL and method",
  1626. content: {
  1627. "application/json": {
  1628. schema: resolver(ProviderAuth.Authorization.optional()),
  1629. },
  1630. },
  1631. },
  1632. ...errors(400),
  1633. },
  1634. }),
  1635. validator(
  1636. "param",
  1637. z.object({
  1638. providerID: z.string().meta({ description: "Provider ID" }),
  1639. }),
  1640. ),
  1641. validator(
  1642. "json",
  1643. z.object({
  1644. method: z.number().meta({ description: "Auth method index" }),
  1645. }),
  1646. ),
  1647. async (c) => {
  1648. const providerID = c.req.valid("param").providerID
  1649. const { method } = c.req.valid("json")
  1650. const result = await ProviderAuth.authorize({
  1651. providerID,
  1652. method,
  1653. })
  1654. return c.json(result)
  1655. },
  1656. )
  1657. .post(
  1658. "/provider/:providerID/oauth/callback",
  1659. describeRoute({
  1660. summary: "OAuth callback",
  1661. description: "Handle the OAuth callback from a provider after user authorization.",
  1662. operationId: "provider.oauth.callback",
  1663. responses: {
  1664. 200: {
  1665. description: "OAuth callback processed successfully",
  1666. content: {
  1667. "application/json": {
  1668. schema: resolver(z.boolean()),
  1669. },
  1670. },
  1671. },
  1672. ...errors(400),
  1673. },
  1674. }),
  1675. validator(
  1676. "param",
  1677. z.object({
  1678. providerID: z.string().meta({ description: "Provider ID" }),
  1679. }),
  1680. ),
  1681. validator(
  1682. "json",
  1683. z.object({
  1684. method: z.number().meta({ description: "Auth method index" }),
  1685. code: z.string().optional().meta({ description: "OAuth authorization code" }),
  1686. }),
  1687. ),
  1688. async (c) => {
  1689. const providerID = c.req.valid("param").providerID
  1690. const { method, code } = c.req.valid("json")
  1691. await ProviderAuth.callback({
  1692. providerID,
  1693. method,
  1694. code,
  1695. })
  1696. return c.json(true)
  1697. },
  1698. )
  1699. .get(
  1700. "/find",
  1701. describeRoute({
  1702. summary: "Find text",
  1703. description: "Search for text patterns across files in the project using ripgrep.",
  1704. operationId: "find.text",
  1705. responses: {
  1706. 200: {
  1707. description: "Matches",
  1708. content: {
  1709. "application/json": {
  1710. schema: resolver(Ripgrep.Match.shape.data.array()),
  1711. },
  1712. },
  1713. },
  1714. },
  1715. }),
  1716. validator(
  1717. "query",
  1718. z.object({
  1719. pattern: z.string(),
  1720. }),
  1721. ),
  1722. async (c) => {
  1723. const pattern = c.req.valid("query").pattern
  1724. const result = await Ripgrep.search({
  1725. cwd: Instance.directory,
  1726. pattern,
  1727. limit: 10,
  1728. })
  1729. return c.json(result)
  1730. },
  1731. )
  1732. .get(
  1733. "/find/file",
  1734. describeRoute({
  1735. summary: "Find files",
  1736. description: "Search for files by name or pattern in the project directory.",
  1737. operationId: "find.files",
  1738. responses: {
  1739. 200: {
  1740. description: "File paths",
  1741. content: {
  1742. "application/json": {
  1743. schema: resolver(z.string().array()),
  1744. },
  1745. },
  1746. },
  1747. },
  1748. }),
  1749. validator(
  1750. "query",
  1751. z.object({
  1752. query: z.string(),
  1753. dirs: z.enum(["true", "false"]).optional(),
  1754. }),
  1755. ),
  1756. async (c) => {
  1757. const query = c.req.valid("query").query
  1758. const dirs = c.req.valid("query").dirs
  1759. const results = await File.search({
  1760. query,
  1761. limit: 10,
  1762. dirs: dirs !== "false",
  1763. })
  1764. return c.json(results)
  1765. },
  1766. )
  1767. .get(
  1768. "/find/symbol",
  1769. describeRoute({
  1770. summary: "Find symbols",
  1771. description: "Search for workspace symbols like functions, classes, and variables using LSP.",
  1772. operationId: "find.symbols",
  1773. responses: {
  1774. 200: {
  1775. description: "Symbols",
  1776. content: {
  1777. "application/json": {
  1778. schema: resolver(LSP.Symbol.array()),
  1779. },
  1780. },
  1781. },
  1782. },
  1783. }),
  1784. validator(
  1785. "query",
  1786. z.object({
  1787. query: z.string(),
  1788. }),
  1789. ),
  1790. async (c) => {
  1791. /*
  1792. const query = c.req.valid("query").query
  1793. const result = await LSP.workspaceSymbol(query)
  1794. return c.json(result)
  1795. */
  1796. return c.json([])
  1797. },
  1798. )
  1799. .get(
  1800. "/file",
  1801. describeRoute({
  1802. summary: "List files",
  1803. description: "List files and directories in a specified path.",
  1804. operationId: "file.list",
  1805. responses: {
  1806. 200: {
  1807. description: "Files and directories",
  1808. content: {
  1809. "application/json": {
  1810. schema: resolver(File.Node.array()),
  1811. },
  1812. },
  1813. },
  1814. },
  1815. }),
  1816. validator(
  1817. "query",
  1818. z.object({
  1819. path: z.string(),
  1820. }),
  1821. ),
  1822. async (c) => {
  1823. const path = c.req.valid("query").path
  1824. const content = await File.list(path)
  1825. return c.json(content)
  1826. },
  1827. )
  1828. .get(
  1829. "/file/content",
  1830. describeRoute({
  1831. summary: "Read file",
  1832. description: "Read the content of a specified file.",
  1833. operationId: "file.read",
  1834. responses: {
  1835. 200: {
  1836. description: "File content",
  1837. content: {
  1838. "application/json": {
  1839. schema: resolver(File.Content),
  1840. },
  1841. },
  1842. },
  1843. },
  1844. }),
  1845. validator(
  1846. "query",
  1847. z.object({
  1848. path: z.string(),
  1849. }),
  1850. ),
  1851. async (c) => {
  1852. const path = c.req.valid("query").path
  1853. const content = await File.read(path)
  1854. return c.json(content)
  1855. },
  1856. )
  1857. .get(
  1858. "/file/status",
  1859. describeRoute({
  1860. summary: "Get file status",
  1861. description: "Get the git status of all files in the project.",
  1862. operationId: "file.status",
  1863. responses: {
  1864. 200: {
  1865. description: "File status",
  1866. content: {
  1867. "application/json": {
  1868. schema: resolver(File.Info.array()),
  1869. },
  1870. },
  1871. },
  1872. },
  1873. }),
  1874. async (c) => {
  1875. const content = await File.status()
  1876. return c.json(content)
  1877. },
  1878. )
  1879. .post(
  1880. "/log",
  1881. describeRoute({
  1882. summary: "Write log",
  1883. description: "Write a log entry to the server logs with specified level and metadata.",
  1884. operationId: "app.log",
  1885. responses: {
  1886. 200: {
  1887. description: "Log entry written successfully",
  1888. content: {
  1889. "application/json": {
  1890. schema: resolver(z.boolean()),
  1891. },
  1892. },
  1893. },
  1894. ...errors(400),
  1895. },
  1896. }),
  1897. validator(
  1898. "json",
  1899. z.object({
  1900. service: z.string().meta({ description: "Service name for the log entry" }),
  1901. level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
  1902. message: z.string().meta({ description: "Log message" }),
  1903. extra: z
  1904. .record(z.string(), z.any())
  1905. .optional()
  1906. .meta({ description: "Additional metadata for the log entry" }),
  1907. }),
  1908. ),
  1909. async (c) => {
  1910. const { service, level, message, extra } = c.req.valid("json")
  1911. const logger = Log.create({ service })
  1912. switch (level) {
  1913. case "debug":
  1914. logger.debug(message, extra)
  1915. break
  1916. case "info":
  1917. logger.info(message, extra)
  1918. break
  1919. case "error":
  1920. logger.error(message, extra)
  1921. break
  1922. case "warn":
  1923. logger.warn(message, extra)
  1924. break
  1925. }
  1926. return c.json(true)
  1927. },
  1928. )
  1929. .get(
  1930. "/agent",
  1931. describeRoute({
  1932. summary: "List agents",
  1933. description: "Get a list of all available AI agents in the OpenCode system.",
  1934. operationId: "app.agents",
  1935. responses: {
  1936. 200: {
  1937. description: "List of agents",
  1938. content: {
  1939. "application/json": {
  1940. schema: resolver(Agent.Info.array()),
  1941. },
  1942. },
  1943. },
  1944. },
  1945. }),
  1946. async (c) => {
  1947. const modes = await Agent.list()
  1948. return c.json(modes)
  1949. },
  1950. )
  1951. .get(
  1952. "/mcp",
  1953. describeRoute({
  1954. summary: "Get MCP status",
  1955. description: "Get the status of all Model Context Protocol (MCP) servers.",
  1956. operationId: "mcp.status",
  1957. responses: {
  1958. 200: {
  1959. description: "MCP server status",
  1960. content: {
  1961. "application/json": {
  1962. schema: resolver(z.record(z.string(), MCP.Status)),
  1963. },
  1964. },
  1965. },
  1966. },
  1967. }),
  1968. async (c) => {
  1969. return c.json(await MCP.status())
  1970. },
  1971. )
  1972. .post(
  1973. "/mcp",
  1974. describeRoute({
  1975. summary: "Add MCP server",
  1976. description: "Dynamically add a new Model Context Protocol (MCP) server to the system.",
  1977. operationId: "mcp.add",
  1978. responses: {
  1979. 200: {
  1980. description: "MCP server added successfully",
  1981. content: {
  1982. "application/json": {
  1983. schema: resolver(z.record(z.string(), MCP.Status)),
  1984. },
  1985. },
  1986. },
  1987. ...errors(400),
  1988. },
  1989. }),
  1990. validator(
  1991. "json",
  1992. z.object({
  1993. name: z.string(),
  1994. config: Config.Mcp,
  1995. }),
  1996. ),
  1997. async (c) => {
  1998. const { name, config } = c.req.valid("json")
  1999. const result = await MCP.add(name, config)
  2000. return c.json(result.status)
  2001. },
  2002. )
  2003. .post(
  2004. "/mcp/:name/auth",
  2005. describeRoute({
  2006. summary: "Start MCP OAuth",
  2007. description: "Start OAuth authentication flow for a Model Context Protocol (MCP) server.",
  2008. operationId: "mcp.auth.start",
  2009. responses: {
  2010. 200: {
  2011. description: "OAuth flow started",
  2012. content: {
  2013. "application/json": {
  2014. schema: resolver(
  2015. z.object({
  2016. authorizationUrl: z.string().describe("URL to open in browser for authorization"),
  2017. }),
  2018. ),
  2019. },
  2020. },
  2021. },
  2022. ...errors(400, 404),
  2023. },
  2024. }),
  2025. async (c) => {
  2026. const name = c.req.param("name")
  2027. const supportsOAuth = await MCP.supportsOAuth(name)
  2028. if (!supportsOAuth) {
  2029. return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
  2030. }
  2031. const result = await MCP.startAuth(name)
  2032. return c.json(result)
  2033. },
  2034. )
  2035. .post(
  2036. "/mcp/:name/auth/callback",
  2037. describeRoute({
  2038. summary: "Complete MCP OAuth",
  2039. description:
  2040. "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.",
  2041. operationId: "mcp.auth.callback",
  2042. responses: {
  2043. 200: {
  2044. description: "OAuth authentication completed",
  2045. content: {
  2046. "application/json": {
  2047. schema: resolver(MCP.Status),
  2048. },
  2049. },
  2050. },
  2051. ...errors(400, 404),
  2052. },
  2053. }),
  2054. validator(
  2055. "json",
  2056. z.object({
  2057. code: z.string().describe("Authorization code from OAuth callback"),
  2058. }),
  2059. ),
  2060. async (c) => {
  2061. const name = c.req.param("name")
  2062. const { code } = c.req.valid("json")
  2063. const status = await MCP.finishAuth(name, code)
  2064. return c.json(status)
  2065. },
  2066. )
  2067. .post(
  2068. "/mcp/:name/auth/authenticate",
  2069. describeRoute({
  2070. summary: "Authenticate MCP OAuth",
  2071. description: "Start OAuth flow and wait for callback (opens browser)",
  2072. operationId: "mcp.auth.authenticate",
  2073. responses: {
  2074. 200: {
  2075. description: "OAuth authentication completed",
  2076. content: {
  2077. "application/json": {
  2078. schema: resolver(MCP.Status),
  2079. },
  2080. },
  2081. },
  2082. ...errors(400, 404),
  2083. },
  2084. }),
  2085. async (c) => {
  2086. const name = c.req.param("name")
  2087. const supportsOAuth = await MCP.supportsOAuth(name)
  2088. if (!supportsOAuth) {
  2089. return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
  2090. }
  2091. const status = await MCP.authenticate(name)
  2092. return c.json(status)
  2093. },
  2094. )
  2095. .delete(
  2096. "/mcp/:name/auth",
  2097. describeRoute({
  2098. summary: "Remove MCP OAuth",
  2099. description: "Remove OAuth credentials for an MCP server",
  2100. operationId: "mcp.auth.remove",
  2101. responses: {
  2102. 200: {
  2103. description: "OAuth credentials removed",
  2104. content: {
  2105. "application/json": {
  2106. schema: resolver(z.object({ success: z.literal(true) })),
  2107. },
  2108. },
  2109. },
  2110. ...errors(404),
  2111. },
  2112. }),
  2113. async (c) => {
  2114. const name = c.req.param("name")
  2115. await MCP.removeAuth(name)
  2116. return c.json({ success: true as const })
  2117. },
  2118. )
  2119. .post(
  2120. "/mcp/:name/connect",
  2121. describeRoute({
  2122. description: "Connect an MCP server",
  2123. operationId: "mcp.connect",
  2124. responses: {
  2125. 200: {
  2126. description: "MCP server connected successfully",
  2127. content: {
  2128. "application/json": {
  2129. schema: resolver(z.boolean()),
  2130. },
  2131. },
  2132. },
  2133. },
  2134. }),
  2135. validator("param", z.object({ name: z.string() })),
  2136. async (c) => {
  2137. const { name } = c.req.valid("param")
  2138. await MCP.connect(name)
  2139. return c.json(true)
  2140. },
  2141. )
  2142. .post(
  2143. "/mcp/:name/disconnect",
  2144. describeRoute({
  2145. description: "Disconnect an MCP server",
  2146. operationId: "mcp.disconnect",
  2147. responses: {
  2148. 200: {
  2149. description: "MCP server disconnected successfully",
  2150. content: {
  2151. "application/json": {
  2152. schema: resolver(z.boolean()),
  2153. },
  2154. },
  2155. },
  2156. },
  2157. }),
  2158. validator("param", z.object({ name: z.string() })),
  2159. async (c) => {
  2160. const { name } = c.req.valid("param")
  2161. await MCP.disconnect(name)
  2162. return c.json(true)
  2163. },
  2164. )
  2165. .get(
  2166. "/lsp",
  2167. describeRoute({
  2168. summary: "Get LSP status",
  2169. description: "Get LSP server status",
  2170. operationId: "lsp.status",
  2171. responses: {
  2172. 200: {
  2173. description: "LSP server status",
  2174. content: {
  2175. "application/json": {
  2176. schema: resolver(LSP.Status.array()),
  2177. },
  2178. },
  2179. },
  2180. },
  2181. }),
  2182. async (c) => {
  2183. return c.json(await LSP.status())
  2184. },
  2185. )
  2186. .get(
  2187. "/formatter",
  2188. describeRoute({
  2189. summary: "Get formatter status",
  2190. description: "Get formatter status",
  2191. operationId: "formatter.status",
  2192. responses: {
  2193. 200: {
  2194. description: "Formatter status",
  2195. content: {
  2196. "application/json": {
  2197. schema: resolver(Format.Status.array()),
  2198. },
  2199. },
  2200. },
  2201. },
  2202. }),
  2203. async (c) => {
  2204. return c.json(await Format.status())
  2205. },
  2206. )
  2207. .post(
  2208. "/tui/append-prompt",
  2209. describeRoute({
  2210. summary: "Append TUI prompt",
  2211. description: "Append prompt to the TUI",
  2212. operationId: "tui.appendPrompt",
  2213. responses: {
  2214. 200: {
  2215. description: "Prompt processed successfully",
  2216. content: {
  2217. "application/json": {
  2218. schema: resolver(z.boolean()),
  2219. },
  2220. },
  2221. },
  2222. ...errors(400),
  2223. },
  2224. }),
  2225. validator("json", TuiEvent.PromptAppend.properties),
  2226. async (c) => {
  2227. await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
  2228. return c.json(true)
  2229. },
  2230. )
  2231. .post(
  2232. "/tui/open-help",
  2233. describeRoute({
  2234. summary: "Open help dialog",
  2235. description: "Open the help dialog in the TUI to display user assistance information.",
  2236. operationId: "tui.openHelp",
  2237. responses: {
  2238. 200: {
  2239. description: "Help dialog opened successfully",
  2240. content: {
  2241. "application/json": {
  2242. schema: resolver(z.boolean()),
  2243. },
  2244. },
  2245. },
  2246. },
  2247. }),
  2248. async (c) => {
  2249. // TODO: open dialog
  2250. return c.json(true)
  2251. },
  2252. )
  2253. .post(
  2254. "/tui/open-sessions",
  2255. describeRoute({
  2256. summary: "Open sessions dialog",
  2257. description: "Open the session dialog",
  2258. operationId: "tui.openSessions",
  2259. responses: {
  2260. 200: {
  2261. description: "Session dialog opened successfully",
  2262. content: {
  2263. "application/json": {
  2264. schema: resolver(z.boolean()),
  2265. },
  2266. },
  2267. },
  2268. },
  2269. }),
  2270. async (c) => {
  2271. await Bus.publish(TuiEvent.CommandExecute, {
  2272. command: "session.list",
  2273. })
  2274. return c.json(true)
  2275. },
  2276. )
  2277. .post(
  2278. "/tui/open-themes",
  2279. describeRoute({
  2280. summary: "Open themes dialog",
  2281. description: "Open the theme dialog",
  2282. operationId: "tui.openThemes",
  2283. responses: {
  2284. 200: {
  2285. description: "Theme dialog opened successfully",
  2286. content: {
  2287. "application/json": {
  2288. schema: resolver(z.boolean()),
  2289. },
  2290. },
  2291. },
  2292. },
  2293. }),
  2294. async (c) => {
  2295. await Bus.publish(TuiEvent.CommandExecute, {
  2296. command: "session.list",
  2297. })
  2298. return c.json(true)
  2299. },
  2300. )
  2301. .post(
  2302. "/tui/open-models",
  2303. describeRoute({
  2304. summary: "Open models dialog",
  2305. description: "Open the model dialog",
  2306. operationId: "tui.openModels",
  2307. responses: {
  2308. 200: {
  2309. description: "Model dialog opened successfully",
  2310. content: {
  2311. "application/json": {
  2312. schema: resolver(z.boolean()),
  2313. },
  2314. },
  2315. },
  2316. },
  2317. }),
  2318. async (c) => {
  2319. await Bus.publish(TuiEvent.CommandExecute, {
  2320. command: "model.list",
  2321. })
  2322. return c.json(true)
  2323. },
  2324. )
  2325. .post(
  2326. "/tui/submit-prompt",
  2327. describeRoute({
  2328. summary: "Submit TUI prompt",
  2329. description: "Submit the prompt",
  2330. operationId: "tui.submitPrompt",
  2331. responses: {
  2332. 200: {
  2333. description: "Prompt submitted successfully",
  2334. content: {
  2335. "application/json": {
  2336. schema: resolver(z.boolean()),
  2337. },
  2338. },
  2339. },
  2340. },
  2341. }),
  2342. async (c) => {
  2343. await Bus.publish(TuiEvent.CommandExecute, {
  2344. command: "prompt.submit",
  2345. })
  2346. return c.json(true)
  2347. },
  2348. )
  2349. .post(
  2350. "/tui/clear-prompt",
  2351. describeRoute({
  2352. summary: "Clear TUI prompt",
  2353. description: "Clear the prompt",
  2354. operationId: "tui.clearPrompt",
  2355. responses: {
  2356. 200: {
  2357. description: "Prompt cleared successfully",
  2358. content: {
  2359. "application/json": {
  2360. schema: resolver(z.boolean()),
  2361. },
  2362. },
  2363. },
  2364. },
  2365. }),
  2366. async (c) => {
  2367. await Bus.publish(TuiEvent.CommandExecute, {
  2368. command: "prompt.clear",
  2369. })
  2370. return c.json(true)
  2371. },
  2372. )
  2373. .post(
  2374. "/tui/execute-command",
  2375. describeRoute({
  2376. summary: "Execute TUI command",
  2377. description: "Execute a TUI command (e.g. agent_cycle)",
  2378. operationId: "tui.executeCommand",
  2379. responses: {
  2380. 200: {
  2381. description: "Command executed successfully",
  2382. content: {
  2383. "application/json": {
  2384. schema: resolver(z.boolean()),
  2385. },
  2386. },
  2387. },
  2388. ...errors(400),
  2389. },
  2390. }),
  2391. validator("json", z.object({ command: z.string() })),
  2392. async (c) => {
  2393. const command = c.req.valid("json").command
  2394. await Bus.publish(TuiEvent.CommandExecute, {
  2395. // @ts-expect-error
  2396. command: {
  2397. session_new: "session.new",
  2398. session_share: "session.share",
  2399. session_interrupt: "session.interrupt",
  2400. session_compact: "session.compact",
  2401. messages_page_up: "session.page.up",
  2402. messages_page_down: "session.page.down",
  2403. messages_half_page_up: "session.half.page.up",
  2404. messages_half_page_down: "session.half.page.down",
  2405. messages_first: "session.first",
  2406. messages_last: "session.last",
  2407. agent_cycle: "agent.cycle",
  2408. }[command],
  2409. })
  2410. return c.json(true)
  2411. },
  2412. )
  2413. .post(
  2414. "/tui/show-toast",
  2415. describeRoute({
  2416. summary: "Show TUI toast",
  2417. description: "Show a toast notification in the TUI",
  2418. operationId: "tui.showToast",
  2419. responses: {
  2420. 200: {
  2421. description: "Toast notification shown successfully",
  2422. content: {
  2423. "application/json": {
  2424. schema: resolver(z.boolean()),
  2425. },
  2426. },
  2427. },
  2428. },
  2429. }),
  2430. validator("json", TuiEvent.ToastShow.properties),
  2431. async (c) => {
  2432. await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"))
  2433. return c.json(true)
  2434. },
  2435. )
  2436. .post(
  2437. "/tui/publish",
  2438. describeRoute({
  2439. summary: "Publish TUI event",
  2440. description: "Publish a TUI event",
  2441. operationId: "tui.publish",
  2442. responses: {
  2443. 200: {
  2444. description: "Event published successfully",
  2445. content: {
  2446. "application/json": {
  2447. schema: resolver(z.boolean()),
  2448. },
  2449. },
  2450. },
  2451. ...errors(400),
  2452. },
  2453. }),
  2454. validator(
  2455. "json",
  2456. z.union(
  2457. Object.values(TuiEvent).map((def) => {
  2458. return z
  2459. .object({
  2460. type: z.literal(def.type),
  2461. properties: def.properties,
  2462. })
  2463. .meta({
  2464. ref: "Event" + "." + def.type,
  2465. })
  2466. }),
  2467. ),
  2468. ),
  2469. async (c) => {
  2470. const evt = c.req.valid("json")
  2471. await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties)
  2472. return c.json(true)
  2473. },
  2474. )
  2475. .route("/tui/control", TuiRoute)
  2476. .put(
  2477. "/auth/:providerID",
  2478. describeRoute({
  2479. summary: "Set auth credentials",
  2480. description: "Set authentication credentials",
  2481. operationId: "auth.set",
  2482. responses: {
  2483. 200: {
  2484. description: "Successfully set authentication credentials",
  2485. content: {
  2486. "application/json": {
  2487. schema: resolver(z.boolean()),
  2488. },
  2489. },
  2490. },
  2491. ...errors(400),
  2492. },
  2493. }),
  2494. validator(
  2495. "param",
  2496. z.object({
  2497. providerID: z.string(),
  2498. }),
  2499. ),
  2500. validator("json", Auth.Info),
  2501. async (c) => {
  2502. const providerID = c.req.valid("param").providerID
  2503. const info = c.req.valid("json")
  2504. await Auth.set(providerID, info)
  2505. return c.json(true)
  2506. },
  2507. )
  2508. .get(
  2509. "/event",
  2510. describeRoute({
  2511. summary: "Subscribe to events",
  2512. description: "Get events",
  2513. operationId: "event.subscribe",
  2514. responses: {
  2515. 200: {
  2516. description: "Event stream",
  2517. content: {
  2518. "text/event-stream": {
  2519. schema: resolver(BusEvent.payloads()),
  2520. },
  2521. },
  2522. },
  2523. },
  2524. }),
  2525. async (c) => {
  2526. log.info("event connected")
  2527. return streamSSE(c, async (stream) => {
  2528. stream.writeSSE({
  2529. data: JSON.stringify({
  2530. type: "server.connected",
  2531. properties: {},
  2532. }),
  2533. })
  2534. const unsub = Bus.subscribeAll(async (event) => {
  2535. await stream.writeSSE({
  2536. data: JSON.stringify(event),
  2537. })
  2538. if (event.type === Bus.InstanceDisposed.type) {
  2539. stream.close()
  2540. }
  2541. })
  2542. // Send heartbeat every 30s to prevent WKWebView timeout (60s default)
  2543. const heartbeat = setInterval(() => {
  2544. stream.writeSSE({
  2545. data: JSON.stringify({
  2546. type: "server.heartbeat",
  2547. properties: {},
  2548. }),
  2549. })
  2550. }, 30000)
  2551. await new Promise<void>((resolve) => {
  2552. stream.onAbort(() => {
  2553. clearInterval(heartbeat)
  2554. unsub()
  2555. resolve()
  2556. log.info("event disconnected")
  2557. })
  2558. })
  2559. })
  2560. },
  2561. )
  2562. .all("/*", async (c) => {
  2563. return proxy(`https://desktop.opencode.ai${c.req.path}`, {
  2564. ...c.req,
  2565. headers: {
  2566. host: "desktop.opencode.ai",
  2567. },
  2568. })
  2569. }),
  2570. )
  2571. export async function openapi() {
  2572. const result = await generateSpecs(App(), {
  2573. documentation: {
  2574. info: {
  2575. title: "opencode",
  2576. version: "1.0.0",
  2577. description: "opencode api",
  2578. },
  2579. openapi: "3.1.1",
  2580. },
  2581. })
  2582. return result
  2583. }
  2584. export function listen(opts: { port: number; hostname: string }) {
  2585. const args = {
  2586. hostname: opts.hostname,
  2587. idleTimeout: 0,
  2588. fetch: App().fetch,
  2589. websocket: websocket,
  2590. } as const
  2591. if (opts.port === 0) {
  2592. try {
  2593. return Bun.serve({ ...args, port: 4096 })
  2594. } catch {
  2595. // port 4096 not available, fall through to use port 0
  2596. }
  2597. }
  2598. return Bun.serve({ ...args, port: opts.port })
  2599. }
  2600. }