agentbox-app 0.1.6

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 (302) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +176 -0
  3. package/bin/agentbox.mjs +103 -0
  4. package/briefs/adhd-mode.md +5 -0
  5. package/briefs/message-rules.md +95 -0
  6. package/briefs/worker.md +230 -0
  7. package/briefs/writing-rules.md +336 -0
  8. package/main/account-discovery.mjs +145 -0
  9. package/main/account-tooling.mjs +261 -0
  10. package/main/agent-activity.mjs +56 -0
  11. package/main/agent-files.mjs +422 -0
  12. package/main/agent-schedule.mjs +254 -0
  13. package/main/agent-sessions.mjs +435 -0
  14. package/main/agents.mjs +1095 -0
  15. package/main/analytics.mjs +206 -0
  16. package/main/answer-modes.mjs +206 -0
  17. package/main/approval-prompt-server.mjs +160 -0
  18. package/main/approvals.mjs +487 -0
  19. package/main/artifact-path.mjs +144 -0
  20. package/main/claude-bin.mjs +311 -0
  21. package/main/claude-input.mjs +117 -0
  22. package/main/claude-models.mjs +244 -0
  23. package/main/claude-plan.mjs +148 -0
  24. package/main/claude-usage.mjs +149 -0
  25. package/main/code-change.mjs +383 -0
  26. package/main/code-file.mjs +116 -0
  27. package/main/codex-account.mjs +146 -0
  28. package/main/codex-app-server.mjs +796 -0
  29. package/main/codex-approvals.mjs +712 -0
  30. package/main/codex-bin.mjs +261 -0
  31. package/main/codex-compaction.mjs +49 -0
  32. package/main/codex-models.mjs +194 -0
  33. package/main/codex-session.mjs +797 -0
  34. package/main/codex-threads.mjs +268 -0
  35. package/main/codex-usage-file.mjs +192 -0
  36. package/main/codex-usage.mjs +149 -0
  37. package/main/codex-watch.mjs +35 -0
  38. package/main/codex.mjs +757 -0
  39. package/main/config.mjs +345 -0
  40. package/main/copy-selection.mjs +55 -0
  41. package/main/crash-report.mjs +158 -0
  42. package/main/demo.mjs +254 -0
  43. package/main/dev-window.mjs +103 -0
  44. package/main/doc-file.mjs +99 -0
  45. package/main/doc-scheme.mjs +126 -0
  46. package/main/first-run.mjs +190 -0
  47. package/main/folders.mjs +91 -0
  48. package/main/fresh-user.mjs +120 -0
  49. package/main/git-change.mjs +323 -0
  50. package/main/img-scheme.mjs +90 -0
  51. package/main/instruction-settings.mjs +205 -0
  52. package/main/ipc.mjs +1558 -0
  53. package/main/live-replies.mjs +77 -0
  54. package/main/machine.mjs +113 -0
  55. package/main/main.mjs +972 -0
  56. package/main/menu.mjs +177 -0
  57. package/main/notify.mjs +155 -0
  58. package/main/open-source-file.mjs +8 -0
  59. package/main/project-identity.mjs +149 -0
  60. package/main/provider-commands.mjs +32 -0
  61. package/main/rail-note.mjs +74 -0
  62. package/main/repeats.mjs +261 -0
  63. package/main/row-label.mjs +185 -0
  64. package/main/screen-detail.mjs +44 -0
  65. package/main/serve.mjs +262 -0
  66. package/main/settings.mjs +912 -0
  67. package/main/staleness.mjs +76 -0
  68. package/main/store/chats.mjs +188 -0
  69. package/main/store/home.mjs +383 -0
  70. package/main/store/migrate.mjs +40 -0
  71. package/main/store/paths.mjs +82 -0
  72. package/main/store/pending-writes.mjs +122 -0
  73. package/main/store/project-lock.mjs +200 -0
  74. package/main/store/project.mjs +523 -0
  75. package/main/store/queues.mjs +31 -0
  76. package/main/store/work-items.mjs +299 -0
  77. package/main/store/write-queue.mjs +14 -0
  78. package/main/store-modules.mjs +23 -0
  79. package/main/store.mjs +1269 -0
  80. package/main/supervisor.mjs +6875 -0
  81. package/main/task-commands.mjs +161 -0
  82. package/main/task-folders-offthread.mjs +84 -0
  83. package/main/task-folders-thread.mjs +13 -0
  84. package/main/task-folders.mjs +409 -0
  85. package/main/task-remote-control.mjs +65 -0
  86. package/main/task-terminals.mjs +169 -0
  87. package/main/terminal-access.mjs +6 -0
  88. package/main/updater.mjs +240 -0
  89. package/main/zoom-keys.mjs +54 -0
  90. package/main/zoom-readout.mjs +20 -0
  91. package/mcp/agentbox-mcp.sh +34 -0
  92. package/mcp/core/account.mjs +77 -0
  93. package/mcp/core/chat.mjs +61 -0
  94. package/mcp/core/documents.mjs +238 -0
  95. package/mcp/core/page.mjs +152 -0
  96. package/mcp/core/products.mjs +110 -0
  97. package/mcp/core/skills.mjs +126 -0
  98. package/mcp/core/work.mjs +377 -0
  99. package/mcp/stdio.mjs +70 -0
  100. package/mcp/tools.mjs +257 -0
  101. package/package.json +186 -0
  102. package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
  103. package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
  104. package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
  105. package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
  106. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  107. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  108. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  109. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  110. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  111. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  112. package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
  113. package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
  114. package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
  115. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  116. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  117. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  118. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  119. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  120. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  121. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  122. package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
  123. package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
  124. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  125. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  126. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  127. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  128. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  129. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  130. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  131. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  132. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  133. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  134. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  135. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  136. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  137. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  138. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  139. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  140. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  141. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  142. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  143. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  144. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  145. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  146. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  147. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  148. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  149. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  150. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  151. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  152. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  153. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  154. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  155. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  156. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  157. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  158. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  159. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  160. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  161. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  162. package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
  163. package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  164. package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
  165. package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
  166. package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  167. package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
  168. package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
  169. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  170. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  171. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  172. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  173. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  174. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  175. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  176. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  177. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  178. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  179. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  180. package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
  181. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  182. package/renderer/dist/emulator/data/compression/README.md +11 -0
  183. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  184. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  185. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  186. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  187. package/renderer/dist/emulator/data/cores/README.md +22 -0
  188. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  189. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  190. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  191. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  192. package/renderer/dist/emulator/data/cores/package.json +65 -0
  193. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  194. package/renderer/dist/emulator/data/emulator.css +1614 -0
  195. package/renderer/dist/emulator/data/loader.js +170 -0
  196. package/renderer/dist/emulator/data/localization/README.md +63 -0
  197. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  198. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  199. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  200. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  201. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  202. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  203. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  204. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  205. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  206. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  207. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  208. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  209. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  210. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  211. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  212. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  213. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  214. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  215. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  216. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  217. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  218. package/renderer/dist/emulator/data/src/compression.js +142 -0
  219. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  220. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  221. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  222. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  223. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  224. package/renderer/dist/emulator/data/src/storage.js +125 -0
  225. package/renderer/dist/emulator/data/version.json +5 -0
  226. package/renderer/dist/emulator/host.js +299 -0
  227. package/renderer/dist/emulator/index.html +28 -0
  228. package/renderer/dist/emulator/provenance.json +6 -0
  229. package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
  230. package/renderer/dist/index.html +13 -0
  231. package/scripts/after-install.mjs +59 -0
  232. package/scripts/approval-server.sh +43 -0
  233. package/scripts/before-publish.mjs +133 -0
  234. package/shared/agent-import.d.mts +13 -0
  235. package/shared/agent-import.mjs +252 -0
  236. package/shared/agents.d.mts +190 -0
  237. package/shared/agents.mjs +937 -0
  238. package/shared/analytics-events.mjs +63 -0
  239. package/shared/answers.d.mts +12 -0
  240. package/shared/answers.mjs +80 -0
  241. package/shared/artifact-keys.d.mts +28 -0
  242. package/shared/artifact-keys.mjs +140 -0
  243. package/shared/bridge-map.mjs +128 -0
  244. package/shared/claude-commands.d.mts +20 -0
  245. package/shared/claude-commands.generated.mjs +149 -0
  246. package/shared/claude-commands.mjs +131 -0
  247. package/shared/claude-models.generated.d.mts +15 -0
  248. package/shared/claude-models.generated.mjs +26 -0
  249. package/shared/claude-usage.d.mts +16 -0
  250. package/shared/claude-usage.mjs +172 -0
  251. package/shared/codex-commands.d.mts +4 -0
  252. package/shared/codex-commands.mjs +17 -0
  253. package/shared/codex-import.d.mts +38 -0
  254. package/shared/codex-import.mjs +232 -0
  255. package/shared/codex-modes.mjs +159 -0
  256. package/shared/codex-usage.mjs +180 -0
  257. package/shared/contracts.mjs +412 -0
  258. package/shared/crash-scrub.mjs +182 -0
  259. package/shared/dashboard.mjs +481 -0
  260. package/shared/dead-run-trace.mjs +127 -0
  261. package/shared/demo-world.mjs +415 -0
  262. package/shared/effort-levels.d.mts +15 -0
  263. package/shared/effort-levels.mjs +76 -0
  264. package/shared/engines.d.mts +62 -0
  265. package/shared/engines.mjs +389 -0
  266. package/shared/first-run-examples.mjs +60 -0
  267. package/shared/first-run-line.mjs +149 -0
  268. package/shared/first-run-practice.d.mts +32 -0
  269. package/shared/first-run-practice.mjs +254 -0
  270. package/shared/first-run-shape.mjs +100 -0
  271. package/shared/fresh-user-home.mjs +189 -0
  272. package/shared/hook-failure.mjs +22 -0
  273. package/shared/install-id.mjs +39 -0
  274. package/shared/instruction-defaults.json +5 -0
  275. package/shared/library-folder.mjs +28 -0
  276. package/shared/notify-rules.mjs +120 -0
  277. package/shared/product-name.d.mts +6 -0
  278. package/shared/product-name.mjs +144 -0
  279. package/shared/project-folder-check.d.mts +22 -0
  280. package/shared/project-folder-check.mjs +152 -0
  281. package/shared/provider-commands.d.mts +8 -0
  282. package/shared/provider-commands.mjs +58 -0
  283. package/shared/rank.d.mts +19 -0
  284. package/shared/rank.mjs +146 -0
  285. package/shared/recovery.mjs +26 -0
  286. package/shared/referenced-files.d.mts +11 -0
  287. package/shared/referenced-files.mjs +122 -0
  288. package/shared/repeats.d.mts +45 -0
  289. package/shared/repeats.mjs +193 -0
  290. package/shared/schemes.mjs +33 -0
  291. package/shared/settings-terminal.d.mts +1 -0
  292. package/shared/settings-terminal.mjs +14 -0
  293. package/shared/side-build.mjs +57 -0
  294. package/shared/spawn-trouble.d.mts +8 -0
  295. package/shared/spawn-trouble.mjs +599 -0
  296. package/shared/terminal-state.d.mts +1 -0
  297. package/shared/terminal-state.mjs +58 -0
  298. package/shared/usage.d.mts +53 -0
  299. package/shared/usage.mjs +319 -0
  300. package/shared/work-items.mjs +561 -0
  301. package/shared/work-lines.mjs +360 -0
  302. package/worker-permissions.json +6 -0
package/main/serve.mjs ADDED
@@ -0,0 +1,262 @@
1
+ // THE SECOND FRONT DOOR: the same app, answering a browser tab.
2
+ //
3
+ // Agentbox is two halves. The screen, and the half that starts `claude -p`,
4
+ // reads the store, watches sessions and opens terminals. The second half is
5
+ // 27,000 lines of ordinary Node and has never cared what is drawing.
6
+ //
7
+ // Until now the only way to reach it was Electron's private channel between a
8
+ // window and the app, which a browser tab cannot knock on. This is the other
9
+ // door: the same `registerIpc` surface, the same 78 channels, reachable at
10
+ // http://127.0.0.1:<port> so an ordinary tab can use it.
11
+ //
12
+ // WHAT THIS IS NOT. It is not hosting and it is not the internet. It binds to
13
+ // the loopback address, which is the machine's name for itself, so nothing off
14
+ // this computer can reach it. The token below is what stops another PROGRAM on
15
+ // the same machine driving somebody's agents: a browser will happily let any
16
+ // page make a request to localhost, so the port alone is not a door key.
17
+ //
18
+ // HOW IT REUSES RATHER THAN REPEATS. `registerIpc` takes its three Electron
19
+ // things as an argument, so this file hands it plain Node stand-ins: an
20
+ // `ipcMain` that files each handler in a Map instead of wiring it to a window,
21
+ // an `app` with the two flags and the quit hook it reads, and a `dialog` that
22
+ // says "not here" until the browser folder picker exists. Every channel is then
23
+ // answered by the code the desktop app answers with.
24
+
25
+ import http from 'node:http';
26
+ import fs from 'node:fs';
27
+ import path from 'node:path';
28
+ import crypto from 'node:crypto';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ import { loadConfig } from './config.mjs';
32
+ import { Store } from './store.mjs';
33
+ import { Supervisor } from './supervisor.mjs';
34
+ import { registerIpc } from './ipc.mjs';
35
+
36
+ const here = path.dirname(fileURLToPath(import.meta.url));
37
+ const repoRoot = path.dirname(here);
38
+
39
+ /**
40
+ * The stand-in for Electron's `ipcMain`.
41
+ *
42
+ * It files handlers rather than wiring them, so the http layer can look one up
43
+ * by name. `on` and `handle` are the same thing here: Electron's `on` is the
44
+ * fire-and-forget half, and over http a caller who does not want the answer
45
+ * can simply ignore it.
46
+ */
47
+ export function collectingIpcMain() {
48
+ const handlers = new Map();
49
+ return {
50
+ handlers,
51
+ handle(channel, fn) { handlers.set(channel, fn); },
52
+ on(channel, fn) { handlers.set(channel, fn); },
53
+ removeHandler(channel) { handlers.delete(channel); },
54
+ };
55
+ }
56
+
57
+ /**
58
+ * The stand-in for the window.
59
+ *
60
+ * `registerIpc` pushes to the screen through `window.webContents.send`, seven
61
+ * channels of it. Here each push goes to every open listener instead, which is
62
+ * what the websocket half will hand to the tab.
63
+ */
64
+ export function broadcastingWindow() {
65
+ const listeners = new Set();
66
+ return {
67
+ listeners,
68
+ isDestroyed: () => false,
69
+ webContents: {
70
+ send(channel, ...args) {
71
+ for (const send of listeners) {
72
+ try { send({ channel, args }); } catch { /* a closed tab is not an error */ }
73
+ }
74
+ },
75
+ },
76
+ };
77
+ }
78
+
79
+ /**
80
+ * The stand-in for `app` and `dialog`.
81
+ *
82
+ * `app` is read for two flags and one quit hook. `isPackaged` is false because
83
+ * this is being run from a checkout or from npm, never from a signed bundle,
84
+ * and the screens that read it use it to decide whether to offer to open a
85
+ * second copy of a packaged app.
86
+ *
87
+ * `dialog` says there is no dialog, and that is all it says. It does NOT mean
88
+ * a tab cannot choose a folder: this process reads the disk perfectly well and
89
+ * `zero:list-folders` walks it, so the screen draws its own picker and comes
90
+ * back with a real absolute path. `noDialog` is the flag that tells it to.
91
+ * See main/folders.mjs.
92
+ */
93
+ export function nodeHost({ onQuit = () => {} } = {}) {
94
+ const quitHooks = [];
95
+ return {
96
+ ipcMain: collectingIpcMain(),
97
+ app: {
98
+ isPackaged: false,
99
+ getVersion: () => readVersion(),
100
+ on(event, fn) { if (event === 'before-quit') quitHooks.push(fn); },
101
+ quit() { for (const fn of quitHooks) { try { fn({ preventDefault() {} }); } catch {} } onQuit(); },
102
+ },
103
+ dialog: {
104
+ showOpenDialog: async () => ({ canceled: true, noDialog: true, filePaths: [] }),
105
+ },
106
+ };
107
+ }
108
+
109
+ function readVersion() {
110
+ try {
111
+ return JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')).version ?? '0.0.0';
112
+ } catch {
113
+ return '0.0.0';
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Boot the doing half, with no Electron anywhere.
119
+ *
120
+ * Six lines, and they are the same six `main/main.mjs` runs. `analytics` and
121
+ * `updater` are left at the defaults `registerIpc` already has for them: one
122
+ * sends nothing and the other reports that there is nothing to update, which
123
+ * is the truth for a copy run out of npm.
124
+ */
125
+ export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, userDir = dataDir } = {}) {
126
+ const config = loadConfig(dataDir);
127
+ const store = await new Store(config).init();
128
+ const supervisor = new Supervisor(config, store, appDir, dataDir, userDir);
129
+ const window = broadcastingWindow();
130
+ const host = nodeHost();
131
+ const ipc = registerIpc({ store, supervisor, config, window, host });
132
+ return { config, store, supervisor, window, host, ipc, channels: host.ipcMain.handlers };
133
+ }
134
+
135
+ /** A token in the url, because the port alone is not a door key. */
136
+ export const newToken = () => crypto.randomBytes(24).toString('base64url');
137
+
138
+ const MIME = {
139
+ '.html': 'text/html; charset=utf-8',
140
+ '.js': 'text/javascript; charset=utf-8',
141
+ '.css': 'text/css; charset=utf-8',
142
+ '.json': 'application/json; charset=utf-8',
143
+ '.svg': 'image/svg+xml',
144
+ '.png': 'image/png',
145
+ '.webp': 'image/webp',
146
+ '.woff2': 'font/woff2',
147
+ '.map': 'application/json; charset=utf-8',
148
+ };
149
+
150
+ /**
151
+ * Serve one built renderer and one channel endpoint.
152
+ *
153
+ * `POST /api/<channel>` with a json body calls the handler `registerIpc` filed
154
+ * under that name and answers with whatever it returned. That is the whole
155
+ * request half: 78 channels, one route.
156
+ *
157
+ * `listeners` is the Set off `broadcastingWindow`, which is where the pushes
158
+ * going the other way are waiting for somewhere to go.
159
+ */
160
+ export function createServer({ channels, token, listeners = new Set(), dist = path.join(repoRoot, 'renderer', 'dist') }) {
161
+ return http.createServer(async (req, res) => {
162
+ const url = new URL(req.url, 'http://127.0.0.1');
163
+
164
+ // The token rides in a header on api calls and in the query on the first
165
+ // page load, because a browser cannot put a header on a navigation.
166
+ const given = req.headers['x-agentbox-token'] || url.searchParams.get('token');
167
+ if (given !== token) {
168
+ res.writeHead(403, { 'content-type': 'text/plain' });
169
+ res.end('Wrong or missing token. Use the url the terminal printed.');
170
+ return;
171
+ }
172
+
173
+ // The phone line left open. Seven channels talk the other way, main to
174
+ // screen, and every one of them is one-way, so this is an event stream
175
+ // rather than a websocket: the same job with no dependency to add.
176
+ if (url.pathname === '/events') {
177
+ res.writeHead(200, {
178
+ 'content-type': 'text/event-stream',
179
+ 'cache-control': 'no-cache',
180
+ connection: 'keep-alive',
181
+ });
182
+ res.write('retry: 1000\n\n');
183
+ const send = (message) => res.write(`data: ${JSON.stringify(message)}\n\n`);
184
+ listeners.add(send);
185
+ // A proxy or a sleeping laptop will drop a silent connection, and the
186
+ // screen only finds out when a push goes missing. A comment every twenty
187
+ // seconds keeps it honest and costs nothing.
188
+ const beat = setInterval(() => res.write(': beat\n\n'), 20_000);
189
+ req.on('close', () => { clearInterval(beat); listeners.delete(send); });
190
+ return;
191
+ }
192
+
193
+ if (url.pathname.startsWith('/api/')) {
194
+ const channel = decodeURIComponent(url.pathname.slice('/api/'.length));
195
+ const fn = channels.get(channel);
196
+ if (!fn) {
197
+ res.writeHead(404, { 'content-type': 'application/json' });
198
+ res.end(JSON.stringify({ error: `No channel named ${channel}` }));
199
+ return;
200
+ }
201
+ let payload;
202
+ try {
203
+ const body = await readBody(req);
204
+ payload = body ? JSON.parse(body) : undefined;
205
+ } catch {
206
+ res.writeHead(400, { 'content-type': 'application/json' });
207
+ res.end(JSON.stringify({ error: 'Body was not json' }));
208
+ return;
209
+ }
210
+ try {
211
+ // The first argument is Electron's event object, which the handlers
212
+ // either ignore or read a sender off. Nothing here has a sender.
213
+ const value = await fn({ sender: null }, payload);
214
+ res.writeHead(200, { 'content-type': 'application/json' });
215
+ res.end(JSON.stringify(value ?? null));
216
+ } catch (err) {
217
+ res.writeHead(500, { 'content-type': 'application/json' });
218
+ res.end(JSON.stringify({ error: String(err?.message ?? err) }));
219
+ }
220
+ return;
221
+ }
222
+
223
+ // Anything else is the built screen. An unknown path is index.html, because
224
+ // the screen routes itself.
225
+ const rel = url.pathname === '/' ? '/index.html' : url.pathname;
226
+ const file = path.join(dist, path.normalize(rel).replace(/^(\.\.[/\\])+/, ''));
227
+ if (!file.startsWith(dist)) {
228
+ res.writeHead(403).end('No');
229
+ return;
230
+ }
231
+ fs.readFile(file, (err, buf) => {
232
+ if (err) {
233
+ fs.readFile(path.join(dist, 'index.html'), (err2, index) => {
234
+ if (err2) {
235
+ res.writeHead(404, { 'content-type': 'text/plain' });
236
+ res.end(`No built screen at ${dist}. Run: npm run build`);
237
+ return;
238
+ }
239
+ res.writeHead(200, { 'content-type': MIME['.html'] });
240
+ res.end(index);
241
+ });
242
+ return;
243
+ }
244
+ res.writeHead(200, { 'content-type': MIME[path.extname(file)] ?? 'application/octet-stream' });
245
+ res.end(buf);
246
+ });
247
+ });
248
+ }
249
+
250
+ function readBody(req) {
251
+ return new Promise((resolve, reject) => {
252
+ let body = '';
253
+ req.on('data', (c) => {
254
+ body += c;
255
+ // A screen never posts anything large; the uploads go through their own
256
+ // channels. This is here so a stuck client cannot eat the process.
257
+ if (body.length > 8 * 1024 * 1024) reject(new Error('Body too large'));
258
+ });
259
+ req.on('end', () => resolve(body));
260
+ req.on('error', reject);
261
+ });
262
+ }