@bloxystudios/bloxycode 1.0.0

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 (344) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +256 -0
  3. package/bin/bloxycode +84 -0
  4. package/package.json +133 -0
  5. package/src/acp/README.md +164 -0
  6. package/src/acp/agent.ts +1437 -0
  7. package/src/acp/session.ts +105 -0
  8. package/src/acp/types.ts +22 -0
  9. package/src/agent/agent.ts +356 -0
  10. package/src/agent/generate.txt +75 -0
  11. package/src/agent/prompt/bloxy.txt +46 -0
  12. package/src/agent/prompt/compaction.txt +12 -0
  13. package/src/agent/prompt/explore.txt +18 -0
  14. package/src/agent/prompt/summary.txt +11 -0
  15. package/src/agent/prompt/title.txt +44 -0
  16. package/src/auth/index.ts +73 -0
  17. package/src/bloxy/event.ts +41 -0
  18. package/src/bloxy/index.ts +5 -0
  19. package/src/bloxy/parser.ts +263 -0
  20. package/src/bloxy/prompt.ts +121 -0
  21. package/src/bloxy/runner.ts +193 -0
  22. package/src/bloxy/state.ts +246 -0
  23. package/src/bun/index.ts +134 -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/bootstrap.ts +17 -0
  28. package/src/cli/cmd/acp.ts +69 -0
  29. package/src/cli/cmd/agent.ts +257 -0
  30. package/src/cli/cmd/auth.ts +400 -0
  31. package/src/cli/cmd/cmd.ts +7 -0
  32. package/src/cli/cmd/debug/agent.ts +167 -0
  33. package/src/cli/cmd/debug/config.ts +16 -0
  34. package/src/cli/cmd/debug/file.ts +97 -0
  35. package/src/cli/cmd/debug/index.ts +48 -0
  36. package/src/cli/cmd/debug/lsp.ts +52 -0
  37. package/src/cli/cmd/debug/ripgrep.ts +87 -0
  38. package/src/cli/cmd/debug/scrap.ts +16 -0
  39. package/src/cli/cmd/debug/skill.ts +16 -0
  40. package/src/cli/cmd/debug/snapshot.ts +52 -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 +1548 -0
  44. package/src/cli/cmd/import.ts +98 -0
  45. package/src/cli/cmd/mcp.ts +755 -0
  46. package/src/cli/cmd/models.ts +77 -0
  47. package/src/cli/cmd/pr.ts +112 -0
  48. package/src/cli/cmd/run.ts +395 -0
  49. package/src/cli/cmd/serve.ts +20 -0
  50. package/src/cli/cmd/session.ts +135 -0
  51. package/src/cli/cmd/stats.ts +402 -0
  52. package/src/cli/cmd/tui/app.tsx +771 -0
  53. package/src/cli/cmd/tui/attach.ts +39 -0
  54. package/src/cli/cmd/tui/component/border.tsx +21 -0
  55. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  56. package/src/cli/cmd/tui/component/dialog-command.tsx +148 -0
  57. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  58. package/src/cli/cmd/tui/component/dialog-model.tsx +234 -0
  59. package/src/cli/cmd/tui/component/dialog-provider.tsx +256 -0
  60. package/src/cli/cmd/tui/component/dialog-session-list.tsx +114 -0
  61. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  62. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  63. package/src/cli/cmd/tui/component/dialog-status.tsx +164 -0
  64. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  65. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  66. package/src/cli/cmd/tui/component/logo.tsx +102 -0
  67. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +653 -0
  68. package/src/cli/cmd/tui/component/prompt/frecency.tsx +89 -0
  69. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  70. package/src/cli/cmd/tui/component/prompt/index.tsx +1138 -0
  71. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  72. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  73. package/src/cli/cmd/tui/component/tips.tsx +153 -0
  74. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  75. package/src/cli/cmd/tui/context/args.tsx +14 -0
  76. package/src/cli/cmd/tui/context/directory.ts +13 -0
  77. package/src/cli/cmd/tui/context/exit.tsx +23 -0
  78. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  79. package/src/cli/cmd/tui/context/keybind.tsx +101 -0
  80. package/src/cli/cmd/tui/context/kv.tsx +52 -0
  81. package/src/cli/cmd/tui/context/local.tsx +402 -0
  82. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  83. package/src/cli/cmd/tui/context/route.tsx +46 -0
  84. package/src/cli/cmd/tui/context/sdk.tsx +94 -0
  85. package/src/cli/cmd/tui/context/sync.tsx +470 -0
  86. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  87. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  88. package/src/cli/cmd/tui/context/theme/bloxycode.json +245 -0
  89. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  90. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  91. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  92. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  93. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  94. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  95. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  96. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  97. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  98. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  99. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  100. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  101. package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
  102. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  103. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  104. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  105. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  106. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  107. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  108. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  109. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  110. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  111. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  112. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  113. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  114. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  115. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  116. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  117. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  118. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  119. package/src/cli/cmd/tui/context/theme.tsx +1152 -0
  120. package/src/cli/cmd/tui/event.ts +48 -0
  121. package/src/cli/cmd/tui/routes/home.tsx +140 -0
  122. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
  123. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
  124. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  125. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  126. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  127. package/src/cli/cmd/tui/routes/session/header.tsx +142 -0
  128. package/src/cli/cmd/tui/routes/session/index.tsx +2048 -0
  129. package/src/cli/cmd/tui/routes/session/permission.tsx +508 -0
  130. package/src/cli/cmd/tui/routes/session/question.tsx +453 -0
  131. package/src/cli/cmd/tui/routes/session/sidebar.tsx +313 -0
  132. package/src/cli/cmd/tui/thread.ts +165 -0
  133. package/src/cli/cmd/tui/ui/dialog-alert.tsx +57 -0
  134. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
  135. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +204 -0
  136. package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
  137. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +77 -0
  138. package/src/cli/cmd/tui/ui/dialog-select.tsx +385 -0
  139. package/src/cli/cmd/tui/ui/dialog.tsx +167 -0
  140. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  141. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  142. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  143. package/src/cli/cmd/tui/util/clipboard.ts +160 -0
  144. package/src/cli/cmd/tui/util/editor.ts +32 -0
  145. package/src/cli/cmd/tui/util/signal.ts +7 -0
  146. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  147. package/src/cli/cmd/tui/util/transcript.ts +98 -0
  148. package/src/cli/cmd/tui/worker.ts +152 -0
  149. package/src/cli/cmd/uninstall.ts +357 -0
  150. package/src/cli/cmd/upgrade.ts +73 -0
  151. package/src/cli/cmd/web.ts +81 -0
  152. package/src/cli/error.ts +57 -0
  153. package/src/cli/network.ts +53 -0
  154. package/src/cli/ui.ts +86 -0
  155. package/src/cli/upgrade.ts +25 -0
  156. package/src/command/index.ts +173 -0
  157. package/src/command/template/bloxy-resume.txt +15 -0
  158. package/src/command/template/bloxy-status.txt +25 -0
  159. package/src/command/template/bloxy-validate.txt +22 -0
  160. package/src/command/template/bloxy.txt +14 -0
  161. package/src/command/template/initialize.txt +10 -0
  162. package/src/command/template/review.txt +99 -0
  163. package/src/config/config.ts +1367 -0
  164. package/src/config/markdown.ts +93 -0
  165. package/src/env/index.ts +26 -0
  166. package/src/file/ignore.ts +83 -0
  167. package/src/file/index.ts +415 -0
  168. package/src/file/ripgrep.ts +407 -0
  169. package/src/file/time.ts +69 -0
  170. package/src/file/watcher.ts +127 -0
  171. package/src/flag/flag.ts +79 -0
  172. package/src/format/formatter.ts +357 -0
  173. package/src/format/index.ts +137 -0
  174. package/src/global/index.ts +55 -0
  175. package/src/id/id.ts +83 -0
  176. package/src/ide/index.ts +76 -0
  177. package/src/index.ts +159 -0
  178. package/src/installation/index.ts +246 -0
  179. package/src/lsp/client.ts +252 -0
  180. package/src/lsp/index.ts +485 -0
  181. package/src/lsp/language.ts +119 -0
  182. package/src/lsp/server.ts +2046 -0
  183. package/src/mcp/auth.ts +135 -0
  184. package/src/mcp/index.ts +934 -0
  185. package/src/mcp/oauth-callback.ts +200 -0
  186. package/src/mcp/oauth-provider.ts +154 -0
  187. package/src/patch/index.ts +680 -0
  188. package/src/permission/arity.ts +163 -0
  189. package/src/permission/index.ts +210 -0
  190. package/src/permission/next.ts +280 -0
  191. package/src/plugin/antigravity.ts +378 -0
  192. package/src/plugin/codex.ts +506 -0
  193. package/src/plugin/copilot.ts +298 -0
  194. package/src/plugin/index.ts +136 -0
  195. package/src/project/bootstrap.ts +35 -0
  196. package/src/project/instance.ts +91 -0
  197. package/src/project/project.ts +371 -0
  198. package/src/project/state.ts +66 -0
  199. package/src/project/vcs.ts +76 -0
  200. package/src/provider/auth.ts +147 -0
  201. package/src/provider/models-snapshot.ts +2 -0
  202. package/src/provider/models.ts +133 -0
  203. package/src/provider/provider.ts +1241 -0
  204. package/src/provider/sdk/openai-compatible/src/README.md +5 -0
  205. package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
  206. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
  207. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
  208. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +22 -0
  209. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
  210. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
  211. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
  212. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1732 -0
  213. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
  214. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
  215. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
  216. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
  217. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
  218. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
  219. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
  220. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
  221. package/src/provider/transform.ts +741 -0
  222. package/src/pty/index.ts +241 -0
  223. package/src/question/index.ts +171 -0
  224. package/src/scheduler/index.ts +61 -0
  225. package/src/server/error.ts +36 -0
  226. package/src/server/event.ts +7 -0
  227. package/src/server/mdns.ts +59 -0
  228. package/src/server/routes/config.ts +92 -0
  229. package/src/server/routes/experimental.ts +208 -0
  230. package/src/server/routes/file.ts +197 -0
  231. package/src/server/routes/global.ts +135 -0
  232. package/src/server/routes/mcp.ts +225 -0
  233. package/src/server/routes/permission.ts +68 -0
  234. package/src/server/routes/project.ts +82 -0
  235. package/src/server/routes/provider.ts +165 -0
  236. package/src/server/routes/pty.ts +169 -0
  237. package/src/server/routes/question.ts +98 -0
  238. package/src/server/routes/session.ts +939 -0
  239. package/src/server/routes/tui.ts +379 -0
  240. package/src/server/server.ts +604 -0
  241. package/src/session/compaction.ts +225 -0
  242. package/src/session/fallback.ts +246 -0
  243. package/src/session/index.ts +498 -0
  244. package/src/session/instruction.ts +164 -0
  245. package/src/session/llm.ts +298 -0
  246. package/src/session/message-v2.ts +747 -0
  247. package/src/session/message.ts +189 -0
  248. package/src/session/processor.ts +450 -0
  249. package/src/session/prompt/anthropic-20250930.txt +166 -0
  250. package/src/session/prompt/anthropic.txt +105 -0
  251. package/src/session/prompt/beast.txt +147 -0
  252. package/src/session/prompt/build-switch.txt +5 -0
  253. package/src/session/prompt/codex_header.txt +79 -0
  254. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  255. package/src/session/prompt/gemini.txt +155 -0
  256. package/src/session/prompt/max-steps.txt +16 -0
  257. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  258. package/src/session/prompt/plan.txt +26 -0
  259. package/src/session/prompt/qwen.txt +109 -0
  260. package/src/session/prompt.ts +1822 -0
  261. package/src/session/retry.ts +99 -0
  262. package/src/session/revert.ts +121 -0
  263. package/src/session/status.ts +100 -0
  264. package/src/session/summary.ts +217 -0
  265. package/src/session/system.ts +52 -0
  266. package/src/session/todo.ts +37 -0
  267. package/src/share/share-next.ts +200 -0
  268. package/src/share/share.ts +92 -0
  269. package/src/shell/shell.ts +67 -0
  270. package/src/skill/index.ts +1 -0
  271. package/src/skill/skill.ts +135 -0
  272. package/src/snapshot/index.ts +236 -0
  273. package/src/storage/storage.ts +227 -0
  274. package/src/tool/apply_patch.ts +281 -0
  275. package/src/tool/apply_patch.txt +33 -0
  276. package/src/tool/bash.ts +258 -0
  277. package/src/tool/bash.txt +115 -0
  278. package/src/tool/batch.ts +175 -0
  279. package/src/tool/batch.txt +24 -0
  280. package/src/tool/bloxy-control.ts +123 -0
  281. package/src/tool/bloxy-control.txt +13 -0
  282. package/src/tool/codesearch.ts +132 -0
  283. package/src/tool/codesearch.txt +12 -0
  284. package/src/tool/edit.ts +655 -0
  285. package/src/tool/edit.txt +10 -0
  286. package/src/tool/external-directory.ts +32 -0
  287. package/src/tool/glob.ts +77 -0
  288. package/src/tool/glob.txt +6 -0
  289. package/src/tool/grep.ts +154 -0
  290. package/src/tool/grep.txt +8 -0
  291. package/src/tool/invalid.ts +17 -0
  292. package/src/tool/ls.ts +121 -0
  293. package/src/tool/ls.txt +1 -0
  294. package/src/tool/lsp.ts +96 -0
  295. package/src/tool/lsp.txt +19 -0
  296. package/src/tool/multiedit.ts +46 -0
  297. package/src/tool/multiedit.txt +41 -0
  298. package/src/tool/plan-enter.txt +14 -0
  299. package/src/tool/plan-exit.txt +13 -0
  300. package/src/tool/plan.ts +130 -0
  301. package/src/tool/question.ts +33 -0
  302. package/src/tool/question.txt +10 -0
  303. package/src/tool/read.ts +211 -0
  304. package/src/tool/read.txt +12 -0
  305. package/src/tool/registry.ts +161 -0
  306. package/src/tool/skill.ts +82 -0
  307. package/src/tool/task.ts +191 -0
  308. package/src/tool/task.txt +60 -0
  309. package/src/tool/todo.ts +53 -0
  310. package/src/tool/todoread.txt +14 -0
  311. package/src/tool/todowrite.txt +167 -0
  312. package/src/tool/tool.ts +89 -0
  313. package/src/tool/truncation.ts +106 -0
  314. package/src/tool/webfetch.ts +188 -0
  315. package/src/tool/webfetch.txt +13 -0
  316. package/src/tool/websearch.ts +150 -0
  317. package/src/tool/websearch.txt +14 -0
  318. package/src/tool/write.ts +85 -0
  319. package/src/tool/write.txt +8 -0
  320. package/src/util/archive.ts +16 -0
  321. package/src/util/binary.ts +41 -0
  322. package/src/util/color.ts +19 -0
  323. package/src/util/context.ts +25 -0
  324. package/src/util/defer.ts +12 -0
  325. package/src/util/error.ts +54 -0
  326. package/src/util/eventloop.ts +20 -0
  327. package/src/util/filesystem.ts +93 -0
  328. package/src/util/fn.ts +11 -0
  329. package/src/util/format.ts +20 -0
  330. package/src/util/iife.ts +3 -0
  331. package/src/util/keybind.ts +103 -0
  332. package/src/util/lazy.ts +23 -0
  333. package/src/util/locale.ts +81 -0
  334. package/src/util/lock.ts +98 -0
  335. package/src/util/log.ts +180 -0
  336. package/src/util/queue.ts +32 -0
  337. package/src/util/rpc.ts +66 -0
  338. package/src/util/scrap.ts +10 -0
  339. package/src/util/signal.ts +12 -0
  340. package/src/util/slug.ts +74 -0
  341. package/src/util/timeout.ts +14 -0
  342. package/src/util/token.ts +7 -0
  343. package/src/util/wildcard.ts +56 -0
  344. package/src/worktree/index.ts +549 -0
@@ -0,0 +1,241 @@
1
+ import { BusEvent } from "@/bus/bus-event"
2
+ import { Bus } from "@/bus"
3
+ import { type IPty } from "bun-pty"
4
+ import z from "zod"
5
+ import { Identifier } from "../id/id"
6
+ import { Log } from "../util/log"
7
+ import type { WSContext } from "hono/ws"
8
+ import { Instance } from "../project/instance"
9
+ import { lazy } from "@/util/lazy"
10
+ import { Shell } from "@/shell/shell"
11
+
12
+ export namespace Pty {
13
+ const log = Log.create({ service: "pty" })
14
+
15
+ const BUFFER_LIMIT = 1024 * 1024 * 2
16
+ const BUFFER_CHUNK = 64 * 1024
17
+
18
+ const pty = lazy(async () => {
19
+ const { spawn } = await import("bun-pty")
20
+ return spawn
21
+ })
22
+
23
+ export const Info = z
24
+ .object({
25
+ id: Identifier.schema("pty"),
26
+ title: z.string(),
27
+ command: z.string(),
28
+ args: z.array(z.string()),
29
+ cwd: z.string(),
30
+ status: z.enum(["running", "exited"]),
31
+ pid: z.number(),
32
+ })
33
+ .meta({ ref: "Pty" })
34
+
35
+ export type Info = z.infer<typeof Info>
36
+
37
+ export const CreateInput = z.object({
38
+ command: z.string().optional(),
39
+ args: z.array(z.string()).optional(),
40
+ cwd: z.string().optional(),
41
+ title: z.string().optional(),
42
+ env: z.record(z.string(), z.string()).optional(),
43
+ })
44
+
45
+ export type CreateInput = z.infer<typeof CreateInput>
46
+
47
+ export const UpdateInput = z.object({
48
+ title: z.string().optional(),
49
+ size: z
50
+ .object({
51
+ rows: z.number(),
52
+ cols: z.number(),
53
+ })
54
+ .optional(),
55
+ })
56
+
57
+ export type UpdateInput = z.infer<typeof UpdateInput>
58
+
59
+ export const Event = {
60
+ Created: BusEvent.define("pty.created", z.object({ info: Info })),
61
+ Updated: BusEvent.define("pty.updated", z.object({ info: Info })),
62
+ Exited: BusEvent.define("pty.exited", z.object({ id: Identifier.schema("pty"), exitCode: z.number() })),
63
+ Deleted: BusEvent.define("pty.deleted", z.object({ id: Identifier.schema("pty") })),
64
+ }
65
+
66
+ interface ActiveSession {
67
+ info: Info
68
+ process: IPty
69
+ buffer: string
70
+ subscribers: Set<WSContext>
71
+ }
72
+
73
+ const state = Instance.state(
74
+ () => new Map<string, ActiveSession>(),
75
+ async (sessions) => {
76
+ for (const session of sessions.values()) {
77
+ try {
78
+ session.process.kill()
79
+ } catch {}
80
+ for (const ws of session.subscribers) {
81
+ ws.close()
82
+ }
83
+ }
84
+ sessions.clear()
85
+ },
86
+ )
87
+
88
+ export function list() {
89
+ return Array.from(state().values()).map((s) => s.info)
90
+ }
91
+
92
+ export function get(id: string) {
93
+ return state().get(id)?.info
94
+ }
95
+
96
+ export async function create(input: CreateInput) {
97
+ const id = Identifier.create("pty", false)
98
+ const command = input.command || Shell.preferred()
99
+ const args = input.args || []
100
+ if (command.endsWith("sh")) {
101
+ args.push("-l")
102
+ }
103
+
104
+ const cwd = input.cwd || Instance.directory
105
+ const env = {
106
+ ...process.env,
107
+ ...input.env,
108
+ TERM: "xterm-256color",
109
+ BLOXYCODE_TERMINAL: "1",
110
+ } as Record<string, string>
111
+ log.info("creating session", { id, cmd: command, args, cwd })
112
+
113
+ const spawn = await pty()
114
+ const ptyProcess = spawn(command, args, {
115
+ name: "xterm-256color",
116
+ cwd,
117
+ env,
118
+ })
119
+
120
+ const info = {
121
+ id,
122
+ title: input.title || `Terminal ${id.slice(-4)}`,
123
+ command,
124
+ args,
125
+ cwd,
126
+ status: "running",
127
+ pid: ptyProcess.pid,
128
+ } as const
129
+ const session: ActiveSession = {
130
+ info,
131
+ process: ptyProcess,
132
+ buffer: "",
133
+ subscribers: new Set(),
134
+ }
135
+ state().set(id, session)
136
+ ptyProcess.onData((data) => {
137
+ let open = false
138
+ for (const ws of session.subscribers) {
139
+ if (ws.readyState !== 1) {
140
+ session.subscribers.delete(ws)
141
+ continue
142
+ }
143
+ open = true
144
+ ws.send(data)
145
+ }
146
+ if (open) return
147
+ session.buffer += data
148
+ if (session.buffer.length <= BUFFER_LIMIT) return
149
+ session.buffer = session.buffer.slice(-BUFFER_LIMIT)
150
+ })
151
+ ptyProcess.onExit(({ exitCode }) => {
152
+ log.info("session exited", { id, exitCode })
153
+ session.info.status = "exited"
154
+ for (const ws of session.subscribers) {
155
+ ws.close()
156
+ }
157
+ session.subscribers.clear()
158
+ Bus.publish(Event.Exited, { id, exitCode })
159
+ for (const ws of session.subscribers) {
160
+ ws.close()
161
+ }
162
+ state().delete(id)
163
+ })
164
+ Bus.publish(Event.Created, { info })
165
+ return info
166
+ }
167
+
168
+ export async function update(id: string, input: UpdateInput) {
169
+ const session = state().get(id)
170
+ if (!session) return
171
+ if (input.title) {
172
+ session.info.title = input.title
173
+ }
174
+ if (input.size) {
175
+ session.process.resize(input.size.cols, input.size.rows)
176
+ }
177
+ Bus.publish(Event.Updated, { info: session.info })
178
+ return session.info
179
+ }
180
+
181
+ export async function remove(id: string) {
182
+ const session = state().get(id)
183
+ if (!session) return
184
+ log.info("removing session", { id })
185
+ try {
186
+ session.process.kill()
187
+ } catch {}
188
+ for (const ws of session.subscribers) {
189
+ ws.close()
190
+ }
191
+ state().delete(id)
192
+ Bus.publish(Event.Deleted, { id })
193
+ }
194
+
195
+ export function resize(id: string, cols: number, rows: number) {
196
+ const session = state().get(id)
197
+ if (session && session.info.status === "running") {
198
+ session.process.resize(cols, rows)
199
+ }
200
+ }
201
+
202
+ export function write(id: string, data: string) {
203
+ const session = state().get(id)
204
+ if (session && session.info.status === "running") {
205
+ session.process.write(data)
206
+ }
207
+ }
208
+
209
+ export function connect(id: string, ws: WSContext) {
210
+ const session = state().get(id)
211
+ if (!session) {
212
+ ws.close()
213
+ return
214
+ }
215
+ log.info("client connected to session", { id })
216
+ session.subscribers.add(ws)
217
+ if (session.buffer) {
218
+ const buffer = session.buffer.length <= BUFFER_LIMIT ? session.buffer : session.buffer.slice(-BUFFER_LIMIT)
219
+ session.buffer = ""
220
+ try {
221
+ for (let i = 0; i < buffer.length; i += BUFFER_CHUNK) {
222
+ ws.send(buffer.slice(i, i + BUFFER_CHUNK))
223
+ }
224
+ } catch {
225
+ session.subscribers.delete(ws)
226
+ session.buffer = buffer
227
+ ws.close()
228
+ return
229
+ }
230
+ }
231
+ return {
232
+ onMessage: (message: string | ArrayBuffer) => {
233
+ session.process.write(String(message))
234
+ },
235
+ onClose: () => {
236
+ log.info("client disconnected from session", { id })
237
+ session.subscribers.delete(ws)
238
+ },
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,171 @@
1
+ import { Bus } from "@/bus"
2
+ import { BusEvent } from "@/bus/bus-event"
3
+ import { Identifier } from "@/id/id"
4
+ import { Instance } from "@/project/instance"
5
+ import { Log } from "@/util/log"
6
+ import z from "zod"
7
+
8
+ export namespace Question {
9
+ const log = Log.create({ service: "question" })
10
+
11
+ export const Option = z
12
+ .object({
13
+ label: z.string().describe("Display text (1-5 words, concise)"),
14
+ description: z.string().describe("Explanation of choice"),
15
+ })
16
+ .meta({
17
+ ref: "QuestionOption",
18
+ })
19
+ export type Option = z.infer<typeof Option>
20
+
21
+ export const Info = z
22
+ .object({
23
+ question: z.string().describe("Complete question"),
24
+ header: z.string().describe("Very short label (max 30 chars)"),
25
+ options: z.array(Option).describe("Available choices"),
26
+ multiple: z.boolean().optional().describe("Allow selecting multiple choices"),
27
+ custom: z.boolean().optional().describe("Allow typing a custom answer (default: true)"),
28
+ })
29
+ .meta({
30
+ ref: "QuestionInfo",
31
+ })
32
+ export type Info = z.infer<typeof Info>
33
+
34
+ export const Request = z
35
+ .object({
36
+ id: Identifier.schema("question"),
37
+ sessionID: Identifier.schema("session"),
38
+ questions: z.array(Info).describe("Questions to ask"),
39
+ tool: z
40
+ .object({
41
+ messageID: z.string(),
42
+ callID: z.string(),
43
+ })
44
+ .optional(),
45
+ })
46
+ .meta({
47
+ ref: "QuestionRequest",
48
+ })
49
+ export type Request = z.infer<typeof Request>
50
+
51
+ export const Answer = z.array(z.string()).meta({
52
+ ref: "QuestionAnswer",
53
+ })
54
+ export type Answer = z.infer<typeof Answer>
55
+
56
+ export const Reply = z.object({
57
+ answers: z
58
+ .array(Answer)
59
+ .describe("User answers in order of questions (each answer is an array of selected labels)"),
60
+ })
61
+ export type Reply = z.infer<typeof Reply>
62
+
63
+ export const Event = {
64
+ Asked: BusEvent.define("question.asked", Request),
65
+ Replied: BusEvent.define(
66
+ "question.replied",
67
+ z.object({
68
+ sessionID: z.string(),
69
+ requestID: z.string(),
70
+ answers: z.array(Answer),
71
+ }),
72
+ ),
73
+ Rejected: BusEvent.define(
74
+ "question.rejected",
75
+ z.object({
76
+ sessionID: z.string(),
77
+ requestID: z.string(),
78
+ }),
79
+ ),
80
+ }
81
+
82
+ const state = Instance.state(async () => {
83
+ const pending: Record<
84
+ string,
85
+ {
86
+ info: Request
87
+ resolve: (answers: Answer[]) => void
88
+ reject: (e: any) => void
89
+ }
90
+ > = {}
91
+
92
+ return {
93
+ pending,
94
+ }
95
+ })
96
+
97
+ export async function ask(input: {
98
+ sessionID: string
99
+ questions: Info[]
100
+ tool?: { messageID: string; callID: string }
101
+ }): Promise<Answer[]> {
102
+ const s = await state()
103
+ const id = Identifier.ascending("question")
104
+
105
+ log.info("asking", { id, questions: input.questions.length })
106
+
107
+ return new Promise<Answer[]>((resolve, reject) => {
108
+ const info: Request = {
109
+ id,
110
+ sessionID: input.sessionID,
111
+ questions: input.questions,
112
+ tool: input.tool,
113
+ }
114
+ s.pending[id] = {
115
+ info,
116
+ resolve,
117
+ reject,
118
+ }
119
+ Bus.publish(Event.Asked, info)
120
+ })
121
+ }
122
+
123
+ export async function reply(input: { requestID: string; answers: Answer[] }): Promise<void> {
124
+ const s = await state()
125
+ const existing = s.pending[input.requestID]
126
+ if (!existing) {
127
+ log.warn("reply for unknown request", { requestID: input.requestID })
128
+ return
129
+ }
130
+ delete s.pending[input.requestID]
131
+
132
+ log.info("replied", { requestID: input.requestID, answers: input.answers })
133
+
134
+ Bus.publish(Event.Replied, {
135
+ sessionID: existing.info.sessionID,
136
+ requestID: existing.info.id,
137
+ answers: input.answers,
138
+ })
139
+
140
+ existing.resolve(input.answers)
141
+ }
142
+
143
+ export async function reject(requestID: string): Promise<void> {
144
+ const s = await state()
145
+ const existing = s.pending[requestID]
146
+ if (!existing) {
147
+ log.warn("reject for unknown request", { requestID })
148
+ return
149
+ }
150
+ delete s.pending[requestID]
151
+
152
+ log.info("rejected", { requestID })
153
+
154
+ Bus.publish(Event.Rejected, {
155
+ sessionID: existing.info.sessionID,
156
+ requestID: existing.info.id,
157
+ })
158
+
159
+ existing.reject(new RejectedError())
160
+ }
161
+
162
+ export class RejectedError extends Error {
163
+ constructor() {
164
+ super("The user dismissed this question")
165
+ }
166
+ }
167
+
168
+ export async function list() {
169
+ return state().then((x) => Object.values(x.pending).map((x) => x.info))
170
+ }
171
+ }
@@ -0,0 +1,61 @@
1
+ import { Instance } from "../project/instance"
2
+ import { Log } from "../util/log"
3
+
4
+ export namespace Scheduler {
5
+ const log = Log.create({ service: "scheduler" })
6
+
7
+ export type Task = {
8
+ id: string
9
+ interval: number
10
+ run: () => Promise<void>
11
+ scope?: "instance" | "global"
12
+ }
13
+
14
+ type Timer = ReturnType<typeof setInterval>
15
+ type Entry = {
16
+ tasks: Map<string, Task>
17
+ timers: Map<string, Timer>
18
+ }
19
+
20
+ const create = (): Entry => {
21
+ const tasks = new Map<string, Task>()
22
+ const timers = new Map<string, Timer>()
23
+ return { tasks, timers }
24
+ }
25
+
26
+ const shared = create()
27
+
28
+ const state = Instance.state(
29
+ () => create(),
30
+ async (entry) => {
31
+ for (const timer of entry.timers.values()) {
32
+ clearInterval(timer)
33
+ }
34
+ entry.tasks.clear()
35
+ entry.timers.clear()
36
+ },
37
+ )
38
+
39
+ export function register(task: Task) {
40
+ const scope = task.scope ?? "instance"
41
+ const entry = scope === "global" ? shared : state()
42
+ const current = entry.timers.get(task.id)
43
+ if (current && scope === "global") return
44
+ if (current) clearInterval(current)
45
+
46
+ entry.tasks.set(task.id, task)
47
+ void run(task)
48
+ const timer = setInterval(() => {
49
+ void run(task)
50
+ }, task.interval)
51
+ timer.unref()
52
+ entry.timers.set(task.id, timer)
53
+ }
54
+
55
+ async function run(task: Task) {
56
+ log.info("run", { id: task.id })
57
+ await task.run().catch((error) => {
58
+ log.error("run failed", { id: task.id, error })
59
+ })
60
+ }
61
+ }
@@ -0,0 +1,36 @@
1
+ import { resolver } from "hono-openapi"
2
+ import z from "zod"
3
+ import { Storage } from "../storage/storage"
4
+
5
+ export const ERRORS = {
6
+ 400: {
7
+ description: "Bad request",
8
+ content: {
9
+ "application/json": {
10
+ schema: resolver(
11
+ z
12
+ .object({
13
+ data: z.any(),
14
+ errors: z.array(z.record(z.string(), z.any())),
15
+ success: z.literal(false),
16
+ })
17
+ .meta({
18
+ ref: "BadRequestError",
19
+ }),
20
+ ),
21
+ },
22
+ },
23
+ },
24
+ 404: {
25
+ description: "Not found",
26
+ content: {
27
+ "application/json": {
28
+ schema: resolver(Storage.NotFoundError.Schema),
29
+ },
30
+ },
31
+ },
32
+ } as const
33
+
34
+ export function errors(...codes: number[]) {
35
+ return Object.fromEntries(codes.map((code) => [code, ERRORS[code as keyof typeof ERRORS]]))
36
+ }
@@ -0,0 +1,7 @@
1
+ import { BusEvent } from "@/bus/bus-event"
2
+ import z from "zod"
3
+
4
+ export const Event = {
5
+ Connected: BusEvent.define("server.connected", z.object({})),
6
+ Disposed: BusEvent.define("global.disposed", z.object({})),
7
+ }
@@ -0,0 +1,59 @@
1
+ import { Log } from "@/util/log"
2
+ import { Bonjour } from "bonjour-service"
3
+
4
+ const log = Log.create({ service: "mdns" })
5
+
6
+ export namespace MDNS {
7
+ let bonjour: Bonjour | undefined
8
+ let currentPort: number | undefined
9
+
10
+ export function publish(port: number) {
11
+ if (currentPort === port) return
12
+ if (bonjour) unpublish()
13
+
14
+ try {
15
+ const name = `opencode-${port}`
16
+ bonjour = new Bonjour()
17
+ const service = bonjour.publish({
18
+ name,
19
+ type: "http",
20
+ host: "opencode.local",
21
+ port,
22
+ txt: { path: "/" },
23
+ })
24
+
25
+ service.on("up", () => {
26
+ log.info("mDNS service published", { name, port })
27
+ })
28
+
29
+ service.on("error", (err) => {
30
+ log.error("mDNS service error", { error: err })
31
+ })
32
+
33
+ currentPort = port
34
+ } catch (err) {
35
+ log.error("mDNS publish failed", { error: err })
36
+ if (bonjour) {
37
+ try {
38
+ bonjour.destroy()
39
+ } catch {}
40
+ }
41
+ bonjour = undefined
42
+ currentPort = undefined
43
+ }
44
+ }
45
+
46
+ export function unpublish() {
47
+ if (bonjour) {
48
+ try {
49
+ bonjour.unpublishAll()
50
+ bonjour.destroy()
51
+ } catch (err) {
52
+ log.error("mDNS unpublish failed", { error: err })
53
+ }
54
+ bonjour = undefined
55
+ currentPort = undefined
56
+ log.info("mDNS service unpublished")
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,92 @@
1
+ import { Hono } from "hono"
2
+ import { describeRoute, validator, resolver } from "hono-openapi"
3
+ import z from "zod"
4
+ import { Config } from "../../config/config"
5
+ import { Provider } from "../../provider/provider"
6
+ import { mapValues } from "remeda"
7
+ import { errors } from "../error"
8
+ import { Log } from "../../util/log"
9
+ import { lazy } from "../../util/lazy"
10
+
11
+ const log = Log.create({ service: "server" })
12
+
13
+ export const ConfigRoutes = lazy(() =>
14
+ new Hono()
15
+ .get(
16
+ "/",
17
+ describeRoute({
18
+ summary: "Get configuration",
19
+ description: "Retrieve the current BloxyCode configuration settings and preferences.",
20
+ operationId: "config.get",
21
+ responses: {
22
+ 200: {
23
+ description: "Get config info",
24
+ content: {
25
+ "application/json": {
26
+ schema: resolver(Config.Info),
27
+ },
28
+ },
29
+ },
30
+ },
31
+ }),
32
+ async (c) => {
33
+ return c.json(await Config.get())
34
+ },
35
+ )
36
+ .patch(
37
+ "/",
38
+ describeRoute({
39
+ summary: "Update configuration",
40
+ description: "Update BloxyCode configuration settings and preferences.",
41
+ operationId: "config.update",
42
+ responses: {
43
+ 200: {
44
+ description: "Successfully updated config",
45
+ content: {
46
+ "application/json": {
47
+ schema: resolver(Config.Info),
48
+ },
49
+ },
50
+ },
51
+ ...errors(400),
52
+ },
53
+ }),
54
+ validator("json", Config.Info),
55
+ async (c) => {
56
+ const config = c.req.valid("json")
57
+ await Config.update(config)
58
+ return c.json(config)
59
+ },
60
+ )
61
+ .get(
62
+ "/providers",
63
+ describeRoute({
64
+ summary: "List config providers",
65
+ description: "Get a list of all configured AI providers and their default models.",
66
+ operationId: "config.providers",
67
+ responses: {
68
+ 200: {
69
+ description: "List of providers",
70
+ content: {
71
+ "application/json": {
72
+ schema: resolver(
73
+ z.object({
74
+ providers: Provider.Info.array(),
75
+ default: z.record(z.string(), z.string()),
76
+ }),
77
+ ),
78
+ },
79
+ },
80
+ },
81
+ },
82
+ }),
83
+ async (c) => {
84
+ using _ = log.time("providers")
85
+ const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
86
+ return c.json({
87
+ providers: Object.values(providers),
88
+ default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
89
+ })
90
+ },
91
+ ),
92
+ )