server.ts 42 KB

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