server.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  1. import { Log } from "../util/log"
  2. import { Bus } from "../bus"
  3. import { describeRoute, generateSpecs, openAPISpecs } from "hono-openapi"
  4. import { Hono } from "hono"
  5. import { streamSSE } from "hono/streaming"
  6. import { Session } from "../session"
  7. import { resolver, validator as zValidator } from "hono-openapi/zod"
  8. import { z } from "zod"
  9. import { Provider } from "../provider/provider"
  10. import { App } from "../app/app"
  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 { MessageV2 } from "../session/message-v2"
  19. import { Mode } from "../session/mode"
  20. import { callTui, TuiRoute } from "./tui"
  21. const ERRORS = {
  22. 400: {
  23. description: "Bad request",
  24. content: {
  25. "application/json": {
  26. schema: resolver(
  27. z
  28. .object({
  29. data: z.record(z.string(), z.any()),
  30. })
  31. .openapi({
  32. ref: "Error",
  33. }),
  34. ),
  35. },
  36. },
  37. },
  38. } as const
  39. export namespace Server {
  40. const log = Log.create({ service: "server" })
  41. export type Routes = ReturnType<typeof app>
  42. export const Event = {
  43. Connected: Bus.event("server.connected", z.object({})),
  44. }
  45. function app() {
  46. const app = new Hono()
  47. const result = app
  48. .onError((err, c) => {
  49. if (err instanceof NamedError) {
  50. return c.json(err.toObject(), {
  51. status: 400,
  52. })
  53. }
  54. return c.json(new NamedError.Unknown({ message: err.toString() }).toObject(), {
  55. status: 400,
  56. })
  57. })
  58. .use(async (c, next) => {
  59. const skipLogging = c.req.path === "/log"
  60. if (!skipLogging) {
  61. log.info("request", {
  62. method: c.req.method,
  63. path: c.req.path,
  64. })
  65. }
  66. const start = Date.now()
  67. await next()
  68. if (!skipLogging) {
  69. log.info("response", {
  70. duration: Date.now() - start,
  71. })
  72. }
  73. })
  74. .get(
  75. "/doc",
  76. openAPISpecs(app, {
  77. documentation: {
  78. info: {
  79. title: "opencode",
  80. version: "0.0.3",
  81. description: "opencode api",
  82. },
  83. openapi: "3.0.0",
  84. },
  85. }),
  86. )
  87. .get(
  88. "/event",
  89. describeRoute({
  90. description: "Get events",
  91. responses: {
  92. 200: {
  93. description: "Event stream",
  94. content: {
  95. "application/json": {
  96. schema: resolver(
  97. Bus.payloads().openapi({
  98. ref: "Event",
  99. }),
  100. ),
  101. },
  102. },
  103. },
  104. },
  105. }),
  106. async (c) => {
  107. log.info("event connected")
  108. return streamSSE(c, async (stream) => {
  109. stream.writeSSE({
  110. data: JSON.stringify({
  111. type: "server.connected",
  112. properties: {},
  113. }),
  114. })
  115. const unsub = Bus.subscribeAll(async (event) => {
  116. await stream.writeSSE({
  117. data: JSON.stringify(event),
  118. })
  119. })
  120. await new Promise<void>((resolve) => {
  121. stream.onAbort(() => {
  122. unsub()
  123. resolve()
  124. log.info("event disconnected")
  125. })
  126. })
  127. })
  128. },
  129. )
  130. .get(
  131. "/app",
  132. describeRoute({
  133. description: "Get app info",
  134. responses: {
  135. 200: {
  136. description: "200",
  137. content: {
  138. "application/json": {
  139. schema: resolver(App.Info),
  140. },
  141. },
  142. },
  143. },
  144. }),
  145. async (c) => {
  146. return c.json(App.info())
  147. },
  148. )
  149. .post(
  150. "/app/init",
  151. describeRoute({
  152. description: "Initialize the app",
  153. responses: {
  154. 200: {
  155. description: "Initialize the app",
  156. content: {
  157. "application/json": {
  158. schema: resolver(z.boolean()),
  159. },
  160. },
  161. },
  162. },
  163. }),
  164. async (c) => {
  165. await App.initialize()
  166. return c.json(true)
  167. },
  168. )
  169. .get(
  170. "/config",
  171. describeRoute({
  172. description: "Get config info",
  173. responses: {
  174. 200: {
  175. description: "Get config info",
  176. content: {
  177. "application/json": {
  178. schema: resolver(Config.Info),
  179. },
  180. },
  181. },
  182. },
  183. }),
  184. async (c) => {
  185. return c.json(await Config.get())
  186. },
  187. )
  188. .get(
  189. "/session",
  190. describeRoute({
  191. description: "List all sessions",
  192. responses: {
  193. 200: {
  194. description: "List of sessions",
  195. content: {
  196. "application/json": {
  197. schema: resolver(Session.Info.array()),
  198. },
  199. },
  200. },
  201. },
  202. }),
  203. async (c) => {
  204. const sessions = await Array.fromAsync(Session.list())
  205. sessions.sort((a, b) => b.time.updated - a.time.updated)
  206. return c.json(sessions)
  207. },
  208. )
  209. .post(
  210. "/session",
  211. describeRoute({
  212. description: "Create a new session",
  213. responses: {
  214. ...ERRORS,
  215. 200: {
  216. description: "Successfully created session",
  217. content: {
  218. "application/json": {
  219. schema: resolver(Session.Info),
  220. },
  221. },
  222. },
  223. },
  224. }),
  225. async (c) => {
  226. const session = await Session.create()
  227. return c.json(session)
  228. },
  229. )
  230. .delete(
  231. "/session/:id",
  232. describeRoute({
  233. description: "Delete a session and all its data",
  234. responses: {
  235. 200: {
  236. description: "Successfully deleted session",
  237. content: {
  238. "application/json": {
  239. schema: resolver(z.boolean()),
  240. },
  241. },
  242. },
  243. },
  244. }),
  245. zValidator(
  246. "param",
  247. z.object({
  248. id: z.string(),
  249. }),
  250. ),
  251. async (c) => {
  252. await Session.remove(c.req.valid("param").id)
  253. return c.json(true)
  254. },
  255. )
  256. .post(
  257. "/session/:id/init",
  258. describeRoute({
  259. description: "Analyze the app and create an AGENTS.md file",
  260. responses: {
  261. 200: {
  262. description: "200",
  263. content: {
  264. "application/json": {
  265. schema: resolver(z.boolean()),
  266. },
  267. },
  268. },
  269. },
  270. }),
  271. zValidator(
  272. "param",
  273. z.object({
  274. id: z.string().openapi({ description: "Session ID" }),
  275. }),
  276. ),
  277. zValidator(
  278. "json",
  279. z.object({
  280. messageID: z.string(),
  281. providerID: z.string(),
  282. modelID: z.string(),
  283. }),
  284. ),
  285. async (c) => {
  286. const sessionID = c.req.valid("param").id
  287. const body = c.req.valid("json")
  288. await Session.initialize({ ...body, sessionID })
  289. return c.json(true)
  290. },
  291. )
  292. .post(
  293. "/session/:id/abort",
  294. describeRoute({
  295. description: "Abort a session",
  296. responses: {
  297. 200: {
  298. description: "Aborted session",
  299. content: {
  300. "application/json": {
  301. schema: resolver(z.boolean()),
  302. },
  303. },
  304. },
  305. },
  306. }),
  307. zValidator(
  308. "param",
  309. z.object({
  310. id: z.string(),
  311. }),
  312. ),
  313. async (c) => {
  314. return c.json(Session.abort(c.req.valid("param").id))
  315. },
  316. )
  317. .post(
  318. "/session/:id/share",
  319. describeRoute({
  320. description: "Share a session",
  321. responses: {
  322. 200: {
  323. description: "Successfully shared session",
  324. content: {
  325. "application/json": {
  326. schema: resolver(Session.Info),
  327. },
  328. },
  329. },
  330. },
  331. }),
  332. zValidator(
  333. "param",
  334. z.object({
  335. id: z.string(),
  336. }),
  337. ),
  338. async (c) => {
  339. const id = c.req.valid("param").id
  340. await Session.share(id)
  341. const session = await Session.get(id)
  342. return c.json(session)
  343. },
  344. )
  345. .delete(
  346. "/session/:id/share",
  347. describeRoute({
  348. description: "Unshare the session",
  349. responses: {
  350. 200: {
  351. description: "Successfully unshared session",
  352. content: {
  353. "application/json": {
  354. schema: resolver(Session.Info),
  355. },
  356. },
  357. },
  358. },
  359. }),
  360. zValidator(
  361. "param",
  362. z.object({
  363. id: z.string(),
  364. }),
  365. ),
  366. async (c) => {
  367. const id = c.req.valid("param").id
  368. await Session.unshare(id)
  369. const session = await Session.get(id)
  370. return c.json(session)
  371. },
  372. )
  373. .post(
  374. "/session/:id/summarize",
  375. describeRoute({
  376. description: "Summarize the session",
  377. responses: {
  378. 200: {
  379. description: "Summarized session",
  380. content: {
  381. "application/json": {
  382. schema: resolver(z.boolean()),
  383. },
  384. },
  385. },
  386. },
  387. }),
  388. zValidator(
  389. "param",
  390. z.object({
  391. id: z.string().openapi({ description: "Session ID" }),
  392. }),
  393. ),
  394. zValidator(
  395. "json",
  396. z.object({
  397. providerID: z.string(),
  398. modelID: z.string(),
  399. }),
  400. ),
  401. async (c) => {
  402. const id = c.req.valid("param").id
  403. const body = c.req.valid("json")
  404. await Session.summarize({ ...body, sessionID: id })
  405. return c.json(true)
  406. },
  407. )
  408. .get(
  409. "/session/:id/message",
  410. describeRoute({
  411. description: "List messages for a session",
  412. responses: {
  413. 200: {
  414. description: "List of messages",
  415. content: {
  416. "application/json": {
  417. schema: resolver(
  418. z
  419. .object({
  420. info: MessageV2.Info,
  421. parts: MessageV2.Part.array(),
  422. })
  423. .array(),
  424. ),
  425. },
  426. },
  427. },
  428. },
  429. }),
  430. zValidator(
  431. "param",
  432. z.object({
  433. id: z.string().openapi({ description: "Session ID" }),
  434. }),
  435. ),
  436. async (c) => {
  437. const messages = await Session.messages(c.req.valid("param").id)
  438. return c.json(messages)
  439. },
  440. )
  441. .post(
  442. "/session/:id/message",
  443. describeRoute({
  444. description: "Create and send a new message to a session",
  445. responses: {
  446. 200: {
  447. description: "Created message",
  448. content: {
  449. "application/json": {
  450. schema: resolver(MessageV2.Assistant),
  451. },
  452. },
  453. },
  454. },
  455. }),
  456. zValidator(
  457. "param",
  458. z.object({
  459. id: z.string().openapi({ description: "Session ID" }),
  460. }),
  461. ),
  462. zValidator("json", Session.ChatInput.omit({ sessionID: true })),
  463. async (c) => {
  464. const sessionID = c.req.valid("param").id
  465. const body = c.req.valid("json")
  466. const msg = await Session.chat({ ...body, sessionID })
  467. return c.json(msg)
  468. },
  469. )
  470. .post(
  471. "/session/:id/revert",
  472. describeRoute({
  473. description: "Revert a message",
  474. responses: {
  475. 200: {
  476. description: "Updated session",
  477. content: {
  478. "application/json": {
  479. schema: resolver(Session.Info),
  480. },
  481. },
  482. },
  483. },
  484. }),
  485. zValidator(
  486. "param",
  487. z.object({
  488. id: z.string(),
  489. }),
  490. ),
  491. zValidator("json", Session.RevertInput.omit({ sessionID: true })),
  492. async (c) => {
  493. const id = c.req.valid("param").id
  494. log.info("revert", c.req.valid("json"))
  495. const session = await Session.revert({ sessionID: id, ...c.req.valid("json") })
  496. return c.json(session)
  497. },
  498. )
  499. .post(
  500. "/session/:id/unrevert",
  501. describeRoute({
  502. description: "Restore all reverted messages",
  503. responses: {
  504. 200: {
  505. description: "Updated session",
  506. content: {
  507. "application/json": {
  508. schema: resolver(Session.Info),
  509. },
  510. },
  511. },
  512. },
  513. }),
  514. zValidator(
  515. "param",
  516. z.object({
  517. id: z.string(),
  518. }),
  519. ),
  520. async (c) => {
  521. const id = c.req.valid("param").id
  522. const session = await Session.unrevert({ sessionID: id })
  523. return c.json(session)
  524. },
  525. )
  526. .get(
  527. "/config/providers",
  528. describeRoute({
  529. description: "List all providers",
  530. responses: {
  531. 200: {
  532. description: "List of providers",
  533. content: {
  534. "application/json": {
  535. schema: resolver(
  536. z.object({
  537. providers: ModelsDev.Provider.array(),
  538. default: z.record(z.string(), z.string()),
  539. }),
  540. ),
  541. },
  542. },
  543. },
  544. },
  545. }),
  546. async (c) => {
  547. const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info))
  548. return c.json({
  549. providers: Object.values(providers),
  550. default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
  551. })
  552. },
  553. )
  554. .get(
  555. "/find",
  556. describeRoute({
  557. description: "Find text in files",
  558. responses: {
  559. 200: {
  560. description: "Matches",
  561. content: {
  562. "application/json": {
  563. schema: resolver(Ripgrep.Match.shape.data.array()),
  564. },
  565. },
  566. },
  567. },
  568. }),
  569. zValidator(
  570. "query",
  571. z.object({
  572. pattern: z.string(),
  573. }),
  574. ),
  575. async (c) => {
  576. const app = App.info()
  577. const pattern = c.req.valid("query").pattern
  578. const result = await Ripgrep.search({
  579. cwd: app.path.cwd,
  580. pattern,
  581. limit: 10,
  582. })
  583. return c.json(result)
  584. },
  585. )
  586. .get(
  587. "/find/file",
  588. describeRoute({
  589. description: "Find files",
  590. responses: {
  591. 200: {
  592. description: "File paths",
  593. content: {
  594. "application/json": {
  595. schema: resolver(z.string().array()),
  596. },
  597. },
  598. },
  599. },
  600. }),
  601. zValidator(
  602. "query",
  603. z.object({
  604. query: z.string(),
  605. }),
  606. ),
  607. async (c) => {
  608. const query = c.req.valid("query").query
  609. const app = App.info()
  610. const result = await Ripgrep.files({
  611. cwd: app.path.cwd,
  612. query,
  613. limit: 10,
  614. })
  615. return c.json(result)
  616. },
  617. )
  618. .get(
  619. "/find/symbol",
  620. describeRoute({
  621. description: "Find workspace symbols",
  622. responses: {
  623. 200: {
  624. description: "Symbols",
  625. content: {
  626. "application/json": {
  627. schema: resolver(LSP.Symbol.array()),
  628. },
  629. },
  630. },
  631. },
  632. }),
  633. zValidator(
  634. "query",
  635. z.object({
  636. query: z.string(),
  637. }),
  638. ),
  639. async (c) => {
  640. const query = c.req.valid("query").query
  641. const result = await LSP.workspaceSymbol(query)
  642. return c.json(result)
  643. },
  644. )
  645. .get(
  646. "/file",
  647. describeRoute({
  648. description: "Read a file",
  649. responses: {
  650. 200: {
  651. description: "File content",
  652. content: {
  653. "application/json": {
  654. schema: resolver(
  655. z.object({
  656. type: z.enum(["raw", "patch"]),
  657. content: z.string(),
  658. }),
  659. ),
  660. },
  661. },
  662. },
  663. },
  664. }),
  665. zValidator(
  666. "query",
  667. z.object({
  668. path: z.string(),
  669. }),
  670. ),
  671. async (c) => {
  672. const path = c.req.valid("query").path
  673. const content = await File.read(path)
  674. log.info("read file", {
  675. path,
  676. content: content.content,
  677. })
  678. return c.json(content)
  679. },
  680. )
  681. .get(
  682. "/file/status",
  683. describeRoute({
  684. description: "Get file status",
  685. responses: {
  686. 200: {
  687. description: "File status",
  688. content: {
  689. "application/json": {
  690. schema: resolver(File.Info.array()),
  691. },
  692. },
  693. },
  694. },
  695. }),
  696. async (c) => {
  697. const content = await File.status()
  698. return c.json(content)
  699. },
  700. )
  701. .post(
  702. "/log",
  703. describeRoute({
  704. description: "Write a log entry to the server logs",
  705. responses: {
  706. 200: {
  707. description: "Log entry written successfully",
  708. content: {
  709. "application/json": {
  710. schema: resolver(z.boolean()),
  711. },
  712. },
  713. },
  714. },
  715. }),
  716. zValidator(
  717. "json",
  718. z.object({
  719. service: z.string().openapi({ description: "Service name for the log entry" }),
  720. level: z.enum(["debug", "info", "error", "warn"]).openapi({ description: "Log level" }),
  721. message: z.string().openapi({ description: "Log message" }),
  722. extra: z
  723. .record(z.string(), z.any())
  724. .optional()
  725. .openapi({ description: "Additional metadata for the log entry" }),
  726. }),
  727. ),
  728. async (c) => {
  729. const { service, level, message, extra } = c.req.valid("json")
  730. const logger = Log.create({ service })
  731. switch (level) {
  732. case "debug":
  733. logger.debug(message, extra)
  734. break
  735. case "info":
  736. logger.info(message, extra)
  737. break
  738. case "error":
  739. logger.error(message, extra)
  740. break
  741. case "warn":
  742. logger.warn(message, extra)
  743. break
  744. }
  745. return c.json(true)
  746. },
  747. )
  748. .get(
  749. "/mode",
  750. describeRoute({
  751. description: "List all modes",
  752. responses: {
  753. 200: {
  754. description: "List of modes",
  755. content: {
  756. "application/json": {
  757. schema: resolver(Mode.Info.array()),
  758. },
  759. },
  760. },
  761. },
  762. }),
  763. async (c) => {
  764. const modes = await Mode.list()
  765. return c.json(modes)
  766. },
  767. )
  768. .post(
  769. "/tui/append-prompt",
  770. describeRoute({
  771. description: "Append prompt to the TUI",
  772. responses: {
  773. 200: {
  774. description: "Prompt processed successfully",
  775. content: {
  776. "application/json": {
  777. schema: resolver(z.boolean()),
  778. },
  779. },
  780. },
  781. },
  782. }),
  783. zValidator(
  784. "json",
  785. z.object({
  786. text: z.string(),
  787. }),
  788. ),
  789. async (c) => c.json(await callTui(c)),
  790. )
  791. .post(
  792. "/tui/open-help",
  793. describeRoute({
  794. description: "Open the help dialog",
  795. responses: {
  796. 200: {
  797. description: "Help dialog opened successfully",
  798. content: {
  799. "application/json": {
  800. schema: resolver(z.boolean()),
  801. },
  802. },
  803. },
  804. },
  805. }),
  806. async (c) => c.json(await callTui(c)),
  807. )
  808. .route("/tui/control", TuiRoute)
  809. return result
  810. }
  811. export async function openapi() {
  812. const a = app()
  813. const result = await generateSpecs(a, {
  814. documentation: {
  815. info: {
  816. title: "opencode",
  817. version: "1.0.0",
  818. description: "opencode api",
  819. },
  820. openapi: "3.0.0",
  821. },
  822. })
  823. return result
  824. }
  825. export function listen(opts: { port: number; hostname: string }) {
  826. const server = Bun.serve({
  827. port: opts.port,
  828. hostname: opts.hostname,
  829. idleTimeout: 0,
  830. fetch: app().fetch,
  831. })
  832. return server
  833. }
  834. }