server.ts 73 KB

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