server.ts 79 KB

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