server.ts 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802
  1. import { Log } from "../util/log"
  2. import { Bus } from "../bus"
  3. import {
  4. describeRoute,
  5. generateSpecs,
  6. validator,
  7. resolver,
  8. openAPIRouteHandler,
  9. } from "hono-openapi"
  10. import { Hono } from "hono"
  11. import { cors } from "hono/cors"
  12. import { stream, streamSSE } from "hono/streaming"
  13. import { proxy } from "hono/proxy"
  14. import { Session } from "../session"
  15. import z from "zod"
  16. import { Provider } from "../provider/provider"
  17. import { mapValues } from "remeda"
  18. import { NamedError } from "../util/error"
  19. import { ModelsDev } from "../provider/models"
  20. import { Ripgrep } from "../file/ripgrep"
  21. import { Config } from "../config/config"
  22. import { File } from "../file"
  23. import { LSP } from "../lsp"
  24. import { Format } from "../format"
  25. import { MessageV2 } from "../session/message-v2"
  26. import { TuiRoute } from "./tui"
  27. import { Permission } from "../permission"
  28. import { Instance } from "../project/instance"
  29. import { Agent } from "../agent/agent"
  30. import { Auth } from "../auth"
  31. import { Command } from "../command"
  32. import { Global } from "../global"
  33. import { ProjectRoute } from "./project"
  34. import { ToolRegistry } from "../tool/registry"
  35. import { zodToJsonSchema } from "zod-to-json-schema"
  36. import { SessionLock } from "../session/lock"
  37. import { SessionPrompt } from "../session/prompt"
  38. import { SessionCompaction } from "../session/compaction"
  39. import { SessionRevert } from "../session/revert"
  40. import { lazy } from "../util/lazy"
  41. import { Todo } from "../session/todo"
  42. import { InstanceBootstrap } from "../project/bootstrap"
  43. import { MCP } from "../mcp"
  44. import { Storage } from "../storage/storage"
  45. import type { ContentfulStatusCode } from "hono/utils/http-status"
  46. import { TuiEvent } from "@/cli/cmd/tui/event"
  47. import { Snapshot } from "@/snapshot"
  48. import { SessionSummary } from "@/session/summary"
  49. const ERRORS = {
  50. 400: {
  51. description: "Bad request",
  52. content: {
  53. "application/json": {
  54. schema: resolver(
  55. z
  56. .object({
  57. data: z.any().nullable(),
  58. errors: z.array(z.record(z.string(), z.any())),
  59. success: z.literal(false),
  60. })
  61. .meta({
  62. ref: "BadRequestError",
  63. }),
  64. ),
  65. },
  66. },
  67. },
  68. 404: {
  69. description: "Not found",
  70. content: {
  71. "application/json": {
  72. schema: resolver(Storage.NotFoundError.Schema),
  73. },
  74. },
  75. },
  76. } as const
  77. function errors(...codes: number[]) {
  78. return Object.fromEntries(codes.map((code) => [code, ERRORS[code as keyof typeof ERRORS]]))
  79. }
  80. export namespace Server {
  81. const log = Log.create({ service: "server" })
  82. export const Event = {
  83. Connected: Bus.event("server.connected", z.object({})),
  84. }
  85. const app = new Hono()
  86. export const App = lazy(() =>
  87. app
  88. .onError((err, c) => {
  89. log.error("failed", {
  90. error: err,
  91. })
  92. if (err instanceof NamedError) {
  93. let status: ContentfulStatusCode
  94. if (err instanceof Storage.NotFoundError) status = 404
  95. else if (err instanceof Provider.ModelNotFoundError) status = 400
  96. else status = 500
  97. return c.json(err.toObject(), { status })
  98. }
  99. const message = err instanceof Error && err.stack ? err.stack : err.toString()
  100. return c.json(new NamedError.Unknown({ message }).toObject(), {
  101. status: 500,
  102. })
  103. })
  104. .use(async (c, next) => {
  105. const skipLogging = c.req.path === "/log"
  106. if (!skipLogging) {
  107. log.info("request", {
  108. method: c.req.method,
  109. path: c.req.path,
  110. })
  111. }
  112. const start = Date.now()
  113. await next()
  114. if (!skipLogging) {
  115. log.info("response", {
  116. duration: Date.now() - start,
  117. })
  118. }
  119. })
  120. .use(async (c, next) => {
  121. const directory = c.req.query("directory") ?? process.cwd()
  122. return Instance.provide({
  123. directory,
  124. init: InstanceBootstrap,
  125. async fn() {
  126. return next()
  127. },
  128. })
  129. })
  130. .use(cors())
  131. .get(
  132. "/doc",
  133. openAPIRouteHandler(app, {
  134. documentation: {
  135. info: {
  136. title: "opencode",
  137. version: "0.0.3",
  138. description: "opencode api",
  139. },
  140. openapi: "3.1.1",
  141. },
  142. }),
  143. )
  144. .use(validator("query", z.object({ directory: z.string().optional() })))
  145. .route("/project", ProjectRoute)
  146. .get(
  147. "/config",
  148. describeRoute({
  149. description: "Get config info",
  150. operationId: "config.get",
  151. responses: {
  152. 200: {
  153. description: "Get config info",
  154. content: {
  155. "application/json": {
  156. schema: resolver(Config.Info),
  157. },
  158. },
  159. },
  160. },
  161. }),
  162. async (c) => {
  163. return c.json(await Config.get())
  164. },
  165. )
  166. .patch(
  167. "/config",
  168. describeRoute({
  169. description: "Update config",
  170. operationId: "config.update",
  171. responses: {
  172. 200: {
  173. description: "Successfully updated config",
  174. content: {
  175. "application/json": {
  176. schema: resolver(Config.Info),
  177. },
  178. },
  179. },
  180. ...errors(400),
  181. },
  182. }),
  183. validator("json", Config.Info),
  184. async (c) => {
  185. const config = c.req.valid("json")
  186. await Config.update(config)
  187. return c.json(config)
  188. },
  189. )
  190. .get(
  191. "/experimental/tool/ids",
  192. describeRoute({
  193. description: "List all tool IDs (including built-in and dynamically registered)",
  194. operationId: "tool.ids",
  195. responses: {
  196. 200: {
  197. description: "Tool IDs",
  198. content: {
  199. "application/json": {
  200. schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })),
  201. },
  202. },
  203. },
  204. ...errors(400),
  205. },
  206. }),
  207. async (c) => {
  208. return c.json(await ToolRegistry.ids())
  209. },
  210. )
  211. .get(
  212. "/experimental/tool",
  213. describeRoute({
  214. description: "List tools with JSON schema parameters for a provider/model",
  215. operationId: "tool.list",
  216. responses: {
  217. 200: {
  218. description: "Tools",
  219. content: {
  220. "application/json": {
  221. schema: resolver(
  222. z
  223. .array(
  224. z
  225. .object({
  226. id: z.string(),
  227. description: z.string(),
  228. parameters: z.any(),
  229. })
  230. .meta({ ref: "ToolListItem" }),
  231. )
  232. .meta({ ref: "ToolList" }),
  233. ),
  234. },
  235. },
  236. },
  237. ...errors(400),
  238. },
  239. }),
  240. validator(
  241. "query",
  242. z.object({
  243. provider: z.string(),
  244. model: z.string(),
  245. }),
  246. ),
  247. async (c) => {
  248. const { provider, model } = c.req.valid("query")
  249. const tools = await ToolRegistry.tools(provider, model)
  250. return c.json(
  251. tools.map((t) => ({
  252. id: t.id,
  253. description: t.description,
  254. // Handle both Zod schemas and plain JSON schemas
  255. parameters: (t.parameters as any)?._def
  256. ? zodToJsonSchema(t.parameters as any)
  257. : t.parameters,
  258. })),
  259. )
  260. },
  261. )
  262. .get(
  263. "/path",
  264. describeRoute({
  265. description: "Get the current path",
  266. operationId: "path.get",
  267. responses: {
  268. 200: {
  269. description: "Path",
  270. content: {
  271. "application/json": {
  272. schema: resolver(
  273. z
  274. .object({
  275. state: z.string(),
  276. config: z.string(),
  277. worktree: z.string(),
  278. directory: z.string(),
  279. })
  280. .meta({
  281. ref: "Path",
  282. }),
  283. ),
  284. },
  285. },
  286. },
  287. },
  288. }),
  289. async (c) => {
  290. return c.json({
  291. state: Global.Path.state,
  292. config: Global.Path.config,
  293. worktree: Instance.worktree,
  294. directory: Instance.directory,
  295. })
  296. },
  297. )
  298. .get(
  299. "/session",
  300. describeRoute({
  301. description: "List all sessions",
  302. operationId: "session.list",
  303. responses: {
  304. 200: {
  305. description: "List of sessions",
  306. content: {
  307. "application/json": {
  308. schema: resolver(Session.Info.array()),
  309. },
  310. },
  311. },
  312. },
  313. }),
  314. async (c) => {
  315. const sessions = await Array.fromAsync(Session.list())
  316. sessions.sort((a, b) => b.time.updated - a.time.updated)
  317. return c.json(sessions)
  318. },
  319. )
  320. .get(
  321. "/session/:id",
  322. describeRoute({
  323. description: "Get session",
  324. operationId: "session.get",
  325. responses: {
  326. 200: {
  327. description: "Get session",
  328. content: {
  329. "application/json": {
  330. schema: resolver(Session.Info),
  331. },
  332. },
  333. },
  334. ...errors(400, 404),
  335. },
  336. }),
  337. validator(
  338. "param",
  339. z.object({
  340. id: Session.get.schema,
  341. }),
  342. ),
  343. async (c) => {
  344. const sessionID = c.req.valid("param").id
  345. const session = await Session.get(sessionID)
  346. return c.json(session)
  347. },
  348. )
  349. .get(
  350. "/session/:id/children",
  351. describeRoute({
  352. description: "Get a session's children",
  353. operationId: "session.children",
  354. responses: {
  355. 200: {
  356. description: "List of children",
  357. content: {
  358. "application/json": {
  359. schema: resolver(Session.Info.array()),
  360. },
  361. },
  362. },
  363. ...errors(400, 404),
  364. },
  365. }),
  366. validator(
  367. "param",
  368. z.object({
  369. id: Session.children.schema,
  370. }),
  371. ),
  372. async (c) => {
  373. const sessionID = c.req.valid("param").id
  374. const session = await Session.children(sessionID)
  375. return c.json(session)
  376. },
  377. )
  378. .get(
  379. "/session/:id/todo",
  380. describeRoute({
  381. description: "Get the todo list for a session",
  382. operationId: "session.todo",
  383. responses: {
  384. 200: {
  385. description: "Todo list",
  386. content: {
  387. "application/json": {
  388. schema: resolver(Todo.Info.array()),
  389. },
  390. },
  391. },
  392. ...errors(400, 404),
  393. },
  394. }),
  395. validator(
  396. "param",
  397. z.object({
  398. id: z.string().meta({ description: "Session ID" }),
  399. }),
  400. ),
  401. async (c) => {
  402. const sessionID = c.req.valid("param").id
  403. const todos = await Todo.get(sessionID)
  404. return c.json(todos)
  405. },
  406. )
  407. .post(
  408. "/session",
  409. describeRoute({
  410. description: "Create a new session",
  411. operationId: "session.create",
  412. responses: {
  413. ...errors(400),
  414. 200: {
  415. description: "Successfully created session",
  416. content: {
  417. "application/json": {
  418. schema: resolver(Session.Info),
  419. },
  420. },
  421. },
  422. },
  423. }),
  424. validator("json", Session.create.schema.optional()),
  425. async (c) => {
  426. const body = c.req.valid("json") ?? {}
  427. const session = await Session.create(body)
  428. return c.json(session)
  429. },
  430. )
  431. .delete(
  432. "/session/:id",
  433. describeRoute({
  434. description: "Delete a session and all its data",
  435. operationId: "session.delete",
  436. responses: {
  437. 200: {
  438. description: "Successfully deleted session",
  439. content: {
  440. "application/json": {
  441. schema: resolver(z.boolean()),
  442. },
  443. },
  444. },
  445. ...errors(400, 404),
  446. },
  447. }),
  448. validator(
  449. "param",
  450. z.object({
  451. id: Session.remove.schema,
  452. }),
  453. ),
  454. async (c) => {
  455. const sessionID = c.req.valid("param").id
  456. await Session.remove(sessionID)
  457. await Bus.publish(TuiEvent.CommandExecute, {
  458. command: "session.list",
  459. })
  460. return c.json(true)
  461. },
  462. )
  463. .patch(
  464. "/session/:id",
  465. describeRoute({
  466. description: "Update session properties",
  467. operationId: "session.update",
  468. responses: {
  469. 200: {
  470. description: "Successfully updated session",
  471. content: {
  472. "application/json": {
  473. schema: resolver(Session.Info),
  474. },
  475. },
  476. },
  477. ...errors(400, 404),
  478. },
  479. }),
  480. validator(
  481. "param",
  482. z.object({
  483. id: z.string(),
  484. }),
  485. ),
  486. validator(
  487. "json",
  488. z.object({
  489. title: z.string().optional(),
  490. }),
  491. ),
  492. async (c) => {
  493. const sessionID = c.req.valid("param").id
  494. const updates = c.req.valid("json")
  495. const updatedSession = await Session.update(sessionID, (session) => {
  496. if (updates.title !== undefined) {
  497. session.title = updates.title
  498. }
  499. })
  500. return c.json(updatedSession)
  501. },
  502. )
  503. .post(
  504. "/session/:id/init",
  505. describeRoute({
  506. description: "Analyze the app and create an AGENTS.md file",
  507. operationId: "session.init",
  508. responses: {
  509. 200: {
  510. description: "200",
  511. content: {
  512. "application/json": {
  513. schema: resolver(z.boolean()),
  514. },
  515. },
  516. },
  517. ...errors(400, 404),
  518. },
  519. }),
  520. validator(
  521. "param",
  522. z.object({
  523. id: z.string().meta({ description: "Session ID" }),
  524. }),
  525. ),
  526. validator("json", Session.initialize.schema.omit({ sessionID: true })),
  527. async (c) => {
  528. const sessionID = c.req.valid("param").id
  529. const body = c.req.valid("json")
  530. await Session.initialize({ ...body, sessionID })
  531. return c.json(true)
  532. },
  533. )
  534. .post(
  535. "/session/:id/fork",
  536. describeRoute({
  537. description: "Fork an existing session at a specific message",
  538. operationId: "session.fork",
  539. responses: {
  540. 200: {
  541. description: "200",
  542. content: {
  543. "application/json": {
  544. schema: resolver(Session.Info),
  545. },
  546. },
  547. },
  548. },
  549. }),
  550. validator(
  551. "param",
  552. z.object({
  553. id: Session.fork.schema.shape.sessionID,
  554. }),
  555. ),
  556. validator("json", Session.fork.schema.omit({ sessionID: true })),
  557. async (c) => {
  558. const sessionID = c.req.valid("param").id
  559. const body = c.req.valid("json")
  560. const result = await Session.fork({ ...body, sessionID })
  561. return c.json(result)
  562. },
  563. )
  564. .post(
  565. "/session/:id/abort",
  566. describeRoute({
  567. description: "Abort a session",
  568. operationId: "session.abort",
  569. responses: {
  570. 200: {
  571. description: "Aborted session",
  572. content: {
  573. "application/json": {
  574. schema: resolver(z.boolean()),
  575. },
  576. },
  577. },
  578. ...errors(400, 404),
  579. },
  580. }),
  581. validator(
  582. "param",
  583. z.object({
  584. id: z.string(),
  585. }),
  586. ),
  587. async (c) => {
  588. return c.json(SessionLock.abort(c.req.valid("param").id))
  589. },
  590. )
  591. .post(
  592. "/session/:id/share",
  593. describeRoute({
  594. description: "Share a session",
  595. operationId: "session.share",
  596. responses: {
  597. 200: {
  598. description: "Successfully shared session",
  599. content: {
  600. "application/json": {
  601. schema: resolver(Session.Info),
  602. },
  603. },
  604. },
  605. ...errors(400, 404),
  606. },
  607. }),
  608. validator(
  609. "param",
  610. z.object({
  611. id: z.string(),
  612. }),
  613. ),
  614. async (c) => {
  615. const id = c.req.valid("param").id
  616. await Session.share(id)
  617. const session = await Session.get(id)
  618. return c.json(session)
  619. },
  620. )
  621. .get(
  622. "/session/:id/diff",
  623. describeRoute({
  624. description: "Get the diff that resulted from this user message",
  625. operationId: "session.diff",
  626. responses: {
  627. 200: {
  628. description: "Successfully retrieved diff",
  629. content: {
  630. "application/json": {
  631. schema: resolver(Snapshot.FileDiff.array()),
  632. },
  633. },
  634. },
  635. },
  636. }),
  637. validator(
  638. "param",
  639. z.object({
  640. id: SessionSummary.diff.schema.shape.sessionID,
  641. }),
  642. ),
  643. validator(
  644. "query",
  645. z.object({
  646. messageID: SessionSummary.diff.schema.shape.messageID,
  647. }),
  648. ),
  649. async (c) => {
  650. const query = c.req.valid("query")
  651. const params = c.req.valid("param")
  652. const result = await SessionSummary.diff({
  653. sessionID: params.id,
  654. messageID: query.messageID,
  655. })
  656. return c.json(result)
  657. },
  658. )
  659. .delete(
  660. "/session/:id/share",
  661. describeRoute({
  662. description: "Unshare the session",
  663. operationId: "session.unshare",
  664. responses: {
  665. 200: {
  666. description: "Successfully unshared session",
  667. content: {
  668. "application/json": {
  669. schema: resolver(Session.Info),
  670. },
  671. },
  672. },
  673. ...errors(400, 404),
  674. },
  675. }),
  676. validator(
  677. "param",
  678. z.object({
  679. id: Session.unshare.schema,
  680. }),
  681. ),
  682. async (c) => {
  683. const id = c.req.valid("param").id
  684. await Session.unshare(id)
  685. const session = await Session.get(id)
  686. return c.json(session)
  687. },
  688. )
  689. .post(
  690. "/session/:id/summarize",
  691. describeRoute({
  692. description: "Summarize the session",
  693. operationId: "session.summarize",
  694. responses: {
  695. 200: {
  696. description: "Summarized 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. id: z.string().meta({ description: "Session ID" }),
  710. }),
  711. ),
  712. validator(
  713. "json",
  714. z.object({
  715. providerID: z.string(),
  716. modelID: z.string(),
  717. }),
  718. ),
  719. async (c) => {
  720. const id = c.req.valid("param").id
  721. const body = c.req.valid("json")
  722. await SessionCompaction.run({ ...body, sessionID: id })
  723. return c.json(true)
  724. },
  725. )
  726. .get(
  727. "/session/:id/message",
  728. describeRoute({
  729. description: "List messages for a session",
  730. operationId: "session.messages",
  731. responses: {
  732. 200: {
  733. description: "List of messages",
  734. content: {
  735. "application/json": {
  736. schema: resolver(MessageV2.WithParts.array()),
  737. },
  738. },
  739. },
  740. ...errors(400, 404),
  741. },
  742. }),
  743. validator(
  744. "param",
  745. z.object({
  746. id: z.string().meta({ description: "Session ID" }),
  747. }),
  748. ),
  749. validator(
  750. "query",
  751. z.object({
  752. limit: z.coerce.number().optional(),
  753. }),
  754. ),
  755. async (c) => {
  756. const query = c.req.valid("query")
  757. const messages = await Session.messages({
  758. sessionID: c.req.valid("param").id,
  759. limit: query.limit,
  760. })
  761. return c.json(messages)
  762. },
  763. )
  764. .get(
  765. "/session/:id/diff",
  766. describeRoute({
  767. description: "Get the diff for this session",
  768. operationId: "session.diff",
  769. responses: {
  770. 200: {
  771. description: "List of diffs",
  772. content: {
  773. "application/json": {
  774. schema: resolver(Snapshot.FileDiff.array()),
  775. },
  776. },
  777. },
  778. ...errors(400, 404),
  779. },
  780. }),
  781. validator(
  782. "param",
  783. z.object({
  784. id: z.string().meta({ description: "Session ID" }),
  785. }),
  786. ),
  787. async (c) => {
  788. const diff = await Session.diff(c.req.valid("param").id)
  789. return c.json(diff)
  790. },
  791. )
  792. .get(
  793. "/session/:id/message/:messageID",
  794. describeRoute({
  795. description: "Get a message from a session",
  796. operationId: "session.message",
  797. responses: {
  798. 200: {
  799. description: "Message",
  800. content: {
  801. "application/json": {
  802. schema: resolver(
  803. z.object({
  804. info: MessageV2.Info,
  805. parts: MessageV2.Part.array(),
  806. }),
  807. ),
  808. },
  809. },
  810. },
  811. ...errors(400, 404),
  812. },
  813. }),
  814. validator(
  815. "param",
  816. z.object({
  817. id: z.string().meta({ description: "Session ID" }),
  818. messageID: z.string().meta({ description: "Message ID" }),
  819. }),
  820. ),
  821. async (c) => {
  822. const params = c.req.valid("param")
  823. const message = await MessageV2.get({
  824. sessionID: params.id,
  825. messageID: params.messageID,
  826. })
  827. return c.json(message)
  828. },
  829. )
  830. .post(
  831. "/session/:id/message",
  832. describeRoute({
  833. description: "Create and send a new message to a session",
  834. operationId: "session.prompt",
  835. responses: {
  836. 200: {
  837. description: "Created message",
  838. content: {
  839. "application/json": {
  840. schema: resolver(
  841. z.object({
  842. info: MessageV2.Assistant,
  843. parts: MessageV2.Part.array(),
  844. }),
  845. ),
  846. },
  847. },
  848. },
  849. ...errors(400, 404),
  850. },
  851. }),
  852. validator(
  853. "param",
  854. z.object({
  855. id: z.string().meta({ description: "Session ID" }),
  856. }),
  857. ),
  858. validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
  859. async (c) => {
  860. c.status(200)
  861. c.header("Content-Type", "application/json")
  862. return stream(c, async (stream) => {
  863. const sessionID = c.req.valid("param").id
  864. const body = c.req.valid("json")
  865. const msg = await SessionPrompt.prompt({ ...body, sessionID })
  866. stream.write(JSON.stringify(msg))
  867. })
  868. },
  869. )
  870. .post(
  871. "/session/:id/command",
  872. describeRoute({
  873. description: "Send a new command to a session",
  874. operationId: "session.command",
  875. responses: {
  876. 200: {
  877. description: "Created message",
  878. content: {
  879. "application/json": {
  880. schema: resolver(
  881. z.object({
  882. info: MessageV2.Assistant,
  883. parts: MessageV2.Part.array(),
  884. }),
  885. ),
  886. },
  887. },
  888. },
  889. ...errors(400, 404),
  890. },
  891. }),
  892. validator(
  893. "param",
  894. z.object({
  895. id: z.string().meta({ description: "Session ID" }),
  896. }),
  897. ),
  898. validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
  899. async (c) => {
  900. const sessionID = c.req.valid("param").id
  901. const body = c.req.valid("json")
  902. const msg = await SessionPrompt.command({ ...body, sessionID })
  903. return c.json(msg)
  904. },
  905. )
  906. .post(
  907. "/session/:id/shell",
  908. describeRoute({
  909. description: "Run a shell command",
  910. operationId: "session.shell",
  911. responses: {
  912. 200: {
  913. description: "Created message",
  914. content: {
  915. "application/json": {
  916. schema: resolver(MessageV2.Assistant),
  917. },
  918. },
  919. },
  920. ...errors(400, 404),
  921. },
  922. }),
  923. validator(
  924. "param",
  925. z.object({
  926. id: z.string().meta({ description: "Session ID" }),
  927. }),
  928. ),
  929. validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
  930. async (c) => {
  931. const sessionID = c.req.valid("param").id
  932. const body = c.req.valid("json")
  933. const msg = await SessionPrompt.shell({ ...body, sessionID })
  934. return c.json(msg)
  935. },
  936. )
  937. .post(
  938. "/session/:id/revert",
  939. describeRoute({
  940. description: "Revert a message",
  941. operationId: "session.revert",
  942. responses: {
  943. 200: {
  944. description: "Updated session",
  945. content: {
  946. "application/json": {
  947. schema: resolver(Session.Info),
  948. },
  949. },
  950. },
  951. ...errors(400, 404),
  952. },
  953. }),
  954. validator(
  955. "param",
  956. z.object({
  957. id: z.string(),
  958. }),
  959. ),
  960. validator("json", SessionRevert.RevertInput.omit({ sessionID: true })),
  961. async (c) => {
  962. const id = c.req.valid("param").id
  963. log.info("revert", c.req.valid("json"))
  964. const session = await SessionRevert.revert({
  965. sessionID: id,
  966. ...c.req.valid("json"),
  967. })
  968. return c.json(session)
  969. },
  970. )
  971. .post(
  972. "/session/:id/unrevert",
  973. describeRoute({
  974. description: "Restore all reverted messages",
  975. operationId: "session.unrevert",
  976. responses: {
  977. 200: {
  978. description: "Updated session",
  979. content: {
  980. "application/json": {
  981. schema: resolver(Session.Info),
  982. },
  983. },
  984. },
  985. ...errors(400, 404),
  986. },
  987. }),
  988. validator(
  989. "param",
  990. z.object({
  991. id: z.string(),
  992. }),
  993. ),
  994. async (c) => {
  995. const id = c.req.valid("param").id
  996. const session = await SessionRevert.unrevert({ sessionID: id })
  997. return c.json(session)
  998. },
  999. )
  1000. .post(
  1001. "/session/:id/permissions/:permissionID",
  1002. describeRoute({
  1003. description: "Respond to a permission request",
  1004. responses: {
  1005. 200: {
  1006. description: "Permission processed successfully",
  1007. content: {
  1008. "application/json": {
  1009. schema: resolver(z.boolean()),
  1010. },
  1011. },
  1012. },
  1013. ...errors(400, 404),
  1014. },
  1015. }),
  1016. validator(
  1017. "param",
  1018. z.object({
  1019. id: z.string(),
  1020. permissionID: z.string(),
  1021. }),
  1022. ),
  1023. validator("json", z.object({ response: Permission.Response })),
  1024. async (c) => {
  1025. const params = c.req.valid("param")
  1026. const id = params.id
  1027. const permissionID = params.permissionID
  1028. Permission.respond({
  1029. sessionID: id,
  1030. permissionID,
  1031. response: c.req.valid("json").response,
  1032. })
  1033. return c.json(true)
  1034. },
  1035. )
  1036. .get(
  1037. "/command",
  1038. describeRoute({
  1039. description: "List all commands",
  1040. operationId: "command.list",
  1041. responses: {
  1042. 200: {
  1043. description: "List of commands",
  1044. content: {
  1045. "application/json": {
  1046. schema: resolver(Command.Info.array()),
  1047. },
  1048. },
  1049. },
  1050. },
  1051. }),
  1052. async (c) => {
  1053. const commands = await Command.list()
  1054. return c.json(commands)
  1055. },
  1056. )
  1057. .get(
  1058. "/config/providers",
  1059. describeRoute({
  1060. description: "List all providers",
  1061. operationId: "config.providers",
  1062. responses: {
  1063. 200: {
  1064. description: "List of providers",
  1065. content: {
  1066. "application/json": {
  1067. schema: resolver(
  1068. z.object({
  1069. providers: ModelsDev.Provider.array(),
  1070. default: z.record(z.string(), z.string()),
  1071. }),
  1072. ),
  1073. },
  1074. },
  1075. },
  1076. },
  1077. }),
  1078. async (c) => {
  1079. const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info))
  1080. return c.json({
  1081. providers: Object.values(providers),
  1082. default: mapValues(
  1083. providers,
  1084. (item) => Provider.sort(Object.values(item.models))[0].id,
  1085. ),
  1086. })
  1087. },
  1088. )
  1089. .get(
  1090. "/find",
  1091. describeRoute({
  1092. description: "Find text in files",
  1093. operationId: "find.text",
  1094. responses: {
  1095. 200: {
  1096. description: "Matches",
  1097. content: {
  1098. "application/json": {
  1099. schema: resolver(Ripgrep.Match.shape.data.array()),
  1100. },
  1101. },
  1102. },
  1103. },
  1104. }),
  1105. validator(
  1106. "query",
  1107. z.object({
  1108. pattern: z.string(),
  1109. }),
  1110. ),
  1111. async (c) => {
  1112. const pattern = c.req.valid("query").pattern
  1113. const result = await Ripgrep.search({
  1114. cwd: Instance.directory,
  1115. pattern,
  1116. limit: 10,
  1117. })
  1118. return c.json(result)
  1119. },
  1120. )
  1121. .get(
  1122. "/find/file",
  1123. describeRoute({
  1124. description: "Find files",
  1125. operationId: "find.files",
  1126. responses: {
  1127. 200: {
  1128. description: "File paths",
  1129. content: {
  1130. "application/json": {
  1131. schema: resolver(z.string().array()),
  1132. },
  1133. },
  1134. },
  1135. },
  1136. }),
  1137. validator(
  1138. "query",
  1139. z.object({
  1140. query: z.string(),
  1141. dirs: z.union([z.literal("true"), z.literal("false")]).optional(),
  1142. }),
  1143. ),
  1144. async (c) => {
  1145. const query = c.req.valid("query").query
  1146. const dirs = c.req.valid("query").dirs
  1147. const results = await File.search({
  1148. query,
  1149. limit: 10,
  1150. dirs: dirs !== "false",
  1151. })
  1152. return c.json(results)
  1153. },
  1154. )
  1155. .get(
  1156. "/find/symbol",
  1157. describeRoute({
  1158. description: "Find workspace symbols",
  1159. operationId: "find.symbols",
  1160. responses: {
  1161. 200: {
  1162. description: "Symbols",
  1163. content: {
  1164. "application/json": {
  1165. schema: resolver(LSP.Symbol.array()),
  1166. },
  1167. },
  1168. },
  1169. },
  1170. }),
  1171. validator(
  1172. "query",
  1173. z.object({
  1174. query: z.string(),
  1175. }),
  1176. ),
  1177. async (c) => {
  1178. /*
  1179. const query = c.req.valid("query").query
  1180. const result = await LSP.workspaceSymbol(query)
  1181. return c.json(result)
  1182. */
  1183. return c.json([])
  1184. },
  1185. )
  1186. .get(
  1187. "/file",
  1188. describeRoute({
  1189. description: "List files and directories",
  1190. operationId: "file.list",
  1191. responses: {
  1192. 200: {
  1193. description: "Files and directories",
  1194. content: {
  1195. "application/json": {
  1196. schema: resolver(File.Node.array()),
  1197. },
  1198. },
  1199. },
  1200. },
  1201. }),
  1202. validator(
  1203. "query",
  1204. z.object({
  1205. path: z.string(),
  1206. }),
  1207. ),
  1208. async (c) => {
  1209. const path = c.req.valid("query").path
  1210. const content = await File.list(path)
  1211. return c.json(content)
  1212. },
  1213. )
  1214. .get(
  1215. "/file/content",
  1216. describeRoute({
  1217. description: "Read a file",
  1218. operationId: "file.read",
  1219. responses: {
  1220. 200: {
  1221. description: "File content",
  1222. content: {
  1223. "application/json": {
  1224. schema: resolver(File.Content),
  1225. },
  1226. },
  1227. },
  1228. },
  1229. }),
  1230. validator(
  1231. "query",
  1232. z.object({
  1233. path: z.string(),
  1234. }),
  1235. ),
  1236. async (c) => {
  1237. const path = c.req.valid("query").path
  1238. const content = await File.read(path)
  1239. return c.json(content)
  1240. },
  1241. )
  1242. .get(
  1243. "/file/status",
  1244. describeRoute({
  1245. description: "Get file status",
  1246. operationId: "file.status",
  1247. responses: {
  1248. 200: {
  1249. description: "File status",
  1250. content: {
  1251. "application/json": {
  1252. schema: resolver(File.Info.array()),
  1253. },
  1254. },
  1255. },
  1256. },
  1257. }),
  1258. async (c) => {
  1259. const content = await File.status()
  1260. return c.json(content)
  1261. },
  1262. )
  1263. .post(
  1264. "/log",
  1265. describeRoute({
  1266. description: "Write a log entry to the server logs",
  1267. operationId: "app.log",
  1268. responses: {
  1269. 200: {
  1270. description: "Log entry written successfully",
  1271. content: {
  1272. "application/json": {
  1273. schema: resolver(z.boolean()),
  1274. },
  1275. },
  1276. },
  1277. ...errors(400),
  1278. },
  1279. }),
  1280. validator(
  1281. "json",
  1282. z.object({
  1283. service: z.string().meta({ description: "Service name for the log entry" }),
  1284. level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
  1285. message: z.string().meta({ description: "Log message" }),
  1286. extra: z
  1287. .record(z.string(), z.any())
  1288. .optional()
  1289. .meta({ description: "Additional metadata for the log entry" }),
  1290. }),
  1291. ),
  1292. async (c) => {
  1293. const { service, level, message, extra } = c.req.valid("json")
  1294. const logger = Log.create({ service })
  1295. switch (level) {
  1296. case "debug":
  1297. logger.debug(message, extra)
  1298. break
  1299. case "info":
  1300. logger.info(message, extra)
  1301. break
  1302. case "error":
  1303. logger.error(message, extra)
  1304. break
  1305. case "warn":
  1306. logger.warn(message, extra)
  1307. break
  1308. }
  1309. return c.json(true)
  1310. },
  1311. )
  1312. .get(
  1313. "/agent",
  1314. describeRoute({
  1315. description: "List all agents",
  1316. operationId: "app.agents",
  1317. responses: {
  1318. 200: {
  1319. description: "List of agents",
  1320. content: {
  1321. "application/json": {
  1322. schema: resolver(Agent.Info.array()),
  1323. },
  1324. },
  1325. },
  1326. },
  1327. }),
  1328. async (c) => {
  1329. const modes = await Agent.list()
  1330. return c.json(modes)
  1331. },
  1332. )
  1333. .get(
  1334. "/mcp",
  1335. describeRoute({
  1336. description: "Get MCP server status",
  1337. operationId: "mcp.status",
  1338. responses: {
  1339. 200: {
  1340. description: "MCP server status",
  1341. content: {
  1342. "application/json": {
  1343. schema: resolver(z.record(z.string(), MCP.Status)),
  1344. },
  1345. },
  1346. },
  1347. },
  1348. }),
  1349. async (c) => {
  1350. return c.json(await MCP.status())
  1351. },
  1352. )
  1353. .post(
  1354. "/mcp",
  1355. describeRoute({
  1356. description: "Add MCP server dynamically",
  1357. operationId: "mcp.add",
  1358. responses: {
  1359. 200: {
  1360. description: "MCP server added successfully",
  1361. content: {
  1362. "application/json": {
  1363. schema: resolver(z.record(z.string(), MCP.Status)),
  1364. },
  1365. },
  1366. },
  1367. ...errors(400),
  1368. },
  1369. }),
  1370. validator(
  1371. "json",
  1372. z.object({
  1373. name: z.string(),
  1374. config: Config.Mcp,
  1375. }),
  1376. ),
  1377. async (c) => {
  1378. const { name, config } = c.req.valid("json")
  1379. const result = await MCP.add(name, config)
  1380. return c.json(result.status)
  1381. },
  1382. )
  1383. .get(
  1384. "/lsp",
  1385. describeRoute({
  1386. description: "Get LSP server status",
  1387. operationId: "lsp.status",
  1388. responses: {
  1389. 200: {
  1390. description: "LSP server status",
  1391. content: {
  1392. "application/json": {
  1393. schema: resolver(LSP.Status.array()),
  1394. },
  1395. },
  1396. },
  1397. },
  1398. }),
  1399. async (c) => {
  1400. return c.json(await LSP.status())
  1401. },
  1402. )
  1403. .get(
  1404. "/formatter",
  1405. describeRoute({
  1406. description: "Get formatter status",
  1407. operationId: "formatter.status",
  1408. responses: {
  1409. 200: {
  1410. description: "Formatter status",
  1411. content: {
  1412. "application/json": {
  1413. schema: resolver(Format.Status.array()),
  1414. },
  1415. },
  1416. },
  1417. },
  1418. }),
  1419. async (c) => {
  1420. return c.json(await Format.status())
  1421. },
  1422. )
  1423. .post(
  1424. "/tui/append-prompt",
  1425. describeRoute({
  1426. description: "Append prompt to the TUI",
  1427. operationId: "tui.appendPrompt",
  1428. responses: {
  1429. 200: {
  1430. description: "Prompt processed successfully",
  1431. content: {
  1432. "application/json": {
  1433. schema: resolver(z.boolean()),
  1434. },
  1435. },
  1436. },
  1437. ...errors(400),
  1438. },
  1439. }),
  1440. validator("json", TuiEvent.PromptAppend.properties),
  1441. async (c) => {
  1442. await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
  1443. return c.json(true)
  1444. },
  1445. )
  1446. .post(
  1447. "/tui/open-help",
  1448. describeRoute({
  1449. description: "Open the help dialog",
  1450. operationId: "tui.openHelp",
  1451. responses: {
  1452. 200: {
  1453. description: "Help dialog opened successfully",
  1454. content: {
  1455. "application/json": {
  1456. schema: resolver(z.boolean()),
  1457. },
  1458. },
  1459. },
  1460. },
  1461. }),
  1462. async (c) => {
  1463. // TODO: open dialog
  1464. return c.json(true)
  1465. },
  1466. )
  1467. .post(
  1468. "/tui/open-sessions",
  1469. describeRoute({
  1470. description: "Open the session dialog",
  1471. operationId: "tui.openSessions",
  1472. responses: {
  1473. 200: {
  1474. description: "Session dialog opened successfully",
  1475. content: {
  1476. "application/json": {
  1477. schema: resolver(z.boolean()),
  1478. },
  1479. },
  1480. },
  1481. },
  1482. }),
  1483. async (c) => {
  1484. await Bus.publish(TuiEvent.CommandExecute, {
  1485. command: "session.list",
  1486. })
  1487. return c.json(true)
  1488. },
  1489. )
  1490. .post(
  1491. "/tui/open-themes",
  1492. describeRoute({
  1493. description: "Open the theme dialog",
  1494. operationId: "tui.openThemes",
  1495. responses: {
  1496. 200: {
  1497. description: "Theme dialog opened successfully",
  1498. content: {
  1499. "application/json": {
  1500. schema: resolver(z.boolean()),
  1501. },
  1502. },
  1503. },
  1504. },
  1505. }),
  1506. async (c) => {
  1507. await Bus.publish(TuiEvent.CommandExecute, {
  1508. command: "session.list",
  1509. })
  1510. return c.json(true)
  1511. },
  1512. )
  1513. .post(
  1514. "/tui/open-models",
  1515. describeRoute({
  1516. description: "Open the model dialog",
  1517. operationId: "tui.openModels",
  1518. responses: {
  1519. 200: {
  1520. description: "Model dialog opened successfully",
  1521. content: {
  1522. "application/json": {
  1523. schema: resolver(z.boolean()),
  1524. },
  1525. },
  1526. },
  1527. },
  1528. }),
  1529. async (c) => {
  1530. await Bus.publish(TuiEvent.CommandExecute, {
  1531. command: "model.list",
  1532. })
  1533. return c.json(true)
  1534. },
  1535. )
  1536. .post(
  1537. "/tui/submit-prompt",
  1538. describeRoute({
  1539. description: "Submit the prompt",
  1540. operationId: "tui.submitPrompt",
  1541. responses: {
  1542. 200: {
  1543. description: "Prompt submitted successfully",
  1544. content: {
  1545. "application/json": {
  1546. schema: resolver(z.boolean()),
  1547. },
  1548. },
  1549. },
  1550. },
  1551. }),
  1552. async (c) => {
  1553. await Bus.publish(TuiEvent.CommandExecute, {
  1554. command: "prompt.submit",
  1555. })
  1556. return c.json(true)
  1557. },
  1558. )
  1559. .post(
  1560. "/tui/clear-prompt",
  1561. describeRoute({
  1562. description: "Clear the prompt",
  1563. operationId: "tui.clearPrompt",
  1564. responses: {
  1565. 200: {
  1566. description: "Prompt cleared successfully",
  1567. content: {
  1568. "application/json": {
  1569. schema: resolver(z.boolean()),
  1570. },
  1571. },
  1572. },
  1573. },
  1574. }),
  1575. async (c) => {
  1576. await Bus.publish(TuiEvent.CommandExecute, {
  1577. command: "prompt.clear",
  1578. })
  1579. return c.json(true)
  1580. },
  1581. )
  1582. .post(
  1583. "/tui/execute-command",
  1584. describeRoute({
  1585. description: "Execute a TUI command (e.g. agent_cycle)",
  1586. operationId: "tui.executeCommand",
  1587. responses: {
  1588. 200: {
  1589. description: "Command executed successfully",
  1590. content: {
  1591. "application/json": {
  1592. schema: resolver(z.boolean()),
  1593. },
  1594. },
  1595. },
  1596. ...errors(400),
  1597. },
  1598. }),
  1599. validator("json", z.object({ command: z.string() })),
  1600. async (c) => {
  1601. const command = c.req.valid("json").command
  1602. await Bus.publish(TuiEvent.CommandExecute, {
  1603. // @ts-expect-error
  1604. command: {
  1605. session_new: "session.new",
  1606. session_share: "session.share",
  1607. session_interrupt: "session.interrupt",
  1608. session_compact: "session.compact",
  1609. messages_page_up: "session.page.up",
  1610. messages_page_down: "session.page.down",
  1611. messages_half_page_up: "session.half.page.up",
  1612. messages_half_page_down: "session.half.page.down",
  1613. messages_first: "session.first",
  1614. messages_last: "session.last",
  1615. agent_cycle: "agent.cycle",
  1616. }[command],
  1617. })
  1618. return c.json(true)
  1619. },
  1620. )
  1621. .post(
  1622. "/tui/show-toast",
  1623. describeRoute({
  1624. description: "Show a toast notification in the TUI",
  1625. operationId: "tui.showToast",
  1626. responses: {
  1627. 200: {
  1628. description: "Toast notification shown successfully",
  1629. content: {
  1630. "application/json": {
  1631. schema: resolver(z.boolean()),
  1632. },
  1633. },
  1634. },
  1635. },
  1636. }),
  1637. validator("json", TuiEvent.ToastShow.properties),
  1638. async (c) => {
  1639. await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"))
  1640. return c.json(true)
  1641. },
  1642. )
  1643. .post(
  1644. "/tui/publish",
  1645. describeRoute({
  1646. description: "Publish a TUI event",
  1647. operationId: "tui.publish",
  1648. responses: {
  1649. 200: {
  1650. description: "Event published successfully",
  1651. content: {
  1652. "application/json": {
  1653. schema: resolver(z.boolean()),
  1654. },
  1655. },
  1656. },
  1657. ...errors(400),
  1658. },
  1659. }),
  1660. validator(
  1661. "json",
  1662. z.union(
  1663. Object.values(TuiEvent).map((def) => {
  1664. return z
  1665. .object({
  1666. type: z.literal(def.type),
  1667. properties: def.properties,
  1668. })
  1669. .meta({
  1670. ref: "Event" + "." + def.type,
  1671. })
  1672. }),
  1673. ),
  1674. ),
  1675. async (c) => {
  1676. const evt = c.req.valid("json")
  1677. await Bus.publish(
  1678. Object.values(TuiEvent).find((def) => def.type === evt.type)!,
  1679. evt.properties,
  1680. )
  1681. return c.json(true)
  1682. },
  1683. )
  1684. .route("/tui/control", TuiRoute)
  1685. .put(
  1686. "/auth/:id",
  1687. describeRoute({
  1688. description: "Set authentication credentials",
  1689. operationId: "auth.set",
  1690. responses: {
  1691. 200: {
  1692. description: "Successfully set authentication credentials",
  1693. content: {
  1694. "application/json": {
  1695. schema: resolver(z.boolean()),
  1696. },
  1697. },
  1698. },
  1699. ...errors(400),
  1700. },
  1701. }),
  1702. validator(
  1703. "param",
  1704. z.object({
  1705. id: z.string(),
  1706. }),
  1707. ),
  1708. validator("json", Auth.Info),
  1709. async (c) => {
  1710. const id = c.req.valid("param").id
  1711. const info = c.req.valid("json")
  1712. await Auth.set(id, info)
  1713. return c.json(true)
  1714. },
  1715. )
  1716. .get(
  1717. "/event",
  1718. describeRoute({
  1719. description: "Get events",
  1720. operationId: "event.subscribe",
  1721. responses: {
  1722. 200: {
  1723. description: "Event stream",
  1724. content: {
  1725. "text/event-stream": {
  1726. schema: resolver(
  1727. Bus.payloads().meta({
  1728. ref: "Event",
  1729. }),
  1730. ),
  1731. },
  1732. },
  1733. },
  1734. },
  1735. }),
  1736. async (c) => {
  1737. log.info("event connected")
  1738. return streamSSE(c, async (stream) => {
  1739. stream.writeSSE({
  1740. data: JSON.stringify({
  1741. type: "server.connected",
  1742. properties: {},
  1743. }),
  1744. })
  1745. const unsub = Bus.subscribeAll(async (event) => {
  1746. await stream.writeSSE({
  1747. data: JSON.stringify(event),
  1748. })
  1749. })
  1750. await new Promise<void>((resolve) => {
  1751. stream.onAbort(() => {
  1752. unsub()
  1753. resolve()
  1754. log.info("event disconnected")
  1755. })
  1756. })
  1757. })
  1758. },
  1759. )
  1760. .all("/*", async (c) => {
  1761. return proxy(`https://desktop.dev.opencode.ai${c.req.path}`, {
  1762. ...c.req,
  1763. headers: {
  1764. host: "desktop.dev.opencode.ai",
  1765. },
  1766. })
  1767. }),
  1768. )
  1769. export async function openapi() {
  1770. const result = await generateSpecs(App(), {
  1771. documentation: {
  1772. info: {
  1773. title: "opencode",
  1774. version: "1.0.0",
  1775. description: "opencode api",
  1776. },
  1777. openapi: "3.1.1",
  1778. },
  1779. })
  1780. return result
  1781. }
  1782. export function listen(opts: { port: number; hostname: string }) {
  1783. const server = Bun.serve({
  1784. port: opts.port,
  1785. hostname: opts.hostname,
  1786. idleTimeout: 0,
  1787. fetch: App().fetch,
  1788. })
  1789. return server
  1790. }
  1791. }