server.ts 71 KB

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