server.ts 56 KB

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