server.ts 43 KB

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