server.ts 19 KB

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