server.ts 20 KB

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