bincode-cli 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/AGENTS.md +27 -0
  2. package/README.md +15 -0
  3. package/bin/bincode +98 -0
  4. package/bunfig.toml +4 -0
  5. package/package.json +124 -0
  6. package/parsers-config.ts +239 -0
  7. package/script/build.ts +167 -0
  8. package/script/postinstall.mjs +206 -0
  9. package/script/publish.ts +99 -0
  10. package/script/schema.ts +47 -0
  11. package/src/acp/README.md +164 -0
  12. package/src/acp/agent.ts +1051 -0
  13. package/src/acp/session.ts +101 -0
  14. package/src/acp/types.ts +22 -0
  15. package/src/agent/agent.ts +398 -0
  16. package/src/agent/generate.txt +75 -0
  17. package/src/agent/prompt/compaction.txt +12 -0
  18. package/src/agent/prompt/explore.txt +18 -0
  19. package/src/agent/prompt/summary.txt +10 -0
  20. package/src/agent/prompt/title.txt +36 -0
  21. package/src/auth/bineric-login.ts +506 -0
  22. package/src/auth/index.ts +70 -0
  23. package/src/bun/index.ts +114 -0
  24. package/src/bus/bus-event.ts +43 -0
  25. package/src/bus/global.ts +10 -0
  26. package/src/bus/index.ts +105 -0
  27. package/src/cli/auth-check.ts +61 -0
  28. package/src/cli/bootstrap.ts +21 -0
  29. package/src/cli/cmd/acp.ts +88 -0
  30. package/src/cli/cmd/agent.ts +256 -0
  31. package/src/cli/cmd/auth.ts +436 -0
  32. package/src/cli/cmd/cmd.ts +7 -0
  33. package/src/cli/cmd/debug/config.ts +15 -0
  34. package/src/cli/cmd/debug/file.ts +91 -0
  35. package/src/cli/cmd/debug/index.ts +43 -0
  36. package/src/cli/cmd/debug/lsp.ts +48 -0
  37. package/src/cli/cmd/debug/ripgrep.ts +83 -0
  38. package/src/cli/cmd/debug/scrap.ts +15 -0
  39. package/src/cli/cmd/debug/skill.ts +15 -0
  40. package/src/cli/cmd/debug/snapshot.ts +48 -0
  41. package/src/cli/cmd/export.ts +88 -0
  42. package/src/cli/cmd/generate.ts +38 -0
  43. package/src/cli/cmd/github.ts +1399 -0
  44. package/src/cli/cmd/import.ts +98 -0
  45. package/src/cli/cmd/login.ts +112 -0
  46. package/src/cli/cmd/logout.ts +38 -0
  47. package/src/cli/cmd/mcp.ts +654 -0
  48. package/src/cli/cmd/models.ts +77 -0
  49. package/src/cli/cmd/pr.ts +112 -0
  50. package/src/cli/cmd/run.ts +368 -0
  51. package/src/cli/cmd/serve.ts +31 -0
  52. package/src/cli/cmd/session.ts +106 -0
  53. package/src/cli/cmd/stats.ts +298 -0
  54. package/src/cli/cmd/tui/app.tsx +669 -0
  55. package/src/cli/cmd/tui/attach.ts +30 -0
  56. package/src/cli/cmd/tui/component/border.tsx +21 -0
  57. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  58. package/src/cli/cmd/tui/component/dialog-command.tsx +123 -0
  59. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  60. package/src/cli/cmd/tui/component/dialog-model.tsx +223 -0
  61. package/src/cli/cmd/tui/component/dialog-provider.tsx +224 -0
  62. package/src/cli/cmd/tui/component/dialog-session-list.tsx +102 -0
  63. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  64. package/src/cli/cmd/tui/component/dialog-status.tsx +162 -0
  65. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  66. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  67. package/src/cli/cmd/tui/component/logo.tsx +32 -0
  68. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +560 -0
  69. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  70. package/src/cli/cmd/tui/component/prompt/index.tsx +1052 -0
  71. package/src/cli/cmd/tui/context/args.tsx +14 -0
  72. package/src/cli/cmd/tui/context/directory.ts +13 -0
  73. package/src/cli/cmd/tui/context/exit.tsx +23 -0
  74. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  75. package/src/cli/cmd/tui/context/keybind.tsx +101 -0
  76. package/src/cli/cmd/tui/context/kv.tsx +49 -0
  77. package/src/cli/cmd/tui/context/local.tsx +339 -0
  78. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  79. package/src/cli/cmd/tui/context/route.tsx +46 -0
  80. package/src/cli/cmd/tui/context/sdk.tsx +74 -0
  81. package/src/cli/cmd/tui/context/sync.tsx +372 -0
  82. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  83. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  84. package/src/cli/cmd/tui/context/theme/bincode.json +245 -0
  85. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  86. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  87. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  88. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  89. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  90. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  91. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  92. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  93. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  94. package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
  95. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  96. package/src/cli/cmd/tui/context/theme/lucent-orng.json +227 -0
  97. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  98. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  99. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  100. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  101. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  102. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  103. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  104. package/src/cli/cmd/tui/context/theme/orng.json +245 -0
  105. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  106. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  107. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  108. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  109. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  110. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  111. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  112. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  113. package/src/cli/cmd/tui/context/theme.tsx +1109 -0
  114. package/src/cli/cmd/tui/event.ts +40 -0
  115. package/src/cli/cmd/tui/routes/home.tsx +105 -0
  116. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
  117. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
  118. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  119. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  120. package/src/cli/cmd/tui/routes/session/footer.tsx +88 -0
  121. package/src/cli/cmd/tui/routes/session/header.tsx +141 -0
  122. package/src/cli/cmd/tui/routes/session/index.tsx +1888 -0
  123. package/src/cli/cmd/tui/routes/session/sidebar.tsx +321 -0
  124. package/src/cli/cmd/tui/spawn.ts +60 -0
  125. package/src/cli/cmd/tui/thread.ts +120 -0
  126. package/src/cli/cmd/tui/ui/dialog-alert.tsx +57 -0
  127. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
  128. package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
  129. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +77 -0
  130. package/src/cli/cmd/tui/ui/dialog-select.tsx +330 -0
  131. package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
  132. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  133. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  134. package/src/cli/cmd/tui/util/clipboard.ts +127 -0
  135. package/src/cli/cmd/tui/util/editor.ts +32 -0
  136. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  137. package/src/cli/cmd/tui/worker.ts +63 -0
  138. package/src/cli/cmd/uninstall.ts +344 -0
  139. package/src/cli/cmd/upgrade.ts +67 -0
  140. package/src/cli/cmd/web.ts +84 -0
  141. package/src/cli/error.ts +55 -0
  142. package/src/cli/ui.ts +84 -0
  143. package/src/cli/upgrade.ts +25 -0
  144. package/src/command/index.ts +80 -0
  145. package/src/command/template/initialize.txt +10 -0
  146. package/src/command/template/review.txt +97 -0
  147. package/src/config/config.ts +995 -0
  148. package/src/config/markdown.ts +41 -0
  149. package/src/env/index.ts +26 -0
  150. package/src/file/ignore.ts +83 -0
  151. package/src/file/index.ts +328 -0
  152. package/src/file/ripgrep.ts +393 -0
  153. package/src/file/time.ts +64 -0
  154. package/src/file/watcher.ts +103 -0
  155. package/src/flag/flag.ts +46 -0
  156. package/src/format/formatter.ts +315 -0
  157. package/src/format/index.ts +137 -0
  158. package/src/global/index.ts +52 -0
  159. package/src/id/id.ts +73 -0
  160. package/src/ide/index.ts +76 -0
  161. package/src/index.ts +217 -0
  162. package/src/installation/index.ts +196 -0
  163. package/src/lsp/client.ts +229 -0
  164. package/src/lsp/index.ts +485 -0
  165. package/src/lsp/language.ts +116 -0
  166. package/src/lsp/server.ts +1895 -0
  167. package/src/mcp/auth.ts +135 -0
  168. package/src/mcp/index.ts +654 -0
  169. package/src/mcp/oauth-callback.ts +200 -0
  170. package/src/mcp/oauth-provider.ts +154 -0
  171. package/src/patch/index.ts +622 -0
  172. package/src/permission/index.ts +199 -0
  173. package/src/plugin/index.ts +101 -0
  174. package/src/project/bootstrap.ts +31 -0
  175. package/src/project/instance.ts +78 -0
  176. package/src/project/project.ts +221 -0
  177. package/src/project/state.ts +65 -0
  178. package/src/project/vcs.ts +76 -0
  179. package/src/provider/auth.ts +143 -0
  180. package/src/provider/models-macro.ts +11 -0
  181. package/src/provider/models.ts +106 -0
  182. package/src/provider/provider.ts +1071 -0
  183. package/src/provider/sdk/openai-compatible/src/README.md +5 -0
  184. package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
  185. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +101 -0
  186. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
  187. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +22 -0
  188. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
  189. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
  190. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
  191. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1713 -0
  192. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
  193. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
  194. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
  195. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
  196. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
  197. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
  198. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
  199. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
  200. package/src/provider/transform.ts +455 -0
  201. package/src/pty/index.ts +231 -0
  202. package/src/server/error.ts +36 -0
  203. package/src/server/project.ts +79 -0
  204. package/src/server/server.ts +2642 -0
  205. package/src/server/tui.ts +71 -0
  206. package/src/session/compaction.ts +223 -0
  207. package/src/session/index.ts +458 -0
  208. package/src/session/llm.ts +201 -0
  209. package/src/session/message-v2.ts +659 -0
  210. package/src/session/message.ts +189 -0
  211. package/src/session/processor.ts +409 -0
  212. package/src/session/prompt/anthropic-20250930.txt +166 -0
  213. package/src/session/prompt/anthropic.txt +104 -0
  214. package/src/session/prompt/anthropic_spoof.txt +1 -0
  215. package/src/session/prompt/beast.txt +147 -0
  216. package/src/session/prompt/build-switch.txt +5 -0
  217. package/src/session/prompt/codex.txt +318 -0
  218. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  219. package/src/session/prompt/gemini.txt +155 -0
  220. package/src/session/prompt/max-steps.txt +16 -0
  221. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  222. package/src/session/prompt/plan.txt +26 -0
  223. package/src/session/prompt/polaris.txt +106 -0
  224. package/src/session/prompt/qwen.txt +109 -0
  225. package/src/session/prompt.ts +1446 -0
  226. package/src/session/retry.ts +86 -0
  227. package/src/session/revert.ts +108 -0
  228. package/src/session/status.ts +76 -0
  229. package/src/session/summary.ts +194 -0
  230. package/src/session/system.ts +120 -0
  231. package/src/session/todo.ts +37 -0
  232. package/src/share/share-next.ts +194 -0
  233. package/src/share/share.ts +87 -0
  234. package/src/shell/shell.ts +67 -0
  235. package/src/skill/index.ts +1 -0
  236. package/src/skill/skill.ts +83 -0
  237. package/src/snapshot/index.ts +197 -0
  238. package/src/storage/storage.ts +226 -0
  239. package/src/tool/bash.ts +306 -0
  240. package/src/tool/bash.txt +158 -0
  241. package/src/tool/batch.ts +175 -0
  242. package/src/tool/batch.txt +24 -0
  243. package/src/tool/codesearch.ts +138 -0
  244. package/src/tool/codesearch.txt +12 -0
  245. package/src/tool/edit.ts +675 -0
  246. package/src/tool/edit.txt +10 -0
  247. package/src/tool/glob.ts +65 -0
  248. package/src/tool/glob.txt +6 -0
  249. package/src/tool/grep.ts +121 -0
  250. package/src/tool/grep.txt +8 -0
  251. package/src/tool/invalid.ts +17 -0
  252. package/src/tool/ls.ts +110 -0
  253. package/src/tool/ls.txt +1 -0
  254. package/src/tool/lsp-diagnostics.ts +26 -0
  255. package/src/tool/lsp-diagnostics.txt +1 -0
  256. package/src/tool/lsp-hover.ts +31 -0
  257. package/src/tool/lsp-hover.txt +1 -0
  258. package/src/tool/lsp.ts +87 -0
  259. package/src/tool/lsp.txt +19 -0
  260. package/src/tool/multiedit.ts +46 -0
  261. package/src/tool/multiedit.txt +41 -0
  262. package/src/tool/patch.ts +233 -0
  263. package/src/tool/patch.txt +1 -0
  264. package/src/tool/read.ts +219 -0
  265. package/src/tool/read.txt +12 -0
  266. package/src/tool/registry.ts +162 -0
  267. package/src/tool/skill.ts +100 -0
  268. package/src/tool/task.ts +136 -0
  269. package/src/tool/task.txt +60 -0
  270. package/src/tool/todo.ts +39 -0
  271. package/src/tool/todoread.txt +14 -0
  272. package/src/tool/todowrite.txt +167 -0
  273. package/src/tool/tool.ts +71 -0
  274. package/src/tool/webfetch.ts +187 -0
  275. package/src/tool/webfetch.txt +13 -0
  276. package/src/tool/websearch.ts +150 -0
  277. package/src/tool/websearch.txt +11 -0
  278. package/src/tool/write.ts +110 -0
  279. package/src/tool/write.txt +8 -0
  280. package/src/util/archive.ts +16 -0
  281. package/src/util/color.ts +19 -0
  282. package/src/util/context.ts +25 -0
  283. package/src/util/defer.ts +12 -0
  284. package/src/util/eventloop.ts +20 -0
  285. package/src/util/filesystem.ts +83 -0
  286. package/src/util/fn.ts +11 -0
  287. package/src/util/iife.ts +3 -0
  288. package/src/util/keybind.ts +102 -0
  289. package/src/util/lazy.ts +11 -0
  290. package/src/util/locale.ts +81 -0
  291. package/src/util/lock.ts +98 -0
  292. package/src/util/log.ts +180 -0
  293. package/src/util/queue.ts +32 -0
  294. package/src/util/rpc.ts +42 -0
  295. package/src/util/scrap.ts +10 -0
  296. package/src/util/signal.ts +12 -0
  297. package/src/util/timeout.ts +14 -0
  298. package/src/util/token.ts +7 -0
  299. package/src/util/wildcard.ts +54 -0
  300. package/tsconfig.json +16 -0
@@ -0,0 +1,2642 @@
1
+ import { BusEvent } from "@/bus/bus-event"
2
+ import { Bus } from "@/bus"
3
+ import { GlobalBus } from "@/bus/global"
4
+ import { Log } from "../util/log"
5
+ import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
6
+ import { Hono } from "hono"
7
+ import { cors } from "hono/cors"
8
+ import { stream, streamSSE } from "hono/streaming"
9
+ import { proxy } from "hono/proxy"
10
+ import { Session } from "../session"
11
+ import z from "zod"
12
+ import { Provider } from "../provider/provider"
13
+ import { filter, mapValues, sortBy, pipe } from "remeda"
14
+ import { NamedError } from "@bincode-ai/util/error"
15
+ import { ModelsDev } from "../provider/models"
16
+ import { Ripgrep } from "../file/ripgrep"
17
+ import { Config } from "../config/config"
18
+ import { File } from "../file"
19
+ import { LSP } from "../lsp"
20
+ import { Format } from "../format"
21
+ import { MessageV2 } from "../session/message-v2"
22
+ import { TuiRoute } from "./tui"
23
+ import { Permission } from "../permission"
24
+ import { Instance } from "../project/instance"
25
+ import { Vcs } from "../project/vcs"
26
+ import { Agent } from "../agent/agent"
27
+ import { Auth } from "../auth"
28
+ import { Command } from "../command"
29
+ import { ProviderAuth } from "../provider/auth"
30
+ import { Global } from "../global"
31
+ import { ProjectRoute } from "./project"
32
+ import { ToolRegistry } from "../tool/registry"
33
+ import { zodToJsonSchema } from "zod-to-json-schema"
34
+ import { SessionPrompt } from "../session/prompt"
35
+ import { SessionCompaction } from "../session/compaction"
36
+ import { SessionRevert } from "../session/revert"
37
+ import { lazy } from "../util/lazy"
38
+ import { Todo } from "../session/todo"
39
+ import { InstanceBootstrap } from "../project/bootstrap"
40
+ import { MCP } from "../mcp"
41
+ import { Storage } from "../storage/storage"
42
+ import type { ContentfulStatusCode } from "hono/utils/http-status"
43
+ import { TuiEvent } from "@/cli/cmd/tui/event"
44
+ import { Snapshot } from "@/snapshot"
45
+ import { SessionSummary } from "@/session/summary"
46
+ import { SessionStatus } from "@/session/status"
47
+ import { upgradeWebSocket, websocket } from "hono/bun"
48
+ import { errors } from "./error"
49
+ import { Pty } from "@/pty"
50
+ import { Installation } from "@/installation"
51
+
52
+ // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
53
+ globalThis.AI_SDK_LOG_WARNINGS = false
54
+
55
+ export namespace Server {
56
+ const log = Log.create({ service: "server" })
57
+
58
+ export const Event = {
59
+ Connected: BusEvent.define("server.connected", z.object({})),
60
+ Disposed: BusEvent.define("global.disposed", z.object({})),
61
+ }
62
+
63
+ const app = new Hono()
64
+ export const App = lazy(() =>
65
+ app
66
+ .onError((err, c) => {
67
+ log.error("failed", {
68
+ error: err,
69
+ })
70
+ if (err instanceof NamedError) {
71
+ let status: ContentfulStatusCode
72
+ if (err instanceof Storage.NotFoundError) status = 404
73
+ else if (err instanceof Provider.ModelNotFoundError) status = 400
74
+ else status = 500
75
+ return c.json(err.toObject(), { status })
76
+ }
77
+ const message = err instanceof Error && err.stack ? err.stack : err.toString()
78
+ return c.json(new NamedError.Unknown({ message }).toObject(), {
79
+ status: 500,
80
+ })
81
+ })
82
+ .use(async (c, next) => {
83
+ const skipLogging = c.req.path === "/log"
84
+ if (!skipLogging) {
85
+ log.info("request", {
86
+ method: c.req.method,
87
+ path: c.req.path,
88
+ })
89
+ }
90
+ const timer = log.time("request", {
91
+ method: c.req.method,
92
+ path: c.req.path,
93
+ })
94
+ await next()
95
+ if (!skipLogging) {
96
+ timer.stop()
97
+ }
98
+ })
99
+ .use(cors())
100
+ .get(
101
+ "/global/health",
102
+ describeRoute({
103
+ summary: "Get health",
104
+ description: "Get health information about the Bincode server.",
105
+ operationId: "global.health",
106
+ responses: {
107
+ 200: {
108
+ description: "Health information",
109
+ content: {
110
+ "application/json": {
111
+ schema: resolver(z.object({ healthy: z.literal(true), version: z.string() })),
112
+ },
113
+ },
114
+ },
115
+ },
116
+ }),
117
+ async (c) => {
118
+ return c.json({ healthy: true, version: Installation.VERSION })
119
+ },
120
+ )
121
+ .get(
122
+ "/global/event",
123
+ describeRoute({
124
+ summary: "Get global events",
125
+ description: "Subscribe to global events from the Bincode system using server-sent events.",
126
+ operationId: "global.event",
127
+ responses: {
128
+ 200: {
129
+ description: "Event stream",
130
+ content: {
131
+ "text/event-stream": {
132
+ schema: resolver(
133
+ z
134
+ .object({
135
+ directory: z.string(),
136
+ payload: BusEvent.payloads(),
137
+ })
138
+ .meta({
139
+ ref: "GlobalEvent",
140
+ }),
141
+ ),
142
+ },
143
+ },
144
+ },
145
+ },
146
+ }),
147
+ async (c) => {
148
+ log.info("global event connected")
149
+ return streamSSE(c, async (stream) => {
150
+ stream.writeSSE({
151
+ data: JSON.stringify({
152
+ payload: {
153
+ type: "server.connected",
154
+ properties: {},
155
+ },
156
+ }),
157
+ })
158
+ async function handler(event: any) {
159
+ await stream.writeSSE({
160
+ data: JSON.stringify(event),
161
+ })
162
+ }
163
+ GlobalBus.on("event", handler)
164
+
165
+ // Send heartbeat every 30s to prevent WKWebView timeout (60s default)
166
+ const heartbeat = setInterval(() => {
167
+ stream.writeSSE({
168
+ data: JSON.stringify({
169
+ payload: {
170
+ type: "server.heartbeat",
171
+ properties: {},
172
+ },
173
+ }),
174
+ })
175
+ }, 30000)
176
+
177
+ await new Promise<void>((resolve) => {
178
+ stream.onAbort(() => {
179
+ clearInterval(heartbeat)
180
+ GlobalBus.off("event", handler)
181
+ resolve()
182
+ log.info("global event disconnected")
183
+ })
184
+ })
185
+ })
186
+ },
187
+ )
188
+ .post(
189
+ "/global/dispose",
190
+ describeRoute({
191
+ summary: "Dispose instance",
192
+ description: "Clean up and dispose all Bincode instances, releasing all resources.",
193
+ operationId: "global.dispose",
194
+ responses: {
195
+ 200: {
196
+ description: "Global disposed",
197
+ content: {
198
+ "application/json": {
199
+ schema: resolver(z.boolean()),
200
+ },
201
+ },
202
+ },
203
+ },
204
+ }),
205
+ async (c) => {
206
+ await Instance.disposeAll()
207
+ GlobalBus.emit("event", {
208
+ directory: "global",
209
+ payload: {
210
+ type: Event.Disposed.type,
211
+ properties: {},
212
+ },
213
+ })
214
+ return c.json(true)
215
+ },
216
+ )
217
+ .use(async (c, next) => {
218
+ const directory = c.req.query("directory") || c.req.header("x-bincode-directory") || c.req.header("x-bincode-directory") || process.cwd()
219
+ return Instance.provide({
220
+ directory,
221
+ init: InstanceBootstrap,
222
+ async fn() {
223
+ return next()
224
+ },
225
+ })
226
+ })
227
+ .get(
228
+ "/doc",
229
+ openAPIRouteHandler(app, {
230
+ documentation: {
231
+ info: {
232
+ title: "bincode",
233
+ version: "0.0.3",
234
+ description: "bincode api",
235
+ },
236
+ openapi: "3.1.1",
237
+ },
238
+ }),
239
+ )
240
+ .use(validator("query", z.object({ directory: z.string().optional() })))
241
+
242
+ .route("/project", ProjectRoute)
243
+
244
+ .get(
245
+ "/pty",
246
+ describeRoute({
247
+ summary: "List PTY sessions",
248
+ description: "Get a list of all active pseudo-terminal (PTY) sessions managed by Bincode.",
249
+ operationId: "pty.list",
250
+ responses: {
251
+ 200: {
252
+ description: "List of sessions",
253
+ content: {
254
+ "application/json": {
255
+ schema: resolver(Pty.Info.array()),
256
+ },
257
+ },
258
+ },
259
+ },
260
+ }),
261
+ async (c) => {
262
+ return c.json(Pty.list())
263
+ },
264
+ )
265
+ .post(
266
+ "/pty",
267
+ describeRoute({
268
+ summary: "Create PTY session",
269
+ description: "Create a new pseudo-terminal (PTY) session for running shell commands and processes.",
270
+ operationId: "pty.create",
271
+ responses: {
272
+ 200: {
273
+ description: "Created session",
274
+ content: {
275
+ "application/json": {
276
+ schema: resolver(Pty.Info),
277
+ },
278
+ },
279
+ },
280
+ ...errors(400),
281
+ },
282
+ }),
283
+ validator("json", Pty.CreateInput),
284
+ async (c) => {
285
+ const info = await Pty.create(c.req.valid("json"))
286
+ return c.json(info)
287
+ },
288
+ )
289
+ .get(
290
+ "/pty/:ptyID",
291
+ describeRoute({
292
+ summary: "Get PTY session",
293
+ description: "Retrieve detailed information about a specific pseudo-terminal (PTY) session.",
294
+ operationId: "pty.get",
295
+ responses: {
296
+ 200: {
297
+ description: "Session info",
298
+ content: {
299
+ "application/json": {
300
+ schema: resolver(Pty.Info),
301
+ },
302
+ },
303
+ },
304
+ ...errors(404),
305
+ },
306
+ }),
307
+ validator("param", z.object({ ptyID: z.string() })),
308
+ async (c) => {
309
+ const info = Pty.get(c.req.valid("param").ptyID)
310
+ if (!info) {
311
+ throw new Storage.NotFoundError({ message: "Session not found" })
312
+ }
313
+ return c.json(info)
314
+ },
315
+ )
316
+ .put(
317
+ "/pty/:ptyID",
318
+ describeRoute({
319
+ summary: "Update PTY session",
320
+ description: "Update properties of an existing pseudo-terminal (PTY) session.",
321
+ operationId: "pty.update",
322
+ responses: {
323
+ 200: {
324
+ description: "Updated session",
325
+ content: {
326
+ "application/json": {
327
+ schema: resolver(Pty.Info),
328
+ },
329
+ },
330
+ },
331
+ ...errors(400),
332
+ },
333
+ }),
334
+ validator("param", z.object({ ptyID: z.string() })),
335
+ validator("json", Pty.UpdateInput),
336
+ async (c) => {
337
+ const info = await Pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
338
+ return c.json(info)
339
+ },
340
+ )
341
+ .delete(
342
+ "/pty/:ptyID",
343
+ describeRoute({
344
+ summary: "Remove PTY session",
345
+ description: "Remove and terminate a specific pseudo-terminal (PTY) session.",
346
+ operationId: "pty.remove",
347
+ responses: {
348
+ 200: {
349
+ description: "Session removed",
350
+ content: {
351
+ "application/json": {
352
+ schema: resolver(z.boolean()),
353
+ },
354
+ },
355
+ },
356
+ ...errors(404),
357
+ },
358
+ }),
359
+ validator("param", z.object({ ptyID: z.string() })),
360
+ async (c) => {
361
+ await Pty.remove(c.req.valid("param").ptyID)
362
+ return c.json(true)
363
+ },
364
+ )
365
+ .get(
366
+ "/pty/:ptyID/connect",
367
+ describeRoute({
368
+ summary: "Connect to PTY session",
369
+ description:
370
+ "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.",
371
+ operationId: "pty.connect",
372
+ responses: {
373
+ 200: {
374
+ description: "Connected session",
375
+ content: {
376
+ "application/json": {
377
+ schema: resolver(z.boolean()),
378
+ },
379
+ },
380
+ },
381
+ ...errors(404),
382
+ },
383
+ }),
384
+ validator("param", z.object({ ptyID: z.string() })),
385
+ upgradeWebSocket((c) => {
386
+ const id = c.req.param("ptyID")
387
+ let handler: ReturnType<typeof Pty.connect>
388
+ if (!Pty.get(id)) throw new Error("Session not found")
389
+ return {
390
+ onOpen(_event, ws) {
391
+ handler = Pty.connect(id, ws)
392
+ },
393
+ onMessage(event) {
394
+ handler?.onMessage(String(event.data))
395
+ },
396
+ onClose() {
397
+ handler?.onClose()
398
+ },
399
+ }
400
+ }),
401
+ )
402
+
403
+ .get(
404
+ "/config",
405
+ describeRoute({
406
+ summary: "Get configuration",
407
+ description: "Retrieve the current Bincode configuration settings and preferences.",
408
+ operationId: "config.get",
409
+ responses: {
410
+ 200: {
411
+ description: "Get config info",
412
+ content: {
413
+ "application/json": {
414
+ schema: resolver(Config.Info),
415
+ },
416
+ },
417
+ },
418
+ },
419
+ }),
420
+ async (c) => {
421
+ return c.json(await Config.get())
422
+ },
423
+ )
424
+
425
+ .patch(
426
+ "/config",
427
+ describeRoute({
428
+ summary: "Update configuration",
429
+ description: "Update Bincode configuration settings and preferences.",
430
+ operationId: "config.update",
431
+ responses: {
432
+ 200: {
433
+ description: "Successfully updated config",
434
+ content: {
435
+ "application/json": {
436
+ schema: resolver(Config.Info),
437
+ },
438
+ },
439
+ },
440
+ ...errors(400),
441
+ },
442
+ }),
443
+ validator("json", Config.Info),
444
+ async (c) => {
445
+ const config = c.req.valid("json")
446
+ await Config.update(config)
447
+ return c.json(config)
448
+ },
449
+ )
450
+ .get(
451
+ "/experimental/tool/ids",
452
+ describeRoute({
453
+ summary: "List tool IDs",
454
+ description:
455
+ "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
456
+ operationId: "tool.ids",
457
+ responses: {
458
+ 200: {
459
+ description: "Tool IDs",
460
+ content: {
461
+ "application/json": {
462
+ schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })),
463
+ },
464
+ },
465
+ },
466
+ ...errors(400),
467
+ },
468
+ }),
469
+ async (c) => {
470
+ return c.json(await ToolRegistry.ids())
471
+ },
472
+ )
473
+ .get(
474
+ "/experimental/tool",
475
+ describeRoute({
476
+ summary: "List tools",
477
+ description:
478
+ "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
479
+ operationId: "tool.list",
480
+ responses: {
481
+ 200: {
482
+ description: "Tools",
483
+ content: {
484
+ "application/json": {
485
+ schema: resolver(
486
+ z
487
+ .array(
488
+ z
489
+ .object({
490
+ id: z.string(),
491
+ description: z.string(),
492
+ parameters: z.any(),
493
+ })
494
+ .meta({ ref: "ToolListItem" }),
495
+ )
496
+ .meta({ ref: "ToolList" }),
497
+ ),
498
+ },
499
+ },
500
+ },
501
+ ...errors(400),
502
+ },
503
+ }),
504
+ validator(
505
+ "query",
506
+ z.object({
507
+ provider: z.string(),
508
+ model: z.string(),
509
+ }),
510
+ ),
511
+ async (c) => {
512
+ const { provider } = c.req.valid("query")
513
+ const tools = await ToolRegistry.tools(provider)
514
+ return c.json(
515
+ tools.map((t) => ({
516
+ id: t.id,
517
+ description: t.description,
518
+ // Handle both Zod schemas and plain JSON schemas
519
+ parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters,
520
+ })),
521
+ )
522
+ },
523
+ )
524
+ .post(
525
+ "/instance/dispose",
526
+ describeRoute({
527
+ summary: "Dispose instance",
528
+ description: "Clean up and dispose the current Bincode instance, releasing all resources.",
529
+ operationId: "instance.dispose",
530
+ responses: {
531
+ 200: {
532
+ description: "Instance disposed",
533
+ content: {
534
+ "application/json": {
535
+ schema: resolver(z.boolean()),
536
+ },
537
+ },
538
+ },
539
+ },
540
+ }),
541
+ async (c) => {
542
+ await Instance.dispose()
543
+ return c.json(true)
544
+ },
545
+ )
546
+ .get(
547
+ "/path",
548
+ describeRoute({
549
+ summary: "Get paths",
550
+ description: "Retrieve the current working directory and related path information for the Bincode instance.",
551
+ operationId: "path.get",
552
+ responses: {
553
+ 200: {
554
+ description: "Path",
555
+ content: {
556
+ "application/json": {
557
+ schema: resolver(
558
+ z
559
+ .object({
560
+ home: z.string(),
561
+ state: z.string(),
562
+ config: z.string(),
563
+ worktree: z.string(),
564
+ directory: z.string(),
565
+ })
566
+ .meta({
567
+ ref: "Path",
568
+ }),
569
+ ),
570
+ },
571
+ },
572
+ },
573
+ },
574
+ }),
575
+ async (c) => {
576
+ return c.json({
577
+ home: Global.Path.home,
578
+ state: Global.Path.state,
579
+ config: Global.Path.config,
580
+ worktree: Instance.worktree,
581
+ directory: Instance.directory,
582
+ })
583
+ },
584
+ )
585
+ .get(
586
+ "/vcs",
587
+ describeRoute({
588
+ summary: "Get VCS info",
589
+ description: "Retrieve version control system (VCS) information for the current project, such as git branch.",
590
+ operationId: "vcs.get",
591
+ responses: {
592
+ 200: {
593
+ description: "VCS info",
594
+ content: {
595
+ "application/json": {
596
+ schema: resolver(Vcs.Info),
597
+ },
598
+ },
599
+ },
600
+ },
601
+ }),
602
+ async (c) => {
603
+ const branch = await Vcs.branch()
604
+ return c.json({
605
+ branch,
606
+ })
607
+ },
608
+ )
609
+ .get(
610
+ "/session",
611
+ describeRoute({
612
+ summary: "List sessions",
613
+ description: "Get a list of all Bincode sessions, sorted by most recently updated.",
614
+ operationId: "session.list",
615
+ responses: {
616
+ 200: {
617
+ description: "List of sessions",
618
+ content: {
619
+ "application/json": {
620
+ schema: resolver(Session.Info.array()),
621
+ },
622
+ },
623
+ },
624
+ },
625
+ }),
626
+ async (c) => {
627
+ const sessions = await Array.fromAsync(Session.list())
628
+ pipe(
629
+ await Array.fromAsync(Session.list()),
630
+ filter((s) => !s.time.archived),
631
+ sortBy((s) => s.time.updated),
632
+ )
633
+ return c.json(sessions)
634
+ },
635
+ )
636
+ .get(
637
+ "/session/status",
638
+ describeRoute({
639
+ summary: "Get session status",
640
+ description: "Retrieve the current status of all sessions, including active, idle, and completed states.",
641
+ operationId: "session.status",
642
+ responses: {
643
+ 200: {
644
+ description: "Get session status",
645
+ content: {
646
+ "application/json": {
647
+ schema: resolver(z.record(z.string(), SessionStatus.Info)),
648
+ },
649
+ },
650
+ },
651
+ ...errors(400),
652
+ },
653
+ }),
654
+ async (c) => {
655
+ const result = SessionStatus.list()
656
+ return c.json(result)
657
+ },
658
+ )
659
+ .get(
660
+ "/session/:sessionID",
661
+ describeRoute({
662
+ summary: "Get session",
663
+ description: "Retrieve detailed information about a specific Bincode session.",
664
+ tags: ["Session"],
665
+ operationId: "session.get",
666
+ responses: {
667
+ 200: {
668
+ description: "Get session",
669
+ content: {
670
+ "application/json": {
671
+ schema: resolver(Session.Info),
672
+ },
673
+ },
674
+ },
675
+ ...errors(400, 404),
676
+ },
677
+ }),
678
+ validator(
679
+ "param",
680
+ z.object({
681
+ sessionID: Session.get.schema,
682
+ }),
683
+ ),
684
+ async (c) => {
685
+ const sessionID = c.req.valid("param").sessionID
686
+ log.info("SEARCH", { url: c.req.url })
687
+ const session = await Session.get(sessionID)
688
+ return c.json(session)
689
+ },
690
+ )
691
+ .get(
692
+ "/session/:sessionID/children",
693
+ describeRoute({
694
+ summary: "Get session children",
695
+ tags: ["Session"],
696
+ description: "Retrieve all child sessions that were forked from the specified parent session.",
697
+ operationId: "session.children",
698
+ responses: {
699
+ 200: {
700
+ description: "List of children",
701
+ content: {
702
+ "application/json": {
703
+ schema: resolver(Session.Info.array()),
704
+ },
705
+ },
706
+ },
707
+ ...errors(400, 404),
708
+ },
709
+ }),
710
+ validator(
711
+ "param",
712
+ z.object({
713
+ sessionID: Session.children.schema,
714
+ }),
715
+ ),
716
+ async (c) => {
717
+ const sessionID = c.req.valid("param").sessionID
718
+ const session = await Session.children(sessionID)
719
+ return c.json(session)
720
+ },
721
+ )
722
+ .get(
723
+ "/session/:sessionID/todo",
724
+ describeRoute({
725
+ summary: "Get session todos",
726
+ description: "Retrieve the todo list associated with a specific session, showing tasks and action items.",
727
+ operationId: "session.todo",
728
+ responses: {
729
+ 200: {
730
+ description: "Todo list",
731
+ content: {
732
+ "application/json": {
733
+ schema: resolver(Todo.Info.array()),
734
+ },
735
+ },
736
+ },
737
+ ...errors(400, 404),
738
+ },
739
+ }),
740
+ validator(
741
+ "param",
742
+ z.object({
743
+ sessionID: z.string().meta({ description: "Session ID" }),
744
+ }),
745
+ ),
746
+ async (c) => {
747
+ const sessionID = c.req.valid("param").sessionID
748
+ const todos = await Todo.get(sessionID)
749
+ return c.json(todos)
750
+ },
751
+ )
752
+ .post(
753
+ "/session",
754
+ describeRoute({
755
+ summary: "Create session",
756
+ description: "Create a new Bincode session for interacting with AI assistants and managing conversations.",
757
+ operationId: "session.create",
758
+ responses: {
759
+ ...errors(400),
760
+ 200: {
761
+ description: "Successfully created session",
762
+ content: {
763
+ "application/json": {
764
+ schema: resolver(Session.Info),
765
+ },
766
+ },
767
+ },
768
+ },
769
+ }),
770
+ validator("json", Session.create.schema.optional()),
771
+ async (c) => {
772
+ const body = c.req.valid("json") ?? {}
773
+ const session = await Session.create(body)
774
+ return c.json(session)
775
+ },
776
+ )
777
+ .delete(
778
+ "/session/:sessionID",
779
+ describeRoute({
780
+ summary: "Delete session",
781
+ description: "Delete a session and permanently remove all associated data, including messages and history.",
782
+ operationId: "session.delete",
783
+ responses: {
784
+ 200: {
785
+ description: "Successfully deleted session",
786
+ content: {
787
+ "application/json": {
788
+ schema: resolver(z.boolean()),
789
+ },
790
+ },
791
+ },
792
+ ...errors(400, 404),
793
+ },
794
+ }),
795
+ validator(
796
+ "param",
797
+ z.object({
798
+ sessionID: Session.remove.schema,
799
+ }),
800
+ ),
801
+ async (c) => {
802
+ const sessionID = c.req.valid("param").sessionID
803
+ await Session.remove(sessionID)
804
+ return c.json(true)
805
+ },
806
+ )
807
+ .patch(
808
+ "/session/:sessionID",
809
+ describeRoute({
810
+ summary: "Update session",
811
+ description: "Update properties of an existing session, such as title or other metadata.",
812
+ operationId: "session.update",
813
+ responses: {
814
+ 200: {
815
+ description: "Successfully updated session",
816
+ content: {
817
+ "application/json": {
818
+ schema: resolver(Session.Info),
819
+ },
820
+ },
821
+ },
822
+ ...errors(400, 404),
823
+ },
824
+ }),
825
+ validator(
826
+ "param",
827
+ z.object({
828
+ sessionID: z.string(),
829
+ }),
830
+ ),
831
+ validator(
832
+ "json",
833
+ z.object({
834
+ title: z.string().optional(),
835
+ time: z
836
+ .object({
837
+ archived: z.number().optional(),
838
+ })
839
+ .optional(),
840
+ }),
841
+ ),
842
+ async (c) => {
843
+ const sessionID = c.req.valid("param").sessionID
844
+ const updates = c.req.valid("json")
845
+
846
+ const updatedSession = await Session.update(sessionID, (session) => {
847
+ if (updates.title !== undefined) {
848
+ session.title = updates.title
849
+ }
850
+ if (updates.time?.archived !== undefined) session.time.archived = updates.time.archived
851
+ })
852
+
853
+ return c.json(updatedSession)
854
+ },
855
+ )
856
+ .post(
857
+ "/session/:sessionID/init",
858
+ describeRoute({
859
+ summary: "Initialize session",
860
+ description:
861
+ "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.",
862
+ operationId: "session.init",
863
+ responses: {
864
+ 200: {
865
+ description: "200",
866
+ content: {
867
+ "application/json": {
868
+ schema: resolver(z.boolean()),
869
+ },
870
+ },
871
+ },
872
+ ...errors(400, 404),
873
+ },
874
+ }),
875
+ validator(
876
+ "param",
877
+ z.object({
878
+ sessionID: z.string().meta({ description: "Session ID" }),
879
+ }),
880
+ ),
881
+ validator("json", Session.initialize.schema.omit({ sessionID: true })),
882
+ async (c) => {
883
+ const sessionID = c.req.valid("param").sessionID
884
+ const body = c.req.valid("json")
885
+ await Session.initialize({ ...body, sessionID })
886
+ return c.json(true)
887
+ },
888
+ )
889
+ .post(
890
+ "/session/:sessionID/fork",
891
+ describeRoute({
892
+ summary: "Fork session",
893
+ description: "Create a new session by forking an existing session at a specific message point.",
894
+ operationId: "session.fork",
895
+ responses: {
896
+ 200: {
897
+ description: "200",
898
+ content: {
899
+ "application/json": {
900
+ schema: resolver(Session.Info),
901
+ },
902
+ },
903
+ },
904
+ },
905
+ }),
906
+ validator(
907
+ "param",
908
+ z.object({
909
+ sessionID: Session.fork.schema.shape.sessionID,
910
+ }),
911
+ ),
912
+ validator("json", Session.fork.schema.omit({ sessionID: true })),
913
+ async (c) => {
914
+ const sessionID = c.req.valid("param").sessionID
915
+ const body = c.req.valid("json")
916
+ const result = await Session.fork({ ...body, sessionID })
917
+ return c.json(result)
918
+ },
919
+ )
920
+ .post(
921
+ "/session/:sessionID/abort",
922
+ describeRoute({
923
+ summary: "Abort session",
924
+ description: "Abort an active session and stop any ongoing AI processing or command execution.",
925
+ operationId: "session.abort",
926
+ responses: {
927
+ 200: {
928
+ description: "Aborted session",
929
+ content: {
930
+ "application/json": {
931
+ schema: resolver(z.boolean()),
932
+ },
933
+ },
934
+ },
935
+ ...errors(400, 404),
936
+ },
937
+ }),
938
+ validator(
939
+ "param",
940
+ z.object({
941
+ sessionID: z.string(),
942
+ }),
943
+ ),
944
+ async (c) => {
945
+ SessionPrompt.cancel(c.req.valid("param").sessionID)
946
+ return c.json(true)
947
+ },
948
+ )
949
+ .post(
950
+ "/session/:sessionID/share",
951
+ describeRoute({
952
+ summary: "Share session",
953
+ description: "Create a shareable link for a session, allowing others to view the conversation.",
954
+ operationId: "session.share",
955
+ responses: {
956
+ 200: {
957
+ description: "Successfully shared session",
958
+ content: {
959
+ "application/json": {
960
+ schema: resolver(Session.Info),
961
+ },
962
+ },
963
+ },
964
+ ...errors(400, 404),
965
+ },
966
+ }),
967
+ validator(
968
+ "param",
969
+ z.object({
970
+ sessionID: z.string(),
971
+ }),
972
+ ),
973
+ async (c) => {
974
+ const sessionID = c.req.valid("param").sessionID
975
+ await Session.share(sessionID)
976
+ const session = await Session.get(sessionID)
977
+ return c.json(session)
978
+ },
979
+ )
980
+ .get(
981
+ "/session/:sessionID/diff",
982
+ describeRoute({
983
+ summary: "Get message diff",
984
+ description: "Get the file changes (diff) that resulted from a specific user message in the session.",
985
+ operationId: "session.diff",
986
+ responses: {
987
+ 200: {
988
+ description: "Successfully retrieved diff",
989
+ content: {
990
+ "application/json": {
991
+ schema: resolver(Snapshot.FileDiff.array()),
992
+ },
993
+ },
994
+ },
995
+ },
996
+ }),
997
+ validator(
998
+ "param",
999
+ z.object({
1000
+ sessionID: SessionSummary.diff.schema.shape.sessionID,
1001
+ }),
1002
+ ),
1003
+ validator(
1004
+ "query",
1005
+ z.object({
1006
+ messageID: SessionSummary.diff.schema.shape.messageID,
1007
+ }),
1008
+ ),
1009
+ async (c) => {
1010
+ const query = c.req.valid("query")
1011
+ const params = c.req.valid("param")
1012
+ const result = await SessionSummary.diff({
1013
+ sessionID: params.sessionID,
1014
+ messageID: query.messageID,
1015
+ })
1016
+ return c.json(result)
1017
+ },
1018
+ )
1019
+ .delete(
1020
+ "/session/:sessionID/share",
1021
+ describeRoute({
1022
+ summary: "Unshare session",
1023
+ description: "Remove the shareable link for a session, making it private again.",
1024
+ operationId: "session.unshare",
1025
+ responses: {
1026
+ 200: {
1027
+ description: "Successfully unshared session",
1028
+ content: {
1029
+ "application/json": {
1030
+ schema: resolver(Session.Info),
1031
+ },
1032
+ },
1033
+ },
1034
+ ...errors(400, 404),
1035
+ },
1036
+ }),
1037
+ validator(
1038
+ "param",
1039
+ z.object({
1040
+ sessionID: Session.unshare.schema,
1041
+ }),
1042
+ ),
1043
+ async (c) => {
1044
+ const sessionID = c.req.valid("param").sessionID
1045
+ await Session.unshare(sessionID)
1046
+ const session = await Session.get(sessionID)
1047
+ return c.json(session)
1048
+ },
1049
+ )
1050
+ .post(
1051
+ "/session/:sessionID/summarize",
1052
+ describeRoute({
1053
+ summary: "Summarize session",
1054
+ description: "Generate a concise summary of the session using AI compaction to preserve key information.",
1055
+ operationId: "session.summarize",
1056
+ responses: {
1057
+ 200: {
1058
+ description: "Summarized session",
1059
+ content: {
1060
+ "application/json": {
1061
+ schema: resolver(z.boolean()),
1062
+ },
1063
+ },
1064
+ },
1065
+ ...errors(400, 404),
1066
+ },
1067
+ }),
1068
+ validator(
1069
+ "param",
1070
+ z.object({
1071
+ sessionID: z.string().meta({ description: "Session ID" }),
1072
+ }),
1073
+ ),
1074
+ validator(
1075
+ "json",
1076
+ z.object({
1077
+ providerID: z.string(),
1078
+ modelID: z.string(),
1079
+ auto: z.boolean().optional().default(false),
1080
+ }),
1081
+ ),
1082
+ async (c) => {
1083
+ const sessionID = c.req.valid("param").sessionID
1084
+ const body = c.req.valid("json")
1085
+ const msgs = await Session.messages({ sessionID })
1086
+ let currentAgent = await Agent.defaultAgent()
1087
+ for (let i = msgs.length - 1; i >= 0; i--) {
1088
+ const info = msgs[i].info
1089
+ if (info.role === "user") {
1090
+ currentAgent = info.agent || (await Agent.defaultAgent())
1091
+ break
1092
+ }
1093
+ }
1094
+ await SessionCompaction.create({
1095
+ sessionID,
1096
+ agent: currentAgent,
1097
+ model: {
1098
+ providerID: body.providerID,
1099
+ modelID: body.modelID,
1100
+ },
1101
+ auto: body.auto,
1102
+ })
1103
+ await SessionPrompt.loop(sessionID)
1104
+ return c.json(true)
1105
+ },
1106
+ )
1107
+ .get(
1108
+ "/session/:sessionID/message",
1109
+ describeRoute({
1110
+ summary: "Get session messages",
1111
+ description: "Retrieve all messages in a session, including user prompts and AI responses.",
1112
+ operationId: "session.messages",
1113
+ responses: {
1114
+ 200: {
1115
+ description: "List of messages",
1116
+ content: {
1117
+ "application/json": {
1118
+ schema: resolver(MessageV2.WithParts.array()),
1119
+ },
1120
+ },
1121
+ },
1122
+ ...errors(400, 404),
1123
+ },
1124
+ }),
1125
+ validator(
1126
+ "param",
1127
+ z.object({
1128
+ sessionID: z.string().meta({ description: "Session ID" }),
1129
+ }),
1130
+ ),
1131
+ validator(
1132
+ "query",
1133
+ z.object({
1134
+ limit: z.coerce.number().optional(),
1135
+ }),
1136
+ ),
1137
+ async (c) => {
1138
+ const query = c.req.valid("query")
1139
+ const messages = await Session.messages({
1140
+ sessionID: c.req.valid("param").sessionID,
1141
+ limit: query.limit,
1142
+ })
1143
+ return c.json(messages)
1144
+ },
1145
+ )
1146
+ .get(
1147
+ "/session/:sessionID/diff",
1148
+ describeRoute({
1149
+ summary: "Get session diff",
1150
+ description: "Get all file changes (diffs) made during this session.",
1151
+ operationId: "session.diff",
1152
+ responses: {
1153
+ 200: {
1154
+ description: "List of diffs",
1155
+ content: {
1156
+ "application/json": {
1157
+ schema: resolver(Snapshot.FileDiff.array()),
1158
+ },
1159
+ },
1160
+ },
1161
+ ...errors(400, 404),
1162
+ },
1163
+ }),
1164
+ validator(
1165
+ "param",
1166
+ z.object({
1167
+ sessionID: z.string().meta({ description: "Session ID" }),
1168
+ }),
1169
+ ),
1170
+ async (c) => {
1171
+ const diff = await Session.diff(c.req.valid("param").sessionID)
1172
+ return c.json(diff)
1173
+ },
1174
+ )
1175
+ .get(
1176
+ "/session/:sessionID/message/:messageID",
1177
+ describeRoute({
1178
+ summary: "Get message",
1179
+ description: "Retrieve a specific message from a session by its message ID.",
1180
+ operationId: "session.message",
1181
+ responses: {
1182
+ 200: {
1183
+ description: "Message",
1184
+ content: {
1185
+ "application/json": {
1186
+ schema: resolver(
1187
+ z.object({
1188
+ info: MessageV2.Info,
1189
+ parts: MessageV2.Part.array(),
1190
+ }),
1191
+ ),
1192
+ },
1193
+ },
1194
+ },
1195
+ ...errors(400, 404),
1196
+ },
1197
+ }),
1198
+ validator(
1199
+ "param",
1200
+ z.object({
1201
+ sessionID: z.string().meta({ description: "Session ID" }),
1202
+ messageID: z.string().meta({ description: "Message ID" }),
1203
+ }),
1204
+ ),
1205
+ async (c) => {
1206
+ const params = c.req.valid("param")
1207
+ const message = await MessageV2.get({
1208
+ sessionID: params.sessionID,
1209
+ messageID: params.messageID,
1210
+ })
1211
+ return c.json(message)
1212
+ },
1213
+ )
1214
+ .delete(
1215
+ "/session/:sessionID/message/:messageID/part/:partID",
1216
+ describeRoute({
1217
+ description: "Delete a part from a message",
1218
+ operationId: "part.delete",
1219
+ responses: {
1220
+ 200: {
1221
+ description: "Successfully deleted part",
1222
+ content: {
1223
+ "application/json": {
1224
+ schema: resolver(z.boolean()),
1225
+ },
1226
+ },
1227
+ },
1228
+ ...errors(400, 404),
1229
+ },
1230
+ }),
1231
+ validator(
1232
+ "param",
1233
+ z.object({
1234
+ sessionID: z.string().meta({ description: "Session ID" }),
1235
+ messageID: z.string().meta({ description: "Message ID" }),
1236
+ partID: z.string().meta({ description: "Part ID" }),
1237
+ }),
1238
+ ),
1239
+ async (c) => {
1240
+ const params = c.req.valid("param")
1241
+ await Session.removePart({
1242
+ sessionID: params.sessionID,
1243
+ messageID: params.messageID,
1244
+ partID: params.partID,
1245
+ })
1246
+ return c.json(true)
1247
+ },
1248
+ )
1249
+ .patch(
1250
+ "/session/:sessionID/message/:messageID/part/:partID",
1251
+ describeRoute({
1252
+ description: "Update a part in a message",
1253
+ operationId: "part.update",
1254
+ responses: {
1255
+ 200: {
1256
+ description: "Successfully updated part",
1257
+ content: {
1258
+ "application/json": {
1259
+ schema: resolver(MessageV2.Part),
1260
+ },
1261
+ },
1262
+ },
1263
+ ...errors(400, 404),
1264
+ },
1265
+ }),
1266
+ validator(
1267
+ "param",
1268
+ z.object({
1269
+ sessionID: z.string().meta({ description: "Session ID" }),
1270
+ messageID: z.string().meta({ description: "Message ID" }),
1271
+ partID: z.string().meta({ description: "Part ID" }),
1272
+ }),
1273
+ ),
1274
+ validator("json", MessageV2.Part),
1275
+ async (c) => {
1276
+ const params = c.req.valid("param")
1277
+ const body = c.req.valid("json")
1278
+ if (body.id !== params.partID || body.messageID !== params.messageID || body.sessionID !== params.sessionID) {
1279
+ throw new Error(
1280
+ `Part mismatch: body.id='${body.id}' vs partID='${params.partID}', body.messageID='${body.messageID}' vs messageID='${params.messageID}', body.sessionID='${body.sessionID}' vs sessionID='${params.sessionID}'`,
1281
+ )
1282
+ }
1283
+ const part = await Session.updatePart(body)
1284
+ return c.json(part)
1285
+ },
1286
+ )
1287
+ .post(
1288
+ "/session/:sessionID/message",
1289
+ describeRoute({
1290
+ summary: "Send message",
1291
+ description: "Create and send a new message to a session, streaming the AI response.",
1292
+ operationId: "session.prompt",
1293
+ responses: {
1294
+ 200: {
1295
+ description: "Created message",
1296
+ content: {
1297
+ "application/json": {
1298
+ schema: resolver(
1299
+ z.object({
1300
+ info: MessageV2.Assistant,
1301
+ parts: MessageV2.Part.array(),
1302
+ }),
1303
+ ),
1304
+ },
1305
+ },
1306
+ },
1307
+ ...errors(400, 404),
1308
+ },
1309
+ }),
1310
+ validator(
1311
+ "param",
1312
+ z.object({
1313
+ sessionID: z.string().meta({ description: "Session ID" }),
1314
+ }),
1315
+ ),
1316
+ validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
1317
+ async (c) => {
1318
+ c.status(200)
1319
+ c.header("Content-Type", "application/json")
1320
+ return stream(c, async (stream) => {
1321
+ const sessionID = c.req.valid("param").sessionID
1322
+ const body = c.req.valid("json")
1323
+ const msg = await SessionPrompt.prompt({ ...body, sessionID })
1324
+ stream.write(JSON.stringify(msg))
1325
+ })
1326
+ },
1327
+ )
1328
+ .post(
1329
+ "/session/:sessionID/prompt_async",
1330
+ describeRoute({
1331
+ summary: "Send async message",
1332
+ description:
1333
+ "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.",
1334
+ operationId: "session.prompt_async",
1335
+ responses: {
1336
+ 204: {
1337
+ description: "Prompt accepted",
1338
+ },
1339
+ ...errors(400, 404),
1340
+ },
1341
+ }),
1342
+ validator(
1343
+ "param",
1344
+ z.object({
1345
+ sessionID: z.string().meta({ description: "Session ID" }),
1346
+ }),
1347
+ ),
1348
+ validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
1349
+ async (c) => {
1350
+ c.status(204)
1351
+ c.header("Content-Type", "application/json")
1352
+ return stream(c, async () => {
1353
+ const sessionID = c.req.valid("param").sessionID
1354
+ const body = c.req.valid("json")
1355
+ SessionPrompt.prompt({ ...body, sessionID })
1356
+ })
1357
+ },
1358
+ )
1359
+ .post(
1360
+ "/session/:sessionID/command",
1361
+ describeRoute({
1362
+ summary: "Send command",
1363
+ description: "Send a new command to a session for execution by the AI assistant.",
1364
+ operationId: "session.command",
1365
+ responses: {
1366
+ 200: {
1367
+ description: "Created message",
1368
+ content: {
1369
+ "application/json": {
1370
+ schema: resolver(
1371
+ z.object({
1372
+ info: MessageV2.Assistant,
1373
+ parts: MessageV2.Part.array(),
1374
+ }),
1375
+ ),
1376
+ },
1377
+ },
1378
+ },
1379
+ ...errors(400, 404),
1380
+ },
1381
+ }),
1382
+ validator(
1383
+ "param",
1384
+ z.object({
1385
+ sessionID: z.string().meta({ description: "Session ID" }),
1386
+ }),
1387
+ ),
1388
+ validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
1389
+ async (c) => {
1390
+ const sessionID = c.req.valid("param").sessionID
1391
+ const body = c.req.valid("json")
1392
+ const msg = await SessionPrompt.command({ ...body, sessionID })
1393
+ return c.json(msg)
1394
+ },
1395
+ )
1396
+ .post(
1397
+ "/session/:sessionID/shell",
1398
+ describeRoute({
1399
+ summary: "Run shell command",
1400
+ description: "Execute a shell command within the session context and return the AI's response.",
1401
+ operationId: "session.shell",
1402
+ responses: {
1403
+ 200: {
1404
+ description: "Created message",
1405
+ content: {
1406
+ "application/json": {
1407
+ schema: resolver(MessageV2.Assistant),
1408
+ },
1409
+ },
1410
+ },
1411
+ ...errors(400, 404),
1412
+ },
1413
+ }),
1414
+ validator(
1415
+ "param",
1416
+ z.object({
1417
+ sessionID: z.string().meta({ description: "Session ID" }),
1418
+ }),
1419
+ ),
1420
+ validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
1421
+ async (c) => {
1422
+ const sessionID = c.req.valid("param").sessionID
1423
+ const body = c.req.valid("json")
1424
+ const msg = await SessionPrompt.shell({ ...body, sessionID })
1425
+ return c.json(msg)
1426
+ },
1427
+ )
1428
+ .post(
1429
+ "/session/:sessionID/revert",
1430
+ describeRoute({
1431
+ summary: "Revert message",
1432
+ description: "Revert a specific message in a session, undoing its effects and restoring the previous state.",
1433
+ operationId: "session.revert",
1434
+ responses: {
1435
+ 200: {
1436
+ description: "Updated session",
1437
+ content: {
1438
+ "application/json": {
1439
+ schema: resolver(Session.Info),
1440
+ },
1441
+ },
1442
+ },
1443
+ ...errors(400, 404),
1444
+ },
1445
+ }),
1446
+ validator(
1447
+ "param",
1448
+ z.object({
1449
+ sessionID: z.string(),
1450
+ }),
1451
+ ),
1452
+ validator("json", SessionRevert.RevertInput.omit({ sessionID: true })),
1453
+ async (c) => {
1454
+ const sessionID = c.req.valid("param").sessionID
1455
+ log.info("revert", c.req.valid("json"))
1456
+ const session = await SessionRevert.revert({
1457
+ sessionID,
1458
+ ...c.req.valid("json"),
1459
+ })
1460
+ return c.json(session)
1461
+ },
1462
+ )
1463
+ .post(
1464
+ "/session/:sessionID/unrevert",
1465
+ describeRoute({
1466
+ summary: "Restore reverted messages",
1467
+ description: "Restore all previously reverted messages in a session.",
1468
+ operationId: "session.unrevert",
1469
+ responses: {
1470
+ 200: {
1471
+ description: "Updated session",
1472
+ content: {
1473
+ "application/json": {
1474
+ schema: resolver(Session.Info),
1475
+ },
1476
+ },
1477
+ },
1478
+ ...errors(400, 404),
1479
+ },
1480
+ }),
1481
+ validator(
1482
+ "param",
1483
+ z.object({
1484
+ sessionID: z.string(),
1485
+ }),
1486
+ ),
1487
+ async (c) => {
1488
+ const sessionID = c.req.valid("param").sessionID
1489
+ const session = await SessionRevert.unrevert({ sessionID })
1490
+ return c.json(session)
1491
+ },
1492
+ )
1493
+ .post(
1494
+ "/session/:sessionID/permissions/:permissionID",
1495
+ describeRoute({
1496
+ summary: "Respond to permission",
1497
+ description: "Approve or deny a permission request from the AI assistant.",
1498
+ operationId: "permission.respond",
1499
+ responses: {
1500
+ 200: {
1501
+ description: "Permission processed successfully",
1502
+ content: {
1503
+ "application/json": {
1504
+ schema: resolver(z.boolean()),
1505
+ },
1506
+ },
1507
+ },
1508
+ ...errors(400, 404),
1509
+ },
1510
+ }),
1511
+ validator(
1512
+ "param",
1513
+ z.object({
1514
+ sessionID: z.string(),
1515
+ permissionID: z.string(),
1516
+ }),
1517
+ ),
1518
+ validator("json", z.object({ response: Permission.Response })),
1519
+ async (c) => {
1520
+ const params = c.req.valid("param")
1521
+ const sessionID = params.sessionID
1522
+ const permissionID = params.permissionID
1523
+ Permission.respond({
1524
+ sessionID,
1525
+ permissionID,
1526
+ response: c.req.valid("json").response,
1527
+ })
1528
+ return c.json(true)
1529
+ },
1530
+ )
1531
+ .get(
1532
+ "/command",
1533
+ describeRoute({
1534
+ summary: "List commands",
1535
+ description: "Get a list of all available commands in the Bincode system.",
1536
+ operationId: "command.list",
1537
+ responses: {
1538
+ 200: {
1539
+ description: "List of commands",
1540
+ content: {
1541
+ "application/json": {
1542
+ schema: resolver(Command.Info.array()),
1543
+ },
1544
+ },
1545
+ },
1546
+ },
1547
+ }),
1548
+ async (c) => {
1549
+ const commands = await Command.list()
1550
+ return c.json(commands)
1551
+ },
1552
+ )
1553
+ .get(
1554
+ "/config/providers",
1555
+ describeRoute({
1556
+ summary: "List config providers",
1557
+ description: "Get a list of all configured AI providers and their default models.",
1558
+ operationId: "config.providers",
1559
+ responses: {
1560
+ 200: {
1561
+ description: "List of providers",
1562
+ content: {
1563
+ "application/json": {
1564
+ schema: resolver(
1565
+ z.object({
1566
+ providers: Provider.Info.array(),
1567
+ default: z.record(z.string(), z.string()),
1568
+ }),
1569
+ ),
1570
+ },
1571
+ },
1572
+ },
1573
+ },
1574
+ }),
1575
+ async (c) => {
1576
+ using _ = log.time("providers")
1577
+ const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
1578
+ return c.json({
1579
+ providers: Object.values(providers),
1580
+ default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
1581
+ })
1582
+ },
1583
+ )
1584
+ .get(
1585
+ "/provider",
1586
+ describeRoute({
1587
+ summary: "List providers",
1588
+ description: "Get a list of all available AI providers, including both available and connected ones.",
1589
+ operationId: "provider.list",
1590
+ responses: {
1591
+ 200: {
1592
+ description: "List of providers",
1593
+ content: {
1594
+ "application/json": {
1595
+ schema: resolver(
1596
+ z.object({
1597
+ all: ModelsDev.Provider.array(),
1598
+ default: z.record(z.string(), z.string()),
1599
+ connected: z.array(z.string()),
1600
+ }),
1601
+ ),
1602
+ },
1603
+ },
1604
+ },
1605
+ },
1606
+ }),
1607
+ async (c) => {
1608
+ const config = await Config.get()
1609
+ const disabled = new Set(config.disabled_providers ?? [])
1610
+ const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
1611
+
1612
+ const allProviders = await ModelsDev.get()
1613
+ const filteredProviders: Record<string, (typeof allProviders)[string]> = {}
1614
+ for (const [key, value] of Object.entries(allProviders)) {
1615
+ if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
1616
+ filteredProviders[key] = value
1617
+ }
1618
+ }
1619
+
1620
+ const connected = await Provider.list()
1621
+ const providers = Object.assign(
1622
+ mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)),
1623
+ connected,
1624
+ )
1625
+ return c.json({
1626
+ all: Object.values(providers),
1627
+ default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
1628
+ connected: Object.keys(connected),
1629
+ })
1630
+ },
1631
+ )
1632
+ .get(
1633
+ "/provider/auth",
1634
+ describeRoute({
1635
+ summary: "Get provider auth methods",
1636
+ description: "Retrieve available authentication methods for all AI providers.",
1637
+ operationId: "provider.auth",
1638
+ responses: {
1639
+ 200: {
1640
+ description: "Provider auth methods",
1641
+ content: {
1642
+ "application/json": {
1643
+ schema: resolver(z.record(z.string(), z.array(ProviderAuth.Method))),
1644
+ },
1645
+ },
1646
+ },
1647
+ },
1648
+ }),
1649
+ async (c) => {
1650
+ return c.json(await ProviderAuth.methods())
1651
+ },
1652
+ )
1653
+ .post(
1654
+ "/provider/:providerID/oauth/authorize",
1655
+ describeRoute({
1656
+ summary: "OAuth authorize",
1657
+ description: "Initiate OAuth authorization for a specific AI provider to get an authorization URL.",
1658
+ operationId: "provider.oauth.authorize",
1659
+ responses: {
1660
+ 200: {
1661
+ description: "Authorization URL and method",
1662
+ content: {
1663
+ "application/json": {
1664
+ schema: resolver(ProviderAuth.Authorization.optional()),
1665
+ },
1666
+ },
1667
+ },
1668
+ ...errors(400),
1669
+ },
1670
+ }),
1671
+ validator(
1672
+ "param",
1673
+ z.object({
1674
+ providerID: z.string().meta({ description: "Provider ID" }),
1675
+ }),
1676
+ ),
1677
+ validator(
1678
+ "json",
1679
+ z.object({
1680
+ method: z.number().meta({ description: "Auth method index" }),
1681
+ }),
1682
+ ),
1683
+ async (c) => {
1684
+ const providerID = c.req.valid("param").providerID
1685
+ const { method } = c.req.valid("json")
1686
+ const result = await ProviderAuth.authorize({
1687
+ providerID,
1688
+ method,
1689
+ })
1690
+ return c.json(result)
1691
+ },
1692
+ )
1693
+ .post(
1694
+ "/provider/:providerID/oauth/callback",
1695
+ describeRoute({
1696
+ summary: "OAuth callback",
1697
+ description: "Handle the OAuth callback from a provider after user authorization.",
1698
+ operationId: "provider.oauth.callback",
1699
+ responses: {
1700
+ 200: {
1701
+ description: "OAuth callback processed successfully",
1702
+ content: {
1703
+ "application/json": {
1704
+ schema: resolver(z.boolean()),
1705
+ },
1706
+ },
1707
+ },
1708
+ ...errors(400),
1709
+ },
1710
+ }),
1711
+ validator(
1712
+ "param",
1713
+ z.object({
1714
+ providerID: z.string().meta({ description: "Provider ID" }),
1715
+ }),
1716
+ ),
1717
+ validator(
1718
+ "json",
1719
+ z.object({
1720
+ method: z.number().meta({ description: "Auth method index" }),
1721
+ code: z.string().optional().meta({ description: "OAuth authorization code" }),
1722
+ }),
1723
+ ),
1724
+ async (c) => {
1725
+ const providerID = c.req.valid("param").providerID
1726
+ const { method, code } = c.req.valid("json")
1727
+ await ProviderAuth.callback({
1728
+ providerID,
1729
+ method,
1730
+ code,
1731
+ })
1732
+ return c.json(true)
1733
+ },
1734
+ )
1735
+ .get(
1736
+ "/find",
1737
+ describeRoute({
1738
+ summary: "Find text",
1739
+ description: "Search for text patterns across files in the project using ripgrep.",
1740
+ operationId: "find.text",
1741
+ responses: {
1742
+ 200: {
1743
+ description: "Matches",
1744
+ content: {
1745
+ "application/json": {
1746
+ schema: resolver(Ripgrep.Match.shape.data.array()),
1747
+ },
1748
+ },
1749
+ },
1750
+ },
1751
+ }),
1752
+ validator(
1753
+ "query",
1754
+ z.object({
1755
+ pattern: z.string(),
1756
+ }),
1757
+ ),
1758
+ async (c) => {
1759
+ const pattern = c.req.valid("query").pattern
1760
+ const result = await Ripgrep.search({
1761
+ cwd: Instance.directory,
1762
+ pattern,
1763
+ limit: 10,
1764
+ })
1765
+ return c.json(result)
1766
+ },
1767
+ )
1768
+ .get(
1769
+ "/find/file",
1770
+ describeRoute({
1771
+ summary: "Find files",
1772
+ description: "Search for files by name or pattern in the project directory.",
1773
+ operationId: "find.files",
1774
+ responses: {
1775
+ 200: {
1776
+ description: "File paths",
1777
+ content: {
1778
+ "application/json": {
1779
+ schema: resolver(z.string().array()),
1780
+ },
1781
+ },
1782
+ },
1783
+ },
1784
+ }),
1785
+ validator(
1786
+ "query",
1787
+ z.object({
1788
+ query: z.string(),
1789
+ dirs: z.enum(["true", "false"]).optional(),
1790
+ }),
1791
+ ),
1792
+ async (c) => {
1793
+ const query = c.req.valid("query").query
1794
+ const dirs = c.req.valid("query").dirs
1795
+ const results = await File.search({
1796
+ query,
1797
+ limit: 10,
1798
+ dirs: dirs !== "false",
1799
+ })
1800
+ return c.json(results)
1801
+ },
1802
+ )
1803
+ .get(
1804
+ "/find/symbol",
1805
+ describeRoute({
1806
+ summary: "Find symbols",
1807
+ description: "Search for workspace symbols like functions, classes, and variables using LSP.",
1808
+ operationId: "find.symbols",
1809
+ responses: {
1810
+ 200: {
1811
+ description: "Symbols",
1812
+ content: {
1813
+ "application/json": {
1814
+ schema: resolver(LSP.Symbol.array()),
1815
+ },
1816
+ },
1817
+ },
1818
+ },
1819
+ }),
1820
+ validator(
1821
+ "query",
1822
+ z.object({
1823
+ query: z.string(),
1824
+ }),
1825
+ ),
1826
+ async (c) => {
1827
+ /*
1828
+ const query = c.req.valid("query").query
1829
+ const result = await LSP.workspaceSymbol(query)
1830
+ return c.json(result)
1831
+ */
1832
+ return c.json([])
1833
+ },
1834
+ )
1835
+ .get(
1836
+ "/file",
1837
+ describeRoute({
1838
+ summary: "List files",
1839
+ description: "List files and directories in a specified path.",
1840
+ operationId: "file.list",
1841
+ responses: {
1842
+ 200: {
1843
+ description: "Files and directories",
1844
+ content: {
1845
+ "application/json": {
1846
+ schema: resolver(File.Node.array()),
1847
+ },
1848
+ },
1849
+ },
1850
+ },
1851
+ }),
1852
+ validator(
1853
+ "query",
1854
+ z.object({
1855
+ path: z.string(),
1856
+ }),
1857
+ ),
1858
+ async (c) => {
1859
+ const path = c.req.valid("query").path
1860
+ const content = await File.list(path)
1861
+ return c.json(content)
1862
+ },
1863
+ )
1864
+ .get(
1865
+ "/file/content",
1866
+ describeRoute({
1867
+ summary: "Read file",
1868
+ description: "Read the content of a specified file.",
1869
+ operationId: "file.read",
1870
+ responses: {
1871
+ 200: {
1872
+ description: "File content",
1873
+ content: {
1874
+ "application/json": {
1875
+ schema: resolver(File.Content),
1876
+ },
1877
+ },
1878
+ },
1879
+ },
1880
+ }),
1881
+ validator(
1882
+ "query",
1883
+ z.object({
1884
+ path: z.string(),
1885
+ }),
1886
+ ),
1887
+ async (c) => {
1888
+ const path = c.req.valid("query").path
1889
+ const content = await File.read(path)
1890
+ return c.json(content)
1891
+ },
1892
+ )
1893
+ .get(
1894
+ "/file/status",
1895
+ describeRoute({
1896
+ summary: "Get file status",
1897
+ description: "Get the git status of all files in the project.",
1898
+ operationId: "file.status",
1899
+ responses: {
1900
+ 200: {
1901
+ description: "File status",
1902
+ content: {
1903
+ "application/json": {
1904
+ schema: resolver(File.Info.array()),
1905
+ },
1906
+ },
1907
+ },
1908
+ },
1909
+ }),
1910
+ async (c) => {
1911
+ const content = await File.status()
1912
+ return c.json(content)
1913
+ },
1914
+ )
1915
+ .post(
1916
+ "/log",
1917
+ describeRoute({
1918
+ summary: "Write log",
1919
+ description: "Write a log entry to the server logs with specified level and metadata.",
1920
+ operationId: "app.log",
1921
+ responses: {
1922
+ 200: {
1923
+ description: "Log entry written successfully",
1924
+ content: {
1925
+ "application/json": {
1926
+ schema: resolver(z.boolean()),
1927
+ },
1928
+ },
1929
+ },
1930
+ ...errors(400),
1931
+ },
1932
+ }),
1933
+ validator(
1934
+ "json",
1935
+ z.object({
1936
+ service: z.string().meta({ description: "Service name for the log entry" }),
1937
+ level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
1938
+ message: z.string().meta({ description: "Log message" }),
1939
+ extra: z
1940
+ .record(z.string(), z.any())
1941
+ .optional()
1942
+ .meta({ description: "Additional metadata for the log entry" }),
1943
+ }),
1944
+ ),
1945
+ async (c) => {
1946
+ const { service, level, message, extra } = c.req.valid("json")
1947
+ const logger = Log.create({ service })
1948
+
1949
+ switch (level) {
1950
+ case "debug":
1951
+ logger.debug(message, extra)
1952
+ break
1953
+ case "info":
1954
+ logger.info(message, extra)
1955
+ break
1956
+ case "error":
1957
+ logger.error(message, extra)
1958
+ break
1959
+ case "warn":
1960
+ logger.warn(message, extra)
1961
+ break
1962
+ }
1963
+
1964
+ return c.json(true)
1965
+ },
1966
+ )
1967
+ .get(
1968
+ "/agent",
1969
+ describeRoute({
1970
+ summary: "List agents",
1971
+ description: "Get a list of all available AI agents in the Bincode system.",
1972
+ operationId: "app.agents",
1973
+ responses: {
1974
+ 200: {
1975
+ description: "List of agents",
1976
+ content: {
1977
+ "application/json": {
1978
+ schema: resolver(Agent.Info.array()),
1979
+ },
1980
+ },
1981
+ },
1982
+ },
1983
+ }),
1984
+ async (c) => {
1985
+ const modes = await Agent.list()
1986
+ return c.json(modes)
1987
+ },
1988
+ )
1989
+ .get(
1990
+ "/mcp",
1991
+ describeRoute({
1992
+ summary: "Get MCP status",
1993
+ description: "Get the status of all Model Context Protocol (MCP) servers.",
1994
+ operationId: "mcp.status",
1995
+ responses: {
1996
+ 200: {
1997
+ description: "MCP server status",
1998
+ content: {
1999
+ "application/json": {
2000
+ schema: resolver(z.record(z.string(), MCP.Status)),
2001
+ },
2002
+ },
2003
+ },
2004
+ },
2005
+ }),
2006
+ async (c) => {
2007
+ return c.json(await MCP.status())
2008
+ },
2009
+ )
2010
+ .post(
2011
+ "/mcp",
2012
+ describeRoute({
2013
+ summary: "Add MCP server",
2014
+ description: "Dynamically add a new Model Context Protocol (MCP) server to the system.",
2015
+ operationId: "mcp.add",
2016
+ responses: {
2017
+ 200: {
2018
+ description: "MCP server added successfully",
2019
+ content: {
2020
+ "application/json": {
2021
+ schema: resolver(z.record(z.string(), MCP.Status)),
2022
+ },
2023
+ },
2024
+ },
2025
+ ...errors(400),
2026
+ },
2027
+ }),
2028
+ validator(
2029
+ "json",
2030
+ z.object({
2031
+ name: z.string(),
2032
+ config: Config.Mcp,
2033
+ }),
2034
+ ),
2035
+ async (c) => {
2036
+ const { name, config } = c.req.valid("json")
2037
+ const result = await MCP.add(name, config)
2038
+ return c.json(result.status)
2039
+ },
2040
+ )
2041
+ .post(
2042
+ "/mcp/:name/auth",
2043
+ describeRoute({
2044
+ summary: "Start MCP OAuth",
2045
+ description: "Start OAuth authentication flow for a Model Context Protocol (MCP) server.",
2046
+ operationId: "mcp.auth.start",
2047
+ responses: {
2048
+ 200: {
2049
+ description: "OAuth flow started",
2050
+ content: {
2051
+ "application/json": {
2052
+ schema: resolver(
2053
+ z.object({
2054
+ authorizationUrl: z.string().describe("URL to open in browser for authorization"),
2055
+ }),
2056
+ ),
2057
+ },
2058
+ },
2059
+ },
2060
+ ...errors(400, 404),
2061
+ },
2062
+ }),
2063
+ async (c) => {
2064
+ const name = c.req.param("name")
2065
+ const supportsOAuth = await MCP.supportsOAuth(name)
2066
+ if (!supportsOAuth) {
2067
+ return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
2068
+ }
2069
+ const result = await MCP.startAuth(name)
2070
+ return c.json(result)
2071
+ },
2072
+ )
2073
+ .post(
2074
+ "/mcp/:name/auth/callback",
2075
+ describeRoute({
2076
+ summary: "Complete MCP OAuth",
2077
+ description:
2078
+ "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.",
2079
+ operationId: "mcp.auth.callback",
2080
+ responses: {
2081
+ 200: {
2082
+ description: "OAuth authentication completed",
2083
+ content: {
2084
+ "application/json": {
2085
+ schema: resolver(MCP.Status),
2086
+ },
2087
+ },
2088
+ },
2089
+ ...errors(400, 404),
2090
+ },
2091
+ }),
2092
+ validator(
2093
+ "json",
2094
+ z.object({
2095
+ code: z.string().describe("Authorization code from OAuth callback"),
2096
+ }),
2097
+ ),
2098
+ async (c) => {
2099
+ const name = c.req.param("name")
2100
+ const { code } = c.req.valid("json")
2101
+ const status = await MCP.finishAuth(name, code)
2102
+ return c.json(status)
2103
+ },
2104
+ )
2105
+ .post(
2106
+ "/mcp/:name/auth/authenticate",
2107
+ describeRoute({
2108
+ summary: "Authenticate MCP OAuth",
2109
+ description: "Start OAuth flow and wait for callback (opens browser)",
2110
+ operationId: "mcp.auth.authenticate",
2111
+ responses: {
2112
+ 200: {
2113
+ description: "OAuth authentication completed",
2114
+ content: {
2115
+ "application/json": {
2116
+ schema: resolver(MCP.Status),
2117
+ },
2118
+ },
2119
+ },
2120
+ ...errors(400, 404),
2121
+ },
2122
+ }),
2123
+ async (c) => {
2124
+ const name = c.req.param("name")
2125
+ const supportsOAuth = await MCP.supportsOAuth(name)
2126
+ if (!supportsOAuth) {
2127
+ return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
2128
+ }
2129
+ const status = await MCP.authenticate(name)
2130
+ return c.json(status)
2131
+ },
2132
+ )
2133
+ .delete(
2134
+ "/mcp/:name/auth",
2135
+ describeRoute({
2136
+ summary: "Remove MCP OAuth",
2137
+ description: "Remove OAuth credentials for an MCP server",
2138
+ operationId: "mcp.auth.remove",
2139
+ responses: {
2140
+ 200: {
2141
+ description: "OAuth credentials removed",
2142
+ content: {
2143
+ "application/json": {
2144
+ schema: resolver(z.object({ success: z.literal(true) })),
2145
+ },
2146
+ },
2147
+ },
2148
+ ...errors(404),
2149
+ },
2150
+ }),
2151
+ async (c) => {
2152
+ const name = c.req.param("name")
2153
+ await MCP.removeAuth(name)
2154
+ return c.json({ success: true as const })
2155
+ },
2156
+ )
2157
+ .post(
2158
+ "/mcp/:name/connect",
2159
+ describeRoute({
2160
+ description: "Connect an MCP server",
2161
+ operationId: "mcp.connect",
2162
+ responses: {
2163
+ 200: {
2164
+ description: "MCP server connected successfully",
2165
+ content: {
2166
+ "application/json": {
2167
+ schema: resolver(z.boolean()),
2168
+ },
2169
+ },
2170
+ },
2171
+ },
2172
+ }),
2173
+ validator("param", z.object({ name: z.string() })),
2174
+ async (c) => {
2175
+ const { name } = c.req.valid("param")
2176
+ await MCP.connect(name)
2177
+ return c.json(true)
2178
+ },
2179
+ )
2180
+ .post(
2181
+ "/mcp/:name/disconnect",
2182
+ describeRoute({
2183
+ description: "Disconnect an MCP server",
2184
+ operationId: "mcp.disconnect",
2185
+ responses: {
2186
+ 200: {
2187
+ description: "MCP server disconnected successfully",
2188
+ content: {
2189
+ "application/json": {
2190
+ schema: resolver(z.boolean()),
2191
+ },
2192
+ },
2193
+ },
2194
+ },
2195
+ }),
2196
+ validator("param", z.object({ name: z.string() })),
2197
+ async (c) => {
2198
+ const { name } = c.req.valid("param")
2199
+ await MCP.disconnect(name)
2200
+ return c.json(true)
2201
+ },
2202
+ )
2203
+ .get(
2204
+ "/lsp",
2205
+ describeRoute({
2206
+ summary: "Get LSP status",
2207
+ description: "Get LSP server status",
2208
+ operationId: "lsp.status",
2209
+ responses: {
2210
+ 200: {
2211
+ description: "LSP server status",
2212
+ content: {
2213
+ "application/json": {
2214
+ schema: resolver(LSP.Status.array()),
2215
+ },
2216
+ },
2217
+ },
2218
+ },
2219
+ }),
2220
+ async (c) => {
2221
+ return c.json(await LSP.status())
2222
+ },
2223
+ )
2224
+ .get(
2225
+ "/formatter",
2226
+ describeRoute({
2227
+ summary: "Get formatter status",
2228
+ description: "Get formatter status",
2229
+ operationId: "formatter.status",
2230
+ responses: {
2231
+ 200: {
2232
+ description: "Formatter status",
2233
+ content: {
2234
+ "application/json": {
2235
+ schema: resolver(Format.Status.array()),
2236
+ },
2237
+ },
2238
+ },
2239
+ },
2240
+ }),
2241
+ async (c) => {
2242
+ return c.json(await Format.status())
2243
+ },
2244
+ )
2245
+ .post(
2246
+ "/tui/append-prompt",
2247
+ describeRoute({
2248
+ summary: "Append TUI prompt",
2249
+ description: "Append prompt to the TUI",
2250
+ operationId: "tui.appendPrompt",
2251
+ responses: {
2252
+ 200: {
2253
+ description: "Prompt processed successfully",
2254
+ content: {
2255
+ "application/json": {
2256
+ schema: resolver(z.boolean()),
2257
+ },
2258
+ },
2259
+ },
2260
+ ...errors(400),
2261
+ },
2262
+ }),
2263
+ validator("json", TuiEvent.PromptAppend.properties),
2264
+ async (c) => {
2265
+ await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
2266
+ return c.json(true)
2267
+ },
2268
+ )
2269
+ .post(
2270
+ "/tui/open-help",
2271
+ describeRoute({
2272
+ summary: "Open help dialog",
2273
+ description: "Open the help dialog in the TUI to display user assistance information.",
2274
+ operationId: "tui.openHelp",
2275
+ responses: {
2276
+ 200: {
2277
+ description: "Help dialog opened successfully",
2278
+ content: {
2279
+ "application/json": {
2280
+ schema: resolver(z.boolean()),
2281
+ },
2282
+ },
2283
+ },
2284
+ },
2285
+ }),
2286
+ async (c) => {
2287
+ // TODO: open dialog
2288
+ return c.json(true)
2289
+ },
2290
+ )
2291
+ .post(
2292
+ "/tui/open-sessions",
2293
+ describeRoute({
2294
+ summary: "Open sessions dialog",
2295
+ description: "Open the session dialog",
2296
+ operationId: "tui.openSessions",
2297
+ responses: {
2298
+ 200: {
2299
+ description: "Session dialog opened successfully",
2300
+ content: {
2301
+ "application/json": {
2302
+ schema: resolver(z.boolean()),
2303
+ },
2304
+ },
2305
+ },
2306
+ },
2307
+ }),
2308
+ async (c) => {
2309
+ await Bus.publish(TuiEvent.CommandExecute, {
2310
+ command: "session.list",
2311
+ })
2312
+ return c.json(true)
2313
+ },
2314
+ )
2315
+ .post(
2316
+ "/tui/open-themes",
2317
+ describeRoute({
2318
+ summary: "Open themes dialog",
2319
+ description: "Open the theme dialog",
2320
+ operationId: "tui.openThemes",
2321
+ responses: {
2322
+ 200: {
2323
+ description: "Theme dialog opened successfully",
2324
+ content: {
2325
+ "application/json": {
2326
+ schema: resolver(z.boolean()),
2327
+ },
2328
+ },
2329
+ },
2330
+ },
2331
+ }),
2332
+ async (c) => {
2333
+ await Bus.publish(TuiEvent.CommandExecute, {
2334
+ command: "session.list",
2335
+ })
2336
+ return c.json(true)
2337
+ },
2338
+ )
2339
+ .post(
2340
+ "/tui/open-models",
2341
+ describeRoute({
2342
+ summary: "Open models dialog",
2343
+ description: "Open the model dialog",
2344
+ operationId: "tui.openModels",
2345
+ responses: {
2346
+ 200: {
2347
+ description: "Model dialog opened successfully",
2348
+ content: {
2349
+ "application/json": {
2350
+ schema: resolver(z.boolean()),
2351
+ },
2352
+ },
2353
+ },
2354
+ },
2355
+ }),
2356
+ async (c) => {
2357
+ await Bus.publish(TuiEvent.CommandExecute, {
2358
+ command: "model.list",
2359
+ })
2360
+ return c.json(true)
2361
+ },
2362
+ )
2363
+ .post(
2364
+ "/tui/submit-prompt",
2365
+ describeRoute({
2366
+ summary: "Submit TUI prompt",
2367
+ description: "Submit the prompt",
2368
+ operationId: "tui.submitPrompt",
2369
+ responses: {
2370
+ 200: {
2371
+ description: "Prompt submitted successfully",
2372
+ content: {
2373
+ "application/json": {
2374
+ schema: resolver(z.boolean()),
2375
+ },
2376
+ },
2377
+ },
2378
+ },
2379
+ }),
2380
+ async (c) => {
2381
+ await Bus.publish(TuiEvent.CommandExecute, {
2382
+ command: "prompt.submit",
2383
+ })
2384
+ return c.json(true)
2385
+ },
2386
+ )
2387
+ .post(
2388
+ "/tui/clear-prompt",
2389
+ describeRoute({
2390
+ summary: "Clear TUI prompt",
2391
+ description: "Clear the prompt",
2392
+ operationId: "tui.clearPrompt",
2393
+ responses: {
2394
+ 200: {
2395
+ description: "Prompt cleared successfully",
2396
+ content: {
2397
+ "application/json": {
2398
+ schema: resolver(z.boolean()),
2399
+ },
2400
+ },
2401
+ },
2402
+ },
2403
+ }),
2404
+ async (c) => {
2405
+ await Bus.publish(TuiEvent.CommandExecute, {
2406
+ command: "prompt.clear",
2407
+ })
2408
+ return c.json(true)
2409
+ },
2410
+ )
2411
+ .post(
2412
+ "/tui/execute-command",
2413
+ describeRoute({
2414
+ summary: "Execute TUI command",
2415
+ description: "Execute a TUI command (e.g. agent_cycle)",
2416
+ operationId: "tui.executeCommand",
2417
+ responses: {
2418
+ 200: {
2419
+ description: "Command executed successfully",
2420
+ content: {
2421
+ "application/json": {
2422
+ schema: resolver(z.boolean()),
2423
+ },
2424
+ },
2425
+ },
2426
+ ...errors(400),
2427
+ },
2428
+ }),
2429
+ validator("json", z.object({ command: z.string() })),
2430
+ async (c) => {
2431
+ const command = c.req.valid("json").command
2432
+ await Bus.publish(TuiEvent.CommandExecute, {
2433
+ // @ts-expect-error
2434
+ command: {
2435
+ session_new: "session.new",
2436
+ session_share: "session.share",
2437
+ session_interrupt: "session.interrupt",
2438
+ session_compact: "session.compact",
2439
+ messages_page_up: "session.page.up",
2440
+ messages_page_down: "session.page.down",
2441
+ messages_half_page_up: "session.half.page.up",
2442
+ messages_half_page_down: "session.half.page.down",
2443
+ messages_first: "session.first",
2444
+ messages_last: "session.last",
2445
+ agent_cycle: "agent.cycle",
2446
+ }[command],
2447
+ })
2448
+ return c.json(true)
2449
+ },
2450
+ )
2451
+ .post(
2452
+ "/tui/show-toast",
2453
+ describeRoute({
2454
+ summary: "Show TUI toast",
2455
+ description: "Show a toast notification in the TUI",
2456
+ operationId: "tui.showToast",
2457
+ responses: {
2458
+ 200: {
2459
+ description: "Toast notification shown successfully",
2460
+ content: {
2461
+ "application/json": {
2462
+ schema: resolver(z.boolean()),
2463
+ },
2464
+ },
2465
+ },
2466
+ },
2467
+ }),
2468
+ validator("json", TuiEvent.ToastShow.properties),
2469
+ async (c) => {
2470
+ await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"))
2471
+ return c.json(true)
2472
+ },
2473
+ )
2474
+ .post(
2475
+ "/tui/publish",
2476
+ describeRoute({
2477
+ summary: "Publish TUI event",
2478
+ description: "Publish a TUI event",
2479
+ operationId: "tui.publish",
2480
+ responses: {
2481
+ 200: {
2482
+ description: "Event published successfully",
2483
+ content: {
2484
+ "application/json": {
2485
+ schema: resolver(z.boolean()),
2486
+ },
2487
+ },
2488
+ },
2489
+ ...errors(400),
2490
+ },
2491
+ }),
2492
+ validator(
2493
+ "json",
2494
+ z.union(
2495
+ Object.values(TuiEvent).map((def) => {
2496
+ return z
2497
+ .object({
2498
+ type: z.literal(def.type),
2499
+ properties: def.properties,
2500
+ })
2501
+ .meta({
2502
+ ref: "Event" + "." + def.type,
2503
+ })
2504
+ }),
2505
+ ),
2506
+ ),
2507
+ async (c) => {
2508
+ const evt = c.req.valid("json")
2509
+ await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties)
2510
+ return c.json(true)
2511
+ },
2512
+ )
2513
+ .route("/tui/control", TuiRoute)
2514
+ .put(
2515
+ "/auth/:providerID",
2516
+ describeRoute({
2517
+ summary: "Set auth credentials",
2518
+ description: "Set authentication credentials",
2519
+ operationId: "auth.set",
2520
+ responses: {
2521
+ 200: {
2522
+ description: "Successfully set authentication credentials",
2523
+ content: {
2524
+ "application/json": {
2525
+ schema: resolver(z.boolean()),
2526
+ },
2527
+ },
2528
+ },
2529
+ ...errors(400),
2530
+ },
2531
+ }),
2532
+ validator(
2533
+ "param",
2534
+ z.object({
2535
+ providerID: z.string(),
2536
+ }),
2537
+ ),
2538
+ validator("json", Auth.Info),
2539
+ async (c) => {
2540
+ const providerID = c.req.valid("param").providerID
2541
+ const info = c.req.valid("json")
2542
+ await Auth.set(providerID, info)
2543
+ return c.json(true)
2544
+ },
2545
+ )
2546
+ .get(
2547
+ "/event",
2548
+ describeRoute({
2549
+ summary: "Subscribe to events",
2550
+ description: "Get events",
2551
+ operationId: "event.subscribe",
2552
+ responses: {
2553
+ 200: {
2554
+ description: "Event stream",
2555
+ content: {
2556
+ "text/event-stream": {
2557
+ schema: resolver(BusEvent.payloads()),
2558
+ },
2559
+ },
2560
+ },
2561
+ },
2562
+ }),
2563
+ async (c) => {
2564
+ log.info("event connected")
2565
+ return streamSSE(c, async (stream) => {
2566
+ stream.writeSSE({
2567
+ data: JSON.stringify({
2568
+ type: "server.connected",
2569
+ properties: {},
2570
+ }),
2571
+ })
2572
+ const unsub = Bus.subscribeAll(async (event) => {
2573
+ await stream.writeSSE({
2574
+ data: JSON.stringify(event),
2575
+ })
2576
+ if (event.type === Bus.InstanceDisposed.type) {
2577
+ stream.close()
2578
+ }
2579
+ })
2580
+
2581
+ // Send heartbeat every 30s to prevent WKWebView timeout (60s default)
2582
+ const heartbeat = setInterval(() => {
2583
+ stream.writeSSE({
2584
+ data: JSON.stringify({
2585
+ type: "server.heartbeat",
2586
+ properties: {},
2587
+ }),
2588
+ })
2589
+ }, 30000)
2590
+
2591
+ await new Promise<void>((resolve) => {
2592
+ stream.onAbort(() => {
2593
+ clearInterval(heartbeat)
2594
+ unsub()
2595
+ resolve()
2596
+ log.info("event disconnected")
2597
+ })
2598
+ })
2599
+ })
2600
+ },
2601
+ )
2602
+ .all("/*", async (c) => {
2603
+ return proxy(`https://app.bincode.ai${c.req.path}`, {
2604
+ ...c.req,
2605
+ headers: {
2606
+ host: "app.bincode.ai",
2607
+ },
2608
+ })
2609
+ }),
2610
+ )
2611
+
2612
+ export async function openapi() {
2613
+ const result = await generateSpecs(App(), {
2614
+ documentation: {
2615
+ info: {
2616
+ title: "bincode",
2617
+ version: "1.0.0",
2618
+ description: "bincode api",
2619
+ },
2620
+ openapi: "3.1.1",
2621
+ },
2622
+ })
2623
+ return result
2624
+ }
2625
+
2626
+ export function listen(opts: { port: number; hostname: string }) {
2627
+ const args = {
2628
+ hostname: opts.hostname,
2629
+ idleTimeout: 0,
2630
+ fetch: App().fetch,
2631
+ websocket: websocket,
2632
+ } as const
2633
+ if (opts.port === 0) {
2634
+ try {
2635
+ return Bun.serve({ ...args, port: 4096 })
2636
+ } catch {
2637
+ // port 4096 not available, fall through to use port 0
2638
+ }
2639
+ }
2640
+ return Bun.serve({ ...args, port: opts.port })
2641
+ }
2642
+ }