server.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  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 { Global } from "../global"
  13. import { mapValues } from "remeda"
  14. import { NamedError } from "../util/error"
  15. import { Fzf } from "../external/fzf"
  16. const ERRORS = {
  17. 400: {
  18. description: "Bad request",
  19. content: {
  20. "application/json": {
  21. schema: resolver(
  22. z
  23. .object({
  24. data: z.record(z.string(), z.any()),
  25. })
  26. .openapi({
  27. ref: "Error",
  28. }),
  29. ),
  30. },
  31. },
  32. },
  33. } as const
  34. export namespace Server {
  35. const log = Log.create({ service: "server" })
  36. export type Routes = ReturnType<typeof app>
  37. function app() {
  38. const app = new Hono()
  39. const result = app
  40. .onError((err, c) => {
  41. if (err instanceof NamedError) {
  42. return c.json(err.toObject(), {
  43. status: 400,
  44. })
  45. }
  46. return c.json(
  47. new NamedError.Unknown({ message: err.toString() }).toObject(),
  48. {
  49. status: 400,
  50. },
  51. )
  52. })
  53. .use((c, next) => {
  54. log.info("request", {
  55. method: c.req.method,
  56. path: c.req.path,
  57. })
  58. return next()
  59. })
  60. .get(
  61. "/openapi",
  62. openAPISpecs(app, {
  63. documentation: {
  64. info: {
  65. title: "opencode",
  66. version: "1.0.0",
  67. description: "opencode api",
  68. },
  69. openapi: "3.0.0",
  70. },
  71. }),
  72. )
  73. .get(
  74. "/event",
  75. describeRoute({
  76. description: "Get events",
  77. responses: {
  78. 200: {
  79. description: "Event stream",
  80. content: {
  81. "application/json": {
  82. schema: resolver(
  83. Bus.payloads().openapi({
  84. ref: "Event",
  85. }),
  86. ),
  87. },
  88. },
  89. },
  90. },
  91. }),
  92. async (c) => {
  93. log.info("event connected")
  94. return streamSSE(c, async (stream) => {
  95. stream.writeSSE({
  96. data: JSON.stringify({}),
  97. })
  98. const unsub = Bus.subscribeAll(async (event) => {
  99. await stream.writeSSE({
  100. data: JSON.stringify(event),
  101. })
  102. })
  103. await new Promise<void>((resolve) => {
  104. stream.onAbort(() => {
  105. unsub()
  106. resolve()
  107. log.info("event disconnected")
  108. })
  109. })
  110. })
  111. },
  112. )
  113. .post(
  114. "/app_info",
  115. describeRoute({
  116. description: "Get app info",
  117. responses: {
  118. 200: {
  119. description: "200",
  120. content: {
  121. "application/json": {
  122. schema: resolver(App.Info),
  123. },
  124. },
  125. },
  126. },
  127. }),
  128. async (c) => {
  129. return c.json(App.info())
  130. },
  131. )
  132. .post(
  133. "/app_initialize",
  134. describeRoute({
  135. description: "Initialize the app",
  136. responses: {
  137. 200: {
  138. description: "Initialize the app",
  139. content: {
  140. "application/json": {
  141. schema: resolver(z.boolean()),
  142. },
  143. },
  144. },
  145. },
  146. }),
  147. async (c) => {
  148. await App.initialize()
  149. return c.json(true)
  150. },
  151. )
  152. .post(
  153. "/session_initialize",
  154. describeRoute({
  155. description: "Analyze the app and create an AGENTS.md file",
  156. responses: {
  157. 200: {
  158. description: "200",
  159. content: {
  160. "application/json": {
  161. schema: resolver(z.boolean()),
  162. },
  163. },
  164. },
  165. },
  166. }),
  167. zValidator(
  168. "json",
  169. z.object({
  170. sessionID: z.string(),
  171. providerID: z.string(),
  172. modelID: z.string(),
  173. }),
  174. ),
  175. async (c) => {
  176. const body = c.req.valid("json")
  177. await Session.initialize(body)
  178. return c.json(true)
  179. },
  180. )
  181. .post(
  182. "/path_get",
  183. describeRoute({
  184. description: "Get paths",
  185. responses: {
  186. 200: {
  187. description: "200",
  188. content: {
  189. "application/json": {
  190. schema: resolver(
  191. z.object({
  192. root: z.string(),
  193. data: z.string(),
  194. cwd: z.string(),
  195. config: z.string(),
  196. }),
  197. ),
  198. },
  199. },
  200. },
  201. },
  202. }),
  203. async (c) => {
  204. const app = App.info()
  205. return c.json({
  206. root: app.path.root,
  207. data: app.path.data,
  208. cwd: app.path.cwd,
  209. config: Global.Path.data,
  210. })
  211. },
  212. )
  213. .post(
  214. "/session_create",
  215. describeRoute({
  216. description: "Create a new session",
  217. responses: {
  218. ...ERRORS,
  219. 200: {
  220. description: "Successfully created session",
  221. content: {
  222. "application/json": {
  223. schema: resolver(Session.Info),
  224. },
  225. },
  226. },
  227. },
  228. }),
  229. async (c) => {
  230. const session = await Session.create()
  231. return c.json(session)
  232. },
  233. )
  234. .post(
  235. "/session_share",
  236. describeRoute({
  237. description: "Share the session",
  238. responses: {
  239. 200: {
  240. description: "Successfully shared session",
  241. content: {
  242. "application/json": {
  243. schema: resolver(Session.Info),
  244. },
  245. },
  246. },
  247. },
  248. }),
  249. zValidator(
  250. "json",
  251. z.object({
  252. sessionID: z.string(),
  253. }),
  254. ),
  255. async (c) => {
  256. const body = c.req.valid("json")
  257. await Session.share(body.sessionID)
  258. const session = await Session.get(body.sessionID)
  259. return c.json(session)
  260. },
  261. )
  262. .post(
  263. "/session_messages",
  264. describeRoute({
  265. description: "Get messages for a session",
  266. responses: {
  267. 200: {
  268. description: "Successfully created session",
  269. content: {
  270. "application/json": {
  271. schema: resolver(Message.Info.array()),
  272. },
  273. },
  274. },
  275. },
  276. }),
  277. zValidator(
  278. "json",
  279. z.object({
  280. sessionID: z.string(),
  281. }),
  282. ),
  283. async (c) => {
  284. const messages = await Session.messages(c.req.valid("json").sessionID)
  285. return c.json(messages)
  286. },
  287. )
  288. .post(
  289. "/session_list",
  290. describeRoute({
  291. description: "List all sessions",
  292. responses: {
  293. 200: {
  294. description: "List of sessions",
  295. content: {
  296. "application/json": {
  297. schema: resolver(Session.Info.array()),
  298. },
  299. },
  300. },
  301. },
  302. }),
  303. async (c) => {
  304. const sessions = await Array.fromAsync(Session.list())
  305. return c.json(sessions)
  306. },
  307. )
  308. .post(
  309. "/session_abort",
  310. describeRoute({
  311. description: "Abort a session",
  312. responses: {
  313. 200: {
  314. description: "Aborted session",
  315. content: {
  316. "application/json": {
  317. schema: resolver(z.boolean()),
  318. },
  319. },
  320. },
  321. },
  322. }),
  323. zValidator(
  324. "json",
  325. z.object({
  326. sessionID: z.string(),
  327. }),
  328. ),
  329. async (c) => {
  330. const body = c.req.valid("json")
  331. return c.json(Session.abort(body.sessionID))
  332. },
  333. )
  334. .post(
  335. "/session_summarize",
  336. describeRoute({
  337. description: "Summarize the session",
  338. responses: {
  339. 200: {
  340. description: "Summarize the session",
  341. content: {
  342. "application/json": {
  343. schema: resolver(z.boolean()),
  344. },
  345. },
  346. },
  347. },
  348. }),
  349. zValidator(
  350. "json",
  351. z.object({
  352. sessionID: z.string(),
  353. providerID: z.string(),
  354. modelID: z.string(),
  355. }),
  356. ),
  357. async (c) => {
  358. const body = c.req.valid("json")
  359. await Session.summarize(body)
  360. return c.json(true)
  361. },
  362. )
  363. .post(
  364. "/session_chat",
  365. describeRoute({
  366. description: "Chat with a model",
  367. responses: {
  368. 200: {
  369. description: "Chat with a model",
  370. content: {
  371. "application/json": {
  372. schema: resolver(Message.Info),
  373. },
  374. },
  375. },
  376. },
  377. }),
  378. zValidator(
  379. "json",
  380. z.object({
  381. sessionID: z.string(),
  382. providerID: z.string(),
  383. modelID: z.string(),
  384. parts: Message.Part.array(),
  385. }),
  386. ),
  387. async (c) => {
  388. const body = c.req.valid("json")
  389. const msg = await Session.chat(body)
  390. return c.json(msg)
  391. },
  392. )
  393. .post(
  394. "/provider_list",
  395. describeRoute({
  396. description: "List all providers",
  397. responses: {
  398. 200: {
  399. description: "List of providers",
  400. content: {
  401. "application/json": {
  402. schema: resolver(
  403. z.object({
  404. providers: Provider.Info.array(),
  405. default: z.record(z.string(), z.string()),
  406. }),
  407. ),
  408. },
  409. },
  410. },
  411. },
  412. }),
  413. async (c) => {
  414. const providers = await Provider.list().then((x) =>
  415. mapValues(x, (item) => item.info),
  416. )
  417. return c.json({
  418. providers: Object.values(providers),
  419. defaults: mapValues(
  420. providers,
  421. (item) => Provider.sort(Object.values(item.models))[0].id,
  422. ),
  423. })
  424. },
  425. )
  426. .post(
  427. "/file_search",
  428. describeRoute({
  429. description: "Search for files",
  430. responses: {
  431. 200: {
  432. description: "Search for files",
  433. content: {
  434. "application/json": {
  435. schema: resolver(z.string().array()),
  436. },
  437. },
  438. },
  439. },
  440. }),
  441. zValidator(
  442. "json",
  443. z.object({
  444. query: z.string(),
  445. }),
  446. ),
  447. async (c) => {
  448. const body = c.req.valid("json")
  449. const app = App.info()
  450. const result = await Fzf.search(app.path.cwd, body.query)
  451. return c.json(result)
  452. },
  453. )
  454. return result
  455. }
  456. export async function openapi() {
  457. const a = app()
  458. const result = await generateSpecs(a, {
  459. documentation: {
  460. info: {
  461. title: "opencode",
  462. version: "1.0.0",
  463. description: "opencode api",
  464. },
  465. openapi: "3.0.0",
  466. },
  467. })
  468. return result
  469. }
  470. export function listen() {
  471. const server = Bun.serve({
  472. port: 0,
  473. hostname: "0.0.0.0",
  474. idleTimeout: 0,
  475. fetch: app().fetch,
  476. })
  477. return server
  478. }
  479. }