server.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  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 { Message } from "../session/message"
  10. import { Provider } from "../provider/provider"
  11. import { App } from "../app/app"
  12. import { mapValues } from "remeda"
  13. import { NamedError } from "../util/error"
  14. import { ModelsDev } from "../provider/models"
  15. import { Ripgrep } from "../file/ripgrep"
  16. import { Config } from "../config/config"
  17. import { File } from "../file"
  18. import { LSP } from "../lsp"
  19. const ERRORS = {
  20. 400: {
  21. description: "Bad request",
  22. content: {
  23. "application/json": {
  24. schema: resolver(
  25. z
  26. .object({
  27. data: z.record(z.string(), z.any()),
  28. })
  29. .openapi({
  30. ref: "Error",
  31. }),
  32. ),
  33. },
  34. },
  35. },
  36. } as const
  37. export namespace Server {
  38. const log = Log.create({ service: "server" })
  39. export type Routes = ReturnType<typeof app>
  40. function app() {
  41. const app = new Hono()
  42. const result = app
  43. .onError((err, c) => {
  44. if (err instanceof NamedError) {
  45. return c.json(err.toObject(), {
  46. status: 400,
  47. })
  48. }
  49. return c.json(
  50. new NamedError.Unknown({ message: err.toString() }).toObject(),
  51. {
  52. status: 400,
  53. },
  54. )
  55. })
  56. .use(async (c, next) => {
  57. log.info("request", {
  58. method: c.req.method,
  59. path: c.req.path,
  60. })
  61. const start = Date.now()
  62. await next()
  63. log.info("response", {
  64. duration: Date.now() - start,
  65. })
  66. })
  67. .get(
  68. "/doc",
  69. openAPISpecs(app, {
  70. documentation: {
  71. info: {
  72. title: "opencode",
  73. version: "0.0.3",
  74. description: "opencode api",
  75. },
  76. openapi: "3.0.0",
  77. },
  78. }),
  79. )
  80. .get(
  81. "/event",
  82. describeRoute({
  83. description: "Get events",
  84. responses: {
  85. 200: {
  86. description: "Event stream",
  87. content: {
  88. "application/json": {
  89. schema: resolver(
  90. Bus.payloads().openapi({
  91. ref: "Event",
  92. }),
  93. ),
  94. },
  95. },
  96. },
  97. },
  98. }),
  99. async (c) => {
  100. log.info("event connected")
  101. return streamSSE(c, async (stream) => {
  102. stream.writeSSE({
  103. data: JSON.stringify({}),
  104. })
  105. const unsub = Bus.subscribeAll(async (event) => {
  106. await stream.writeSSE({
  107. data: JSON.stringify(event),
  108. })
  109. })
  110. await new Promise<void>((resolve) => {
  111. stream.onAbort(() => {
  112. unsub()
  113. resolve()
  114. log.info("event disconnected")
  115. })
  116. })
  117. })
  118. },
  119. )
  120. .get(
  121. "/app",
  122. describeRoute({
  123. description: "Get app info",
  124. responses: {
  125. 200: {
  126. description: "200",
  127. content: {
  128. "application/json": {
  129. schema: resolver(App.Info),
  130. },
  131. },
  132. },
  133. },
  134. }),
  135. async (c) => {
  136. return c.json(App.info())
  137. },
  138. )
  139. .post(
  140. "/app/init",
  141. describeRoute({
  142. description: "Initialize the app",
  143. responses: {
  144. 200: {
  145. description: "Initialize the app",
  146. content: {
  147. "application/json": {
  148. schema: resolver(z.boolean()),
  149. },
  150. },
  151. },
  152. },
  153. }),
  154. async (c) => {
  155. await App.initialize()
  156. return c.json(true)
  157. },
  158. )
  159. .get(
  160. "/config",
  161. describeRoute({
  162. description: "Get config info",
  163. responses: {
  164. 200: {
  165. description: "Get config info",
  166. content: {
  167. "application/json": {
  168. schema: resolver(Config.Info),
  169. },
  170. },
  171. },
  172. },
  173. }),
  174. async (c) => {
  175. return c.json(await Config.get())
  176. },
  177. )
  178. .get(
  179. "/session",
  180. describeRoute({
  181. description: "List all sessions",
  182. responses: {
  183. 200: {
  184. description: "List of sessions",
  185. content: {
  186. "application/json": {
  187. schema: resolver(Session.Info.array()),
  188. },
  189. },
  190. },
  191. },
  192. }),
  193. async (c) => {
  194. const sessions = await Array.fromAsync(Session.list())
  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. providerID: z.string(),
  270. modelID: z.string(),
  271. }),
  272. ),
  273. async (c) => {
  274. const sessionID = c.req.valid("param").id
  275. const body = c.req.valid("json")
  276. await Session.initialize({ ...body, sessionID })
  277. return c.json(true)
  278. },
  279. )
  280. .post(
  281. "/session/:id/abort",
  282. describeRoute({
  283. description: "Abort a session",
  284. responses: {
  285. 200: {
  286. description: "Aborted session",
  287. content: {
  288. "application/json": {
  289. schema: resolver(z.boolean()),
  290. },
  291. },
  292. },
  293. },
  294. }),
  295. zValidator(
  296. "param",
  297. z.object({
  298. id: z.string(),
  299. }),
  300. ),
  301. async (c) => {
  302. return c.json(Session.abort(c.req.valid("param").id))
  303. },
  304. )
  305. .post(
  306. "/session/:id/share",
  307. describeRoute({
  308. description: "Share a session",
  309. responses: {
  310. 200: {
  311. description: "Successfully shared session",
  312. content: {
  313. "application/json": {
  314. schema: resolver(Session.Info),
  315. },
  316. },
  317. },
  318. },
  319. }),
  320. zValidator(
  321. "param",
  322. z.object({
  323. id: z.string(),
  324. }),
  325. ),
  326. async (c) => {
  327. const id = c.req.valid("param").id
  328. await Session.share(id)
  329. const session = await Session.get(id)
  330. return c.json(session)
  331. },
  332. )
  333. .delete(
  334. "/session/:id/share",
  335. describeRoute({
  336. description: "Unshare the session",
  337. responses: {
  338. 200: {
  339. description: "Successfully unshared session",
  340. content: {
  341. "application/json": {
  342. schema: resolver(Session.Info),
  343. },
  344. },
  345. },
  346. },
  347. }),
  348. zValidator(
  349. "param",
  350. z.object({
  351. id: z.string(),
  352. }),
  353. ),
  354. async (c) => {
  355. const id = c.req.valid("param").id
  356. await Session.unshare(id)
  357. const session = await Session.get(id)
  358. return c.json(session)
  359. },
  360. )
  361. .post(
  362. "/session/:id/summarize",
  363. describeRoute({
  364. description: "Summarize the session",
  365. responses: {
  366. 200: {
  367. description: "Summarized session",
  368. content: {
  369. "application/json": {
  370. schema: resolver(z.boolean()),
  371. },
  372. },
  373. },
  374. },
  375. }),
  376. zValidator(
  377. "param",
  378. z.object({
  379. id: z.string().openapi({ description: "Session ID" }),
  380. }),
  381. ),
  382. zValidator(
  383. "json",
  384. z.object({
  385. providerID: z.string(),
  386. modelID: z.string(),
  387. }),
  388. ),
  389. async (c) => {
  390. const id = c.req.valid("param").id
  391. const body = c.req.valid("json")
  392. await Session.summarize({ ...body, sessionID: id })
  393. return c.json(true)
  394. },
  395. )
  396. .get(
  397. "/session/:id/message",
  398. describeRoute({
  399. description: "List messages for a session",
  400. responses: {
  401. 200: {
  402. description: "List of messages",
  403. content: {
  404. "application/json": {
  405. schema: resolver(Message.Info.array()),
  406. },
  407. },
  408. },
  409. },
  410. }),
  411. zValidator(
  412. "param",
  413. z.object({
  414. id: z.string().openapi({ description: "Session ID" }),
  415. }),
  416. ),
  417. async (c) => {
  418. const messages = await Session.messages(c.req.valid("param").id)
  419. return c.json(messages)
  420. },
  421. )
  422. .post(
  423. "/session/:id/message",
  424. describeRoute({
  425. description: "Create and send a new message to a session",
  426. responses: {
  427. 200: {
  428. description: "Created message",
  429. content: {
  430. "application/json": {
  431. schema: resolver(Message.Info),
  432. },
  433. },
  434. },
  435. },
  436. }),
  437. zValidator(
  438. "param",
  439. z.object({
  440. id: z.string().openapi({ description: "Session ID" }),
  441. }),
  442. ),
  443. zValidator(
  444. "json",
  445. z.object({
  446. providerID: z.string(),
  447. modelID: z.string(),
  448. parts: Message.MessagePart.array(),
  449. }),
  450. ),
  451. async (c) => {
  452. const sessionID = c.req.valid("param").id
  453. const body = c.req.valid("json")
  454. const msg = await Session.chat({ ...body, sessionID })
  455. return c.json(msg)
  456. },
  457. )
  458. .get(
  459. "/config/providers",
  460. describeRoute({
  461. description: "List all providers",
  462. responses: {
  463. 200: {
  464. description: "List of providers",
  465. content: {
  466. "application/json": {
  467. schema: resolver(
  468. z.object({
  469. providers: ModelsDev.Provider.array(),
  470. default: z.record(z.string(), z.string()),
  471. }),
  472. ),
  473. },
  474. },
  475. },
  476. },
  477. }),
  478. async (c) => {
  479. const providers = await Provider.list().then((x) =>
  480. mapValues(x, (item) => item.info),
  481. )
  482. return c.json({
  483. providers: Object.values(providers),
  484. default: mapValues(
  485. providers,
  486. (item) => Provider.sort(Object.values(item.models))[0].id,
  487. ),
  488. })
  489. },
  490. )
  491. .get(
  492. "/find",
  493. describeRoute({
  494. description: "Find text in files",
  495. responses: {
  496. 200: {
  497. description: "Matches",
  498. content: {
  499. "application/json": {
  500. schema: resolver(Ripgrep.Match.shape.data.array()),
  501. },
  502. },
  503. },
  504. },
  505. }),
  506. zValidator(
  507. "query",
  508. z.object({
  509. pattern: z.string(),
  510. }),
  511. ),
  512. async (c) => {
  513. const app = App.info()
  514. const pattern = c.req.valid("query").pattern
  515. const result = await Ripgrep.search({
  516. cwd: app.path.cwd,
  517. pattern,
  518. limit: 10,
  519. })
  520. return c.json(result)
  521. },
  522. )
  523. .get(
  524. "/find/file",
  525. describeRoute({
  526. description: "Find files",
  527. responses: {
  528. 200: {
  529. description: "File paths",
  530. content: {
  531. "application/json": {
  532. schema: resolver(z.string().array()),
  533. },
  534. },
  535. },
  536. },
  537. }),
  538. zValidator(
  539. "query",
  540. z.object({
  541. query: z.string(),
  542. }),
  543. ),
  544. async (c) => {
  545. const query = c.req.valid("query").query
  546. const app = App.info()
  547. const result = await Ripgrep.files({
  548. cwd: app.path.cwd,
  549. query,
  550. limit: 10,
  551. })
  552. return c.json(result)
  553. },
  554. )
  555. .get(
  556. "/find/symbol",
  557. describeRoute({
  558. description: "Find workspace symbols",
  559. responses: {
  560. 200: {
  561. description: "Symbols",
  562. content: {
  563. "application/json": {
  564. schema: resolver(z.unknown().array()),
  565. },
  566. },
  567. },
  568. },
  569. }),
  570. zValidator(
  571. "query",
  572. z.object({
  573. query: z.string(),
  574. }),
  575. ),
  576. async (c) => {
  577. const query = c.req.valid("query").query
  578. const result = await LSP.workspaceSymbol(query)
  579. return c.json(result)
  580. },
  581. )
  582. .get(
  583. "/file",
  584. describeRoute({
  585. description: "Read a file",
  586. responses: {
  587. 200: {
  588. description: "File content",
  589. content: {
  590. "application/json": {
  591. schema: resolver(
  592. z.object({
  593. type: z.enum(["raw", "patch"]),
  594. content: z.string(),
  595. }),
  596. ),
  597. },
  598. },
  599. },
  600. },
  601. }),
  602. zValidator(
  603. "query",
  604. z.object({
  605. path: z.string(),
  606. }),
  607. ),
  608. async (c) => {
  609. const path = c.req.valid("query").path
  610. const content = await File.read(path)
  611. log.info("read file", {
  612. path,
  613. content: content.content,
  614. })
  615. return c.json(content)
  616. },
  617. )
  618. .get(
  619. "/file/status",
  620. describeRoute({
  621. description: "Get file status",
  622. responses: {
  623. 200: {
  624. description: "File status",
  625. content: {
  626. "application/json": {
  627. schema: resolver(
  628. z
  629. .object({
  630. file: z.string(),
  631. added: z.number().int(),
  632. removed: z.number().int(),
  633. status: z.enum(["added", "deleted", "modified"]),
  634. })
  635. .array(),
  636. ),
  637. },
  638. },
  639. },
  640. },
  641. }),
  642. async (c) => {
  643. const content = await File.status()
  644. return c.json(content)
  645. },
  646. )
  647. return result
  648. }
  649. export async function openapi() {
  650. const a = app()
  651. const result = await generateSpecs(a, {
  652. documentation: {
  653. info: {
  654. title: "opencode",
  655. version: "1.0.0",
  656. description: "opencode api",
  657. },
  658. openapi: "3.0.0",
  659. },
  660. })
  661. return result
  662. }
  663. export function listen(opts: { port: number; hostname: string }) {
  664. const server = Bun.serve({
  665. port: opts.port,
  666. hostname: opts.hostname,
  667. idleTimeout: 0,
  668. fetch: app().fetch,
  669. })
  670. return server
  671. }
  672. }