agentbox-app 0.1.12 → 0.1.16

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 (370) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/acp.mjs +296 -0
  9. package/main/agent-homes.mjs +158 -0
  10. package/main/agent-sessions.mjs +311 -260
  11. package/main/agent-updates.mjs +9 -22
  12. package/main/agents.mjs +179 -466
  13. package/main/analytics.mjs +42 -2
  14. package/main/answer-modes.mjs +103 -37
  15. package/main/approval-prompt-core.mjs +197 -0
  16. package/main/approval-prompt-server.mjs +4 -138
  17. package/main/approvals.mjs +101 -297
  18. package/main/artifact-path.mjs +47 -18
  19. package/main/claude-bin.mjs +45 -311
  20. package/main/claude-input.mjs +57 -7
  21. package/main/claude-launch-env.mjs +54 -0
  22. package/main/claude-models.mjs +39 -7
  23. package/main/claude-plan.mjs +64 -16
  24. package/main/claude-session-open.mjs +28 -0
  25. package/main/claude-usage-process.mjs +40 -0
  26. package/main/claude-usage.mjs +94 -59
  27. package/main/cloud/account.mjs +179 -0
  28. package/main/cloud/aws.mjs +127 -0
  29. package/main/cloud/browser-return.mjs +58 -0
  30. package/main/cloud/clients.mjs +15 -0
  31. package/main/cloud/codex-cloud.mjs +53 -0
  32. package/main/cloud/config.mjs +107 -0
  33. package/main/cloud/docs.mjs +219 -0
  34. package/main/cloud/exec.mjs +27 -0
  35. package/main/cloud/host.mjs +165 -0
  36. package/main/cloud/kit.mjs +80 -0
  37. package/main/cloud/logins.mjs +543 -0
  38. package/main/cloud/managed.mjs +115 -0
  39. package/main/cloud/marker.mjs +104 -0
  40. package/main/cloud/org.mjs +53 -0
  41. package/main/cloud/reuse.mjs +208 -0
  42. package/main/cloud/run-cli.mjs +177 -0
  43. package/main/cloud/run.mjs +1934 -0
  44. package/main/cloud/usage.mjs +76 -0
  45. package/main/cloud-permission.mjs +51 -0
  46. package/main/codex-account.mjs +78 -0
  47. package/main/codex-app-server.mjs +276 -409
  48. package/main/codex-approvals.mjs +148 -433
  49. package/main/codex-bin.mjs +51 -249
  50. package/main/codex-models.mjs +20 -2
  51. package/main/codex-session.mjs +246 -506
  52. package/main/codex-threads-worker.mjs +12 -0
  53. package/main/codex-threads.mjs +112 -11
  54. package/main/codex-usage-file.mjs +5 -3
  55. package/main/codex-usage.mjs +56 -7
  56. package/main/codex-watch.mjs +83 -5
  57. package/main/codex.mjs +95 -363
  58. package/main/config.mjs +37 -5
  59. package/main/corner-tag.mjs +810 -0
  60. package/main/crash-report.mjs +26 -3
  61. package/main/dev-window.mjs +1 -1
  62. package/main/engine-bin.mjs +545 -0
  63. package/main/engine-setup.mjs +25 -24
  64. package/main/feedback.mjs +14 -31
  65. package/main/folder-pictures.mjs +2 -2
  66. package/main/freeze-recorder-thread.mjs +86 -0
  67. package/main/freeze-recorder.mjs +146 -0
  68. package/main/fresh-user.mjs +20 -9
  69. package/main/git-change-offthread.mjs +17 -3
  70. package/main/git-change-thread.mjs +5 -2
  71. package/main/harnesses/claude-stream.mjs +208 -0
  72. package/main/harnesses/claude.mjs +105 -0
  73. package/main/harnesses/codex.mjs +72 -0
  74. package/main/harnesses/common.mjs +78 -0
  75. package/main/harnesses/instructions-file.mjs +116 -0
  76. package/main/harnesses/opencode.mjs +234 -0
  77. package/main/harnesses.mjs +65 -0
  78. package/main/importable-threads.mjs +96 -0
  79. package/main/ipc.mjs +520 -135
  80. package/main/leftovers.mjs +117 -25
  81. package/main/liquid-glass.mjs +26 -0
  82. package/main/live-replies.mjs +10 -4
  83. package/main/local-app-frames.mjs +82 -0
  84. package/main/main-went-red.mjs +330 -0
  85. package/main/main.mjs +248 -37
  86. package/main/memory-gate-history.mjs +85 -20
  87. package/main/memory-gate-server.mjs +129 -11
  88. package/main/memory-gate.mjs +62 -8
  89. package/main/memory-log.mjs +124 -0
  90. package/main/menu.mjs +4 -2
  91. package/main/message-priority.mjs +2 -2
  92. package/main/mic.mjs +38 -0
  93. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  94. package/main/new-chats-watch.mjs +110 -0
  95. package/main/new-user-window.mjs +25 -0
  96. package/main/notify.mjs +64 -5
  97. package/main/opencode-server.mjs +271 -0
  98. package/main/opencode.mjs +287 -0
  99. package/main/phone-alerts.mjs +117 -0
  100. package/main/phone-inbox.mjs +625 -0
  101. package/main/phone-link.mjs +571 -0
  102. package/main/phone-uploads.mjs +476 -0
  103. package/main/picture-shape.mjs +114 -0
  104. package/main/quit-sequence.mjs +143 -0
  105. package/main/refresh-agent-models.mjs +15 -2
  106. package/main/relay-address.mjs +28 -0
  107. package/main/relay-client.mjs +314 -0
  108. package/main/repeats.mjs +10 -4
  109. package/main/row-label.mjs +18 -47
  110. package/main/run-change.mjs +13 -0
  111. package/main/run-ledger.mjs +135 -0
  112. package/main/runs-on.mjs +32 -35
  113. package/main/save-attachments.mjs +41 -0
  114. package/main/serve.mjs +155 -9
  115. package/main/settings.mjs +364 -50
  116. package/main/shell-path.mjs +234 -0
  117. package/main/ship-conflict-brief.mjs +119 -0
  118. package/main/ship-queue.mjs +134 -20
  119. package/main/sign-in-files.mjs +3 -7
  120. package/main/source-updater.mjs +45 -11
  121. package/main/stage-attachment.mjs +68 -0
  122. package/main/stall-watch.mjs +50 -0
  123. package/main/start-hold.mjs +73 -0
  124. package/main/store/home.mjs +87 -232
  125. package/main/store/paths.mjs +11 -0
  126. package/main/store/teammates-files.mjs +99 -0
  127. package/main/store/work-items.mjs +231 -13
  128. package/main/store.mjs +227 -53
  129. package/main/supervisor.mjs +3865 -699
  130. package/main/task-commands.mjs +13 -17
  131. package/main/task-folders-offthread.mjs +6 -1
  132. package/main/task-folders.mjs +897 -105
  133. package/main/task-terminals.mjs +147 -41
  134. package/main/team/index.mjs +114 -10
  135. package/main/team/memory-cloud.mjs +183 -1
  136. package/main/team/room-hours.mjs +48 -0
  137. package/main/team/room-setup.mjs +31 -0
  138. package/main/team/rooms-memory.mjs +454 -0
  139. package/main/team/rooms-shape.mjs +42 -0
  140. package/main/team/rooms-supabase.mjs +324 -0
  141. package/main/team/rooms.mjs +575 -0
  142. package/main/team/session.mjs +96 -26
  143. package/main/team/supabase-backend.mjs +180 -0
  144. package/main/team/sync.mjs +109 -1
  145. package/main/teammates.mjs +580 -0
  146. package/main/terminal-host.mjs +143 -0
  147. package/main/terminal-hosts.mjs +261 -0
  148. package/main/transcript-search.mjs +80 -0
  149. package/main/updater.mjs +71 -9
  150. package/main/webview-guard.mjs +45 -0
  151. package/main/worktree-include.mjs +365 -0
  152. package/mcp/core/documents.mjs +3 -1
  153. package/mcp/core/repeats.mjs +88 -0
  154. package/mcp/core/teammates.mjs +222 -0
  155. package/mcp/core/work.mjs +76 -4
  156. package/mcp/stdio.mjs +24 -11
  157. package/mcp/tools.mjs +116 -8
  158. package/package.json +30 -5
  159. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
  160. package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
  161. package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
  162. package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
  163. package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
  164. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  165. package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
  166. package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
  167. package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
  168. package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
  169. package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
  170. package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
  171. package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
  172. package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
  173. package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
  174. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
  175. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  176. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  177. package/renderer/dist/assets/cose-bilkent-JH36ORCC-CbkyZOUH.js +1 -0
  178. package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
  179. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
  180. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  181. package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
  182. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  183. package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
  184. package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
  185. package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
  186. package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
  187. package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
  188. package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
  189. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
  190. package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
  191. package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
  192. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
  193. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
  194. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
  195. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  196. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
  197. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  198. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
  199. package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
  200. package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
  201. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  202. package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
  203. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  204. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  205. package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
  206. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
  207. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  208. package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
  209. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
  210. package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
  211. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
  212. package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
  213. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
  214. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
  215. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
  216. package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
  217. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
  218. package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
  219. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  220. package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
  221. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
  222. package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
  223. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
  224. package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.js +7 -0
  225. package/renderer/dist/corner-tag.html +13 -0
  226. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  227. package/renderer/dist/emulator/data/compression/README.md +11 -0
  228. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  229. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  230. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  231. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  232. package/renderer/dist/emulator/data/cores/README.md +22 -0
  233. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  234. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  235. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  236. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  237. package/renderer/dist/emulator/data/cores/package.json +65 -0
  238. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  239. package/renderer/dist/emulator/data/emulator.css +1614 -0
  240. package/renderer/dist/emulator/data/loader.js +170 -0
  241. package/renderer/dist/emulator/data/localization/README.md +63 -0
  242. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  243. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  244. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  245. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  246. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  247. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  248. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  249. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  250. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  251. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  252. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  253. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  254. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  255. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  256. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  257. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  258. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  259. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  260. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  261. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  262. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  263. package/renderer/dist/emulator/data/src/compression.js +142 -0
  264. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  265. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  266. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  267. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  268. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  269. package/renderer/dist/emulator/data/src/storage.js +125 -0
  270. package/renderer/dist/emulator/data/version.json +5 -0
  271. package/renderer/dist/emulator/host.js +297 -0
  272. package/renderer/dist/emulator/index.html +28 -0
  273. package/renderer/dist/emulator/provenance.json +6 -0
  274. package/renderer/dist/index.html +16 -3
  275. package/scripts/after-install.mjs +30 -8
  276. package/scripts/approval-server.sh +24 -5
  277. package/scripts/before-publish.mjs +12 -10
  278. package/scripts/cloud-shim.pl +57 -0
  279. package/shared/account-names.d.mts +6 -0
  280. package/shared/account-names.mjs +15 -0
  281. package/shared/agent-hours.mjs +136 -0
  282. package/shared/agent-import.mjs +62 -34
  283. package/shared/agents.d.mts +1 -0
  284. package/shared/agents.mjs +126 -522
  285. package/shared/analytics-events.mjs +12 -0
  286. package/shared/approval-card.mjs +36 -0
  287. package/shared/bridge-map.mjs +45 -1
  288. package/shared/claude-code-shape.mjs +6 -0
  289. package/shared/claude-commands.generated.mjs +6 -4
  290. package/shared/claude-models.generated.mjs +6 -4
  291. package/shared/claude-usage.d.mts +1 -0
  292. package/shared/claude-usage.mjs +42 -1
  293. package/shared/cloud-hand-back.d.mts +7 -0
  294. package/shared/cloud-hand-back.mjs +20 -0
  295. package/shared/cloud-kit.d.mts +219 -0
  296. package/shared/cloud-kit.mjs +341 -0
  297. package/shared/cloud-login-name.mjs +4 -0
  298. package/shared/cloud-start.mjs +17 -0
  299. package/shared/cloud-versions.mjs +23 -0
  300. package/shared/codex-import.mjs +2 -2
  301. package/shared/codex-modes.d.mts +22 -9
  302. package/shared/codex-modes.mjs +67 -11
  303. package/shared/codex-usage.mjs +108 -2
  304. package/shared/corner-tag.d.mts +36 -0
  305. package/shared/corner-tag.mjs +396 -0
  306. package/shared/dead-run-trace.mjs +19 -2
  307. package/shared/engines.mjs +76 -300
  308. package/shared/feedback.d.mts +7 -0
  309. package/shared/feedback.mjs +25 -0
  310. package/shared/first-run-practice.mjs +17 -24
  311. package/shared/harness-definitions.mjs +111 -0
  312. package/shared/instruction-defaults.json +2 -2
  313. package/shared/key-caps.d.mts +14 -0
  314. package/shared/key-caps.mjs +68 -0
  315. package/shared/message-edits.d.mts +7 -0
  316. package/shared/message-edits.mjs +35 -0
  317. package/shared/move-thread.d.mts +24 -0
  318. package/shared/move-thread.mjs +112 -0
  319. package/shared/named-threads.mjs +72 -0
  320. package/shared/not-shipped.d.mts +22 -0
  321. package/shared/not-shipped.mjs +89 -0
  322. package/shared/notify-rules.d.mts +31 -0
  323. package/shared/notify-rules.mjs +55 -8
  324. package/shared/phone-files.mjs +28 -0
  325. package/shared/phone-settings.mjs +118 -0
  326. package/shared/phone-snapshot.d.mts +16 -0
  327. package/shared/phone-snapshot.mjs +348 -0
  328. package/shared/picture-formats.d.mts +1 -0
  329. package/shared/picture-formats.mjs +13 -0
  330. package/shared/provider-commands.mjs +10 -2
  331. package/shared/rank.d.mts +4 -1
  332. package/shared/rank.mjs +82 -19
  333. package/shared/relay-address.mjs +6 -0
  334. package/shared/relay-crypto.mjs +165 -0
  335. package/shared/repeats.d.mts +8 -0
  336. package/shared/repeats.mjs +33 -2
  337. package/shared/room-cards.mjs +167 -0
  338. package/shared/room-links.d.mts +4 -0
  339. package/shared/room-links.mjs +21 -0
  340. package/shared/runs-on.d.mts +3 -13
  341. package/shared/runs-on.mjs +31 -52
  342. package/shared/shipped-note.d.mts +4 -0
  343. package/shared/shipped-note.mjs +6 -0
  344. package/shared/shortcuts.mjs +24 -10
  345. package/shared/spawn-trouble.d.mts +4 -0
  346. package/shared/spawn-trouble.mjs +484 -30
  347. package/shared/team-rules.mjs +9 -2
  348. package/shared/teammates.d.mts +19 -0
  349. package/shared/teammates.mjs +756 -0
  350. package/shared/terminal-focus.d.mts +1 -0
  351. package/shared/terminal-focus.mjs +13 -0
  352. package/shared/terminal-input.d.mts +1 -0
  353. package/shared/terminal-input.mjs +30 -0
  354. package/shared/terminal-keys.d.mts +1 -0
  355. package/shared/terminal-keys.mjs +16 -0
  356. package/shared/terminal-size.d.mts +4 -0
  357. package/shared/terminal-size.mjs +29 -0
  358. package/shared/thread-cards.mjs +4 -1
  359. package/shared/usage.d.mts +12 -0
  360. package/shared/usage.mjs +56 -3
  361. package/shared/websites.d.mts +15 -0
  362. package/shared/websites.mjs +61 -0
  363. package/shared/where-it-runs-from.d.mts +43 -0
  364. package/shared/where-it-runs-from.mjs +155 -0
  365. package/shared/work-items.mjs +150 -29
  366. package/shared/work-lines.mjs +93 -245
  367. package/shared/xterm-invalid-range.d.mts +2 -0
  368. package/shared/xterm-invalid-range.mjs +58 -0
  369. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  370. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -0,0 +1,13 @@
1
+ // WHAT A RUN CHANGED: the checkout read back against its photograph, then the
2
+ // change file written. Git commands and a whole transcript, so it runs on the
3
+ // git thread (main/git-change-offthread.mjs), not on the app's main thread.
4
+ import { changeFromRepo } from './git-change.mjs';
5
+ import { writeChangeForRun } from './code-change.mjs';
6
+
7
+ export function saveRunChange({ repoBefore = null, ...run }) {
8
+ let repos = [];
9
+ try {
10
+ if (repoBefore) repos = [changeFromRepo(repoBefore)];
11
+ } catch (e) { console.warn('zero: could not read the repository back:', e.message); }
12
+ return writeChangeForRun({ ...run, repos });
13
+ }
@@ -0,0 +1,135 @@
1
+ // THE RUN LEDGER: one line per finished run, for agent hours (w-3d0fa23ffb).
2
+ //
3
+ // `<appHome>/runs.jsonl`, written when a run ends (supervisor `_recordRun`).
4
+ // Cloud runs keep their own ledger (main/cloud/usage.mjs) and are read from
5
+ // there. Old runs are read once from their trace logs (`backfillRuns`). Every
6
+ // line has a key, and every reader dedupes on it, so a run written twice is
7
+ // counted once.
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { appHome, machineryDir } from './store/home.mjs';
11
+ import { dedupeRuns, runFromTrace } from '../shared/agent-hours.mjs';
12
+
13
+ const BACKFILL_DAYS = 400;
14
+ const TAIL_BYTES = 400;
15
+ // A log with no exit line that was written to this recently is a run still
16
+ // going; it writes its own line when it ends.
17
+ const STILL_GOING_MS = 30 * 60_000;
18
+
19
+ export function runLedgerPath(home = appHome()) {
20
+ return path.join(home, 'runs.jsonl');
21
+ }
22
+
23
+ export function appendRun(run, { file = runLedgerPath() } = {}) {
24
+ try {
25
+ fs.mkdirSync(path.dirname(file), { recursive: true });
26
+ fs.appendFileSync(file, JSON.stringify(run) + '\n');
27
+ return true;
28
+ } catch {
29
+ // A run that cannot be written never fails the run.
30
+ return false;
31
+ }
32
+ }
33
+
34
+ export function readRuns({ file = runLedgerPath() } = {}) {
35
+ let text;
36
+ try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
37
+ const out = [];
38
+ for (const line of text.split('\n')) {
39
+ if (!line.trim()) continue;
40
+ try { out.push(JSON.parse(line)); } catch { /* one broken line loses only itself */ }
41
+ }
42
+ return out;
43
+ }
44
+
45
+ export function runsFromCloudUsage(lines) {
46
+ const out = [];
47
+ for (const l of lines ?? []) {
48
+ const at = Date.parse(l?.at);
49
+ const seconds = Number(l?.wallSeconds);
50
+ if (!Number.isFinite(at) || !(seconds > 0)) continue;
51
+ out.push({
52
+ key: `cloud:${l.runId ?? at}`,
53
+ at,
54
+ start: at - seconds * 1000,
55
+ item: l.item ?? null,
56
+ product: null,
57
+ engine: l.engine ?? null,
58
+ seconds,
59
+ failed: Number.isInteger(l.exitCode) ? l.exitCode !== 0 : null,
60
+ where: 'cloud',
61
+ });
62
+ }
63
+ return out;
64
+ }
65
+
66
+ export function allRuns({ file = runLedgerPath(), cloudLines = [] } = {}) {
67
+ return dedupeRuns([...readRuns({ file }), ...runsFromCloudUsage(cloudLines)]);
68
+ }
69
+
70
+ function tailOf(file) {
71
+ let fd;
72
+ try {
73
+ fd = fs.openSync(file, 'r');
74
+ const size = fs.fstatSync(fd).size;
75
+ const len = Math.min(size, TAIL_BYTES);
76
+ const buf = Buffer.alloc(len);
77
+ fs.readSync(fd, buf, 0, len, size - len);
78
+ return buf.toString('utf8');
79
+ } catch {
80
+ return '';
81
+ } finally {
82
+ if (fd !== undefined) try { fs.closeSync(fd); } catch {}
83
+ }
84
+ }
85
+
86
+ // Reads every run log of this store's projects into the ledger, once per run.
87
+ // Resolves to how many runs it added. It hands the event loop back every
88
+ // YIELD_EVERY logs: on the founder's Mac it reads 25,780 logs in 3.4 s, which
89
+ // in one piece would freeze the window.
90
+ const YIELD_EVERY = 200;
91
+ const breathe = () => new Promise((r) => setImmediate(r));
92
+
93
+ export async function backfillRuns({ home = appHome(), products = [], file = runLedgerPath(home), now = Date.now() } = {}) {
94
+ const have = new Set(readRuns({ file }).map((r) => r?.key));
95
+ const since = now - BACKFILL_DAYS * 86_400_000;
96
+ let added = 0;
97
+ let read = 0;
98
+ let pending = [];
99
+ const flush = () => {
100
+ if (!pending.length) return;
101
+ try {
102
+ fs.mkdirSync(path.dirname(file), { recursive: true });
103
+ fs.appendFileSync(file, pending.map((r) => JSON.stringify(r)).join('\n') + '\n');
104
+ added += pending.length;
105
+ } catch { /* the next backfill tries again */ }
106
+ pending = [];
107
+ };
108
+ for (const p of products) {
109
+ if (!p?.dir) continue;
110
+ const sessions = path.join(machineryDir(p.dir, home), 'sessions');
111
+ let items;
112
+ try { items = fs.readdirSync(sessions, { withFileTypes: true }); } catch { continue; }
113
+ for (const it of items) {
114
+ if (!it.isDirectory()) continue;
115
+ let logs;
116
+ try { logs = fs.readdirSync(path.join(sessions, it.name)); } catch { continue; }
117
+ for (const name of logs) {
118
+ const start = Number(/^(\d{12,14})\.log$/.exec(name)?.[1]);
119
+ if (!Number.isFinite(start) || start < since || start > now) continue;
120
+ if (++read % YIELD_EVERY === 0) { flush(); await breathe(); }
121
+ const full = path.join(sessions, it.name, name);
122
+ let mtimeMs;
123
+ try { mtimeMs = fs.statSync(full).mtimeMs; } catch { continue; }
124
+ const tail = tailOf(full);
125
+ if (!/# exited \(/.test(tail) && now - mtimeMs < STILL_GOING_MS) continue;
126
+ const run = runFromTrace({ name, text: tail, mtimeMs, item: it.name, product: p.slug ?? null });
127
+ if (!run || have.has(run.key)) continue;
128
+ pending.push({ ...run, backfilled: true });
129
+ have.add(run.key);
130
+ }
131
+ }
132
+ }
133
+ flush();
134
+ return added;
135
+ }
package/main/runs-on.mjs CHANGED
@@ -1,21 +1,23 @@
1
- // WHICH ACCOUNT THE AGENTS ARE ACTUALLY RUNNING ON, read off this Mac.
1
+ // WHETHER THIS MAC IS ALREADY SIGNED INTO A CODING AGENT, AND WHICH ONE.
2
2
  //
3
- // w-e217e577e5, 2026-10-07, from a session with a new user: "He didn't realize
4
- // it auto-connected to Claude/Codex; he wasn't sure how it was even running."
3
+ // w-e217e577e5, 2026-10-07. A new user ran an agent and, in her words, was
4
+ // "like, 'Oh my God, where are the tokens coming from? I didn't even connect my
5
+ // account.'" The app finds whichever coding agent is already signed in on the
6
+ // Mac and spends that subscription without ever saying so.
5
7
  //
6
- // Every fact here was already on the disk and already being read, each by the
7
- // screen that needed it: main/claude-plan.mjs for the Claude plan (the Accounts
8
- // page), main/codex-account.mjs for the ChatGPT one (the Codex card). What did
9
- // not exist was ONE answer to "what is powering this", in a shape small enough
10
- // to ride the snapshot and be drawn in a corner. That is all this is: the two
11
- // readers, asked about the one account the fleet is spending, and null whenever
12
- // the answer would be a guess.
8
+ // WHICH ENGINE, AND NOTHING ELSE. This read the plan tier too for a few hours,
9
+ // so a row in the sidebar's foot could say "Claude · Max 20x"; that row came
10
+ // out the same day and the tier came out with it ("very critically, I want to
11
+ // get rid of that Claude Max 20x plan"). The question the walk answers is "did
12
+ // it connect to something of mine", and the engine is the whole of that answer.
13
+ // What somebody is paying for is on each agent's own page in Settings, which is
14
+ // where you go when you actually want it (main/settings.mjs reads
15
+ // main/claude-plan.mjs and main/codex-account.mjs for exactly that).
13
16
  //
14
- // NULL IS AN ORDINARY ANSWER AND IT MEANS DRAW NOTHING. A Mac mid-setup, a
15
- // config file being rewritten under us, a login that exists with no plan string
16
- // in it: the first two are null, the third is the name with no plan. Nothing
17
- // here ever reports an account that is not there, because the corner it feeds
18
- // is read by somebody deciding whose money this is spending.
17
+ // NULL IS AN ORDINARY ANSWER AND IT MEANS SAY NOTHING. A Mac mid-setup, a config
18
+ // file being rewritten under us, a login that is not there: all null, and the
19
+ // walk then says nothing at all rather than guessing. That Mac is also the one
20
+ // that gets the plan question instead, which says all of this out loud already.
19
21
  //
20
22
  // IT IS CACHED ON THE LOGIN FILES' OWN TIMES, NOT ON A CLOCK. The snapshot is
21
23
  // built every ten seconds and `~/.claude.json` is 148 KB on the machine this was
@@ -30,11 +32,11 @@
30
32
  import fs from 'node:fs';
31
33
  import os from 'node:os';
32
34
  import path from 'node:path';
33
- import { planFile, planOfAccount } from './claude-plan.mjs';
35
+ import { planFile } from './claude-plan.mjs';
34
36
  import { codexAccount, codexAuthFile } from './codex-account.mjs';
35
37
  import { signInFiles, signInStamp } from './sign-in-files.mjs';
36
38
 
37
- /** keyed by engine and folder, valued by the stamp it was read at. */
39
+ /** keyed by engine, home and folder, valued by the stamp it was read at. */
38
40
  const remembered = new Map();
39
41
 
40
42
  /**
@@ -43,12 +45,11 @@ const remembered = new Map();
43
45
  * @param {object} facts
44
46
  * @param {string} facts.engine Which coding agent the workspace runs on, which
45
47
  * the supervisor has already decided (`Supervisor#engineFacts`). Never worked
46
- * out here: the capability gate lives there, and a corner that guessed could
48
+ * out here: the capability gate lives there, and a line that guessed could
47
49
  * name the subscription the fleet is not spending.
48
50
  * @param {string} [facts.claudeProfile] The Claude login, `default` or a folder.
49
51
  * @param {string} [facts.codexHome] The CODEX_HOME this workspace runs on.
50
- * @returns {{ engine: string, plan: string|null }|null} `plan` is the words the
51
- * tool itself reports, never a label of ours.
52
+ * @returns {{ engine: string }|null}
52
53
  */
53
54
  export function runsOn({ engine = null, home = os.homedir(), claudeProfile = 'default', codexHome = null } = {}) {
54
55
  if (engine !== 'claude' && engine !== 'codex') return null;
@@ -63,28 +64,24 @@ export function runsOn({ engine = null, home = os.homedir(), claudeProfile = 'de
63
64
  const stamp = signInStamp(signInFiles({ engine, folder, home }));
64
65
  const seen = remembered.get(key);
65
66
  if (seen && seen.stamp === stamp) return seen.value;
66
- const value = stamp ? read(engine, { home, claudeProfile, folder }) : null;
67
+ const value = stamp && signedIn(engine, { home, claudeProfile, folder }) ? { engine } : null;
67
68
  remembered.set(key, { stamp, value });
68
69
  return value;
69
70
  }
70
71
 
71
- function read(engine, { home, claudeProfile, folder }) {
72
- if (engine === 'codex') {
73
- const id = codexAccount(null, { file: codexAuthFile(folder) });
74
- return id ? { engine: 'codex', plan: id.plan ?? null } : null;
75
- }
76
- // THE LOGIN AND THE PLAN COME OUT OF ONE READ. `readPlan` answers only the
77
- // second, and "not signed in" and "signed in with no plan string" are two
78
- // different screens: the first draws nothing, the second draws the name.
79
- let account = null;
72
+ /** Whether there is a login in there at all. Never throws, never guesses. */
73
+ function signedIn(engine, { home, claudeProfile, folder }) {
74
+ if (engine === 'codex') return !!codexAccount(null, { file: codexAuthFile(folder) });
75
+ // `oauthAccount` is the object Claude Code writes when somebody signs in, and
76
+ // its absence is the honest "nobody is". A file with no account in it is an
77
+ // ordinary thing: `~/.claude.json` also holds project history, so it exists on
78
+ // plenty of Macs nobody has logged in on.
80
79
  try {
81
- account = JSON.parse(fs.readFileSync(planFile(claudeProfile, home), 'utf8'))?.oauthAccount ?? null;
80
+ const account = JSON.parse(fs.readFileSync(planFile(claudeProfile, home), 'utf8'))?.oauthAccount;
81
+ return !!account && typeof account === 'object';
82
82
  } catch {
83
- return null;
83
+ return false;
84
84
  }
85
- if (!account || typeof account !== 'object') return null;
86
- const plan = planOfAccount(account);
87
- return { engine: 'claude', plan: plan.known ? plan.label : null };
88
85
  }
89
86
 
90
87
  /** Forgets every cached reading. For tests, and for a sign-out that lands. */
@@ -0,0 +1,41 @@
1
+ // SAVING THE FILES A MESSAGE CARRIES, into a project's attachments folder.
2
+ //
3
+ // One copy, used by the reply box (main/ipc.mjs zero:save-attachments) and by
4
+ // a message to a person (main/team/index.mjs), whose conversation may not
5
+ // exist until the message makes it, so it cannot save them beforehand.
6
+ // Relative paths keep them readable by workers through the MCP and renderable
7
+ // in the message view, and they are the names the team sync sends on
8
+ // (main/team/sync.mjs).
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ /**
13
+ * `files` are `{ name, dataBase64 }` or `{ name, srcPath }`. Answers what was
14
+ * saved: `{ rel, name, image }`, in order, skipping one with neither.
15
+ */
16
+ export function saveAttachments(dir, files) {
17
+ const outDir = path.join(dir, 'attachments');
18
+ fs.mkdirSync(outDir, { recursive: true });
19
+ const saved = [];
20
+ for (const file of files ?? []) {
21
+ const safe = String(file?.name ?? 'file').replace(/[^\w.-]+/g, '-').slice(0, 80);
22
+ // A RANDOM SUFFIX, THE SAME ONE THE STAGING PATH ALREADY USES. The name
23
+ // was the millisecond plus the filename, so two files saved in the same
24
+ // millisecond with the same name overwrote each other and the message
25
+ // ended up with two links to one picture. Screenshots are often sent in
26
+ // pairs, which is exactly the shape that collides. It has never been observed
27
+ // happening; it is one line to make impossible.
28
+ const rel = `attachments/${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${safe}`;
29
+ const abs = path.join(dir, rel);
30
+ if (file?.dataBase64) fs.writeFileSync(abs, Buffer.from(file.dataBase64, 'base64'));
31
+ else if (file?.srcPath) fs.copyFileSync(file.srcPath, abs);
32
+ else continue;
33
+ saved.push({ rel, name: safe, image: /\.(png|jpe?g|gif|webp)$/i.test(safe) });
34
+ }
35
+ return saved;
36
+ }
37
+
38
+ /** The links a message carries for what was saved, one per line. */
39
+ export function attachmentMarkdown(saved) {
40
+ return saved.map((s) => (s.image ? `![${s.name}](${s.rel})` : `[${s.name}](${s.rel})`)).join('\n');
41
+ }
package/main/serve.mjs CHANGED
@@ -29,6 +29,9 @@ import crypto from 'node:crypto';
29
29
  import { fileURLToPath } from 'node:url';
30
30
 
31
31
  import { spawn } from 'node:child_process';
32
+ import { Readable } from 'node:stream';
33
+ import { mediaResponse, mediaType, servable } from './img-scheme.mjs';
34
+ import { readPictureShape } from './picture-shape.mjs';
32
35
  import { loadConfig } from './config.mjs';
33
36
  import { Store } from './store.mjs';
34
37
  import { Supervisor } from './supervisor.mjs';
@@ -38,7 +41,9 @@ import { reportFromRenderer } from './crash-report.mjs';
38
41
  import * as workItemsDisk from './store/work-items.mjs';
39
42
  import { createTeamService, teamStateFile } from './team/index.mjs';
40
43
  import { loadCloudConfig, supabaseSession, headlessSessionFile } from './team/session.mjs';
44
+ import { roomsSetup } from './team/room-setup.mjs';
41
45
  import { appHome, carryMisplacedMachinery, storeRootEnv } from './store/home.mjs';
46
+ import { keepAnswerModesIn } from './answer-modes.mjs';
42
47
 
43
48
  const here = path.dirname(fileURLToPath(import.meta.url));
44
49
  const repoRoot = path.dirname(here);
@@ -136,11 +141,16 @@ function readVersion() {
136
141
  * same scrubbed path as the desktop's, which writes nothing until crash
137
142
  * reports are set up for this process.
138
143
  */
139
- function answerWhatOnlyTheWindowAnswered(ipcMain) {
144
+ export function answerWhatOnlyTheWindowAnswered(ipcMain) {
140
145
  ipcMain.handle('zero:badge', () => null);
146
+ ipcMain.handle('zero:corner-tag', () => null);
141
147
  ipcMain.handle('zero:notify', () => null);
142
148
  ipcMain.handle('zero:boot-info', () => ({ reloaded: false, builtAt: null, recovered: null }));
149
+ // A room invite link only ever opens the desktop app.
150
+ ipcMain.handle('zero:take-join', () => null);
143
151
  ipcMain.handle('zero:crash', (_e, payload) => { reportFromRenderer(payload ?? {}); return null; });
152
+ // The phone door belongs to the desktop app (main/phone-link.mjs); a tab is told there is none.
153
+ ipcMain.handle('zero:phone-link', () => null);
144
154
  }
145
155
 
146
156
  /**
@@ -170,6 +180,7 @@ export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, user
170
180
  const inheritedHome = appHome();
171
181
  Object.assign(process.env, storeRootEnv(config.storeRoot));
172
182
  carryMisplacedMachinery(inheritedHome, config.storeRoot);
183
+ keepAnswerModesIn({ userDir, storeRoot: config.storeRoot });
173
184
  const store = await new Store(config).init();
174
185
  const supervisor = new Supervisor(config, store, appDir, dataDir, userDir);
175
186
  const window = broadcastingWindow();
@@ -191,7 +202,15 @@ export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, user
191
202
  accountRoot: config.accountRoot,
192
203
  stateFile: teamStateFile(config.storeRoot),
193
204
  onChange: () => ipc?.push?.(),
205
+ rooms: roomsSetup({ config, store, supervisor, userDir }),
194
206
  }) : null;
207
+ // AND IT STOPS ITS OWN WORKERS ON THE WAY OUT, as main/main.mjs does for the
208
+ // desktop. Control-C reaches the whole process group, so without this each
209
+ // worker died of the signal itself and was written down as a run that failed
210
+ // on its own: a Claude task stopped that way read "it will keep failing the
211
+ // same way until this is fixed" and rested 15 minutes after the copy came
212
+ // back (tests/control-c-on-the-browser-copy-does-not-fail-its-agents).
213
+ host.app.on('before-quit', () => supervisor.killAll());
195
214
  ipc = registerIpc({ store, supervisor, config, window, host, team });
196
215
  answerWhatOnlyTheWindowAnswered(host.ipcMain);
197
216
  return { config, store, supervisor, window, host, ipc, team, channels: host.ipcMain.handlers };
@@ -200,18 +219,98 @@ export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, user
200
219
  /** A token in the url, because the port alone is not a door key. */
201
220
  export const newToken = () => crypto.randomBytes(24).toString('base64url');
202
221
 
222
+ /**
223
+ * THE PICTURES A TAB MAY DRAW: the desktop window's own rule (`servable`,
224
+ * main/img-scheme.mjs) over its own roots (`imageRoots`, main/main.mjs), so a
225
+ * door opened for a screenshot can never hand out `zero.config.json`.
226
+ *
227
+ * Pictures only. A whole document is served under a grant the pane opens one
228
+ * at a time (main/doc-scheme.mjs) and this copy opens none.
229
+ */
230
+ export function picturesTheAppOwns({ config, store }) {
231
+ return {
232
+ allows: (kind, file) => kind === 'img' && servable(file, [
233
+ config?.accountRoot,
234
+ ...(store?.listProducts?.() ?? []).map((p) => p.repoPath),
235
+ ].filter(Boolean)),
236
+ };
237
+ }
238
+
239
+ /**
240
+ * THE KEY THAT ONLY READS FILES. A design page runs its own scripts and can
241
+ * read its own address, so the address a phone loads it from must not carry
242
+ * the phone's key, which starts agents and approves their requests. This is
243
+ * made from that key, changes when it is reset, and opens nothing but /f/.
244
+ */
245
+ export const fileKey = (token) => crypto.createHmac('sha256', String(token ?? '')).update('agentbox-files').digest('base64url').slice(0, 32);
246
+
203
247
  const MIME = {
204
248
  '.html': 'text/html; charset=utf-8',
249
+ '.htm': 'text/html; charset=utf-8',
205
250
  '.js': 'text/javascript; charset=utf-8',
251
+ '.mjs': 'text/javascript; charset=utf-8',
206
252
  '.css': 'text/css; charset=utf-8',
207
253
  '.json': 'application/json; charset=utf-8',
208
254
  '.svg': 'image/svg+xml',
209
255
  '.png': 'image/png',
256
+ '.jpg': 'image/jpeg',
257
+ '.jpeg': 'image/jpeg',
258
+ '.gif': 'image/gif',
210
259
  '.webp': 'image/webp',
260
+ '.avif': 'image/avif',
261
+ '.bmp': 'image/bmp',
262
+ '.ico': 'image/x-icon',
263
+ '.pdf': 'application/pdf',
264
+ '.txt': 'text/plain; charset=utf-8',
265
+ '.md': 'text/plain; charset=utf-8',
211
266
  '.woff2': 'font/woff2',
267
+ '.woff': 'font/woff',
268
+ '.ttf': 'font/ttf',
212
269
  '.map': 'application/json; charset=utf-8',
213
270
  };
214
271
 
272
+ /**
273
+ * One file off this computer, for the phone: /f/<file key>/<doc|img>/<path>.
274
+ * `files.allows(kind, file)` is the same rule the desktop's two schemes use
275
+ * (main/main.mjs passes it in), checked after the path is made whole, so a
276
+ * `..` cannot climb out of what it allows. Never from memory, for the reason
277
+ * the desktop's schemes give: a picture an agent redraws keeps its name.
278
+ */
279
+ async function answerFile(req, res, url, { token, files }) {
280
+ const [, , key, kind, ...rest] = url.pathname.split('/');
281
+ if (kind !== 'doc' && kind !== 'img' && kind !== 'shape') { res.writeHead(404).end('No such kind of file.'); return; }
282
+ if (!sameKey(key, fileKey(typeof token === 'function' ? token() : token))) { res.writeHead(403).end('Wrong key.'); return; }
283
+ let file;
284
+ try { file = path.normalize('/' + rest.map(decodeURIComponent).join('/')); } catch { res.writeHead(400).end('Bad path.'); return; }
285
+ // A shape is asked of a picture, under the picture's own rule.
286
+ if (!path.isAbsolute(file) || !files.allows(kind === 'shape' ? 'img' : kind, file)) { res.writeHead(403).end('Not available.'); return; }
287
+ let stat;
288
+ try { stat = await fs.promises.stat(file); } catch { res.writeHead(404).end('Not there.'); return; }
289
+ if (!stat.isFile()) { res.writeHead(404).end('Not there.'); return; }
290
+ // ITS WIDTH AND HEIGHT, so a picture the phone has never seen holds its
291
+ // place before its last byte comes (main/picture-shape.mjs).
292
+ if (kind === 'shape') {
293
+ const shape = await readPictureShape(file).catch(() => null);
294
+ if (!shape) { res.writeHead(404).end('Not a picture this can measure.'); return; }
295
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
296
+ res.end(JSON.stringify(shape));
297
+ return;
298
+ }
299
+ const media = mediaType(file);
300
+ if (media) {
301
+ const out = await mediaResponse(file, req.headers.range);
302
+ res.writeHead(out.status, { ...Object.fromEntries(out.headers), 'cache-control': 'no-store' });
303
+ if (out.body) Readable.fromWeb(out.body).pipe(res); else res.end();
304
+ return;
305
+ }
306
+ res.writeHead(200, {
307
+ 'content-type': MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream',
308
+ 'content-length': String(stat.size),
309
+ 'cache-control': 'no-store',
310
+ });
311
+ fs.createReadStream(file).pipe(res);
312
+ }
313
+
215
314
  /**
216
315
  * Serve one built renderer and one channel endpoint.
217
316
  *
@@ -222,10 +321,27 @@ const MIME = {
222
321
  * `listeners` is the Set off `broadcastingWindow`, which is where the pushes
223
322
  * going the other way are waiting for somewhere to go.
224
323
  */
225
- export function createServer({ channels, token, listeners = new Set(), dist = path.join(repoRoot, 'renderer', 'dist') }) {
226
- return http.createServer(async (req, res) => {
324
+ export function createServer(options) {
325
+ return http.createServer(createHandler(options));
326
+ }
327
+
328
+ /**
329
+ * The same door as a bare request handler, so main/phone-link.mjs can put its
330
+ * own server in front of it. `token` may be a function, read on every request,
331
+ * so a key that is reset stops working at once.
332
+ */
333
+ export function createHandler({ channels, token, listeners = new Set(), dist = path.join(repoRoot, 'renderer', 'dist'), files = null }) {
334
+ return async (req, res) => {
227
335
  const url = new URL(req.url, 'http://127.0.0.1');
228
336
 
337
+ // A file for the phone, behind the file key rather than the phone's own.
338
+ // Only a server that was handed `files` has any to give.
339
+ if (url.pathname.startsWith('/f/')) {
340
+ if (!files) { res.writeHead(404).end('No files here.'); return; }
341
+ await answerFile(req, res, url, { token, files });
342
+ return;
343
+ }
344
+
229
345
  // The token rides in a header on api calls and in the query on the event
230
346
  // stream, because a browser cannot put a header on either a navigation or
231
347
  // an EventSource. It guards the two doors that reach the store. The built
@@ -234,14 +350,29 @@ export function createServer({ channels, token, listeners = new Set(), dist = pa
234
350
  // NOR DOES THE PAGE ITSELF ASK: the tab takes the token out of its address
235
351
  // bar on load, so a reload asks for `/` bare, and refusing that drew
236
352
  // "Wrong or missing token" on every reload (tests/a-browser-tab-survives-a-reload).
237
- const guarded = url.pathname === '/events' || url.pathname.startsWith('/api/');
353
+ //
354
+ // AND THE HOST HEADER IS NOT CHECKED, asked and answered 2026-10-24.
355
+ // Refusing a Host that is not localhost is the usual guard against a page
356
+ // whose own name resolves to this machine, and it cannot go here:
357
+ // main/phone-link.mjs serves this same handler to a phone through a relay
358
+ // or over Tailscale, where the Host is neither. The token is the key,
359
+ // whatever name the request arrives under
360
+ // (tests/the-doing-half-runs-with-no-electron).
361
+ const guarded = url.pathname === '/events' || url.pathname.startsWith('/api/') || url.pathname === '/file-key';
238
362
  const given = req.headers['x-agentbox-token'] || url.searchParams.get('token');
239
- if (guarded && given !== token) {
363
+ if (guarded && !sameKey(given, typeof token === 'function' ? token() : token)) {
240
364
  res.writeHead(403, { 'content-type': 'text/plain' });
241
365
  res.end('Wrong or missing token. Use the url the terminal printed.');
242
366
  return;
243
367
  }
244
368
 
369
+ // The phone asks for its file key once, with its own key.
370
+ if (url.pathname === '/file-key') {
371
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
372
+ res.end(JSON.stringify({ key: fileKey(typeof token === 'function' ? token() : token) }));
373
+ return;
374
+ }
375
+
245
376
  // The phone line left open. Seven channels talk the other way, main to
246
377
  // screen, and every one of them is one-way, so this is an event stream
247
378
  // rather than a websocket: the same job with no dependency to add.
@@ -316,7 +447,19 @@ export function createServer({ channels, token, listeners = new Set(), dist = pa
316
447
  res.writeHead(200, { 'content-type': MIME[path.extname(file)] ?? 'application/octet-stream' });
317
448
  res.end(buf);
318
449
  });
319
- });
450
+ };
451
+ }
452
+
453
+ /**
454
+ * The key a request carries against the real one, in the same time whatever
455
+ * it gets right, so nobody can learn the key a character at a time by timing
456
+ * the refusals. Both sides are hashed first because a length that differs is
457
+ * itself a thing `timingSafeEqual` would refuse at once.
458
+ */
459
+ export function sameKey(given, key) {
460
+ if (typeof given !== 'string' || typeof key !== 'string' || !given || !key) return false;
461
+ const digest = (s) => crypto.createHash('sha256').update(s).digest();
462
+ return crypto.timingSafeEqual(digest(given), digest(key));
320
463
  }
321
464
 
322
465
  function readBody(req) {
@@ -324,9 +467,12 @@ function readBody(req) {
324
467
  let body = '';
325
468
  req.on('data', (c) => {
326
469
  body += c;
327
- // A screen never posts anything large; the uploads go through their own
328
- // channels. This is here so a stuck client cannot eat the process.
329
- if (body.length > 8 * 1024 * 1024) reject(new Error('Body too large'));
470
+ // This is here so a stuck client cannot eat the process. A PHONE'S
471
+ // UPLOADS DO COME THROUGH HERE, though: it has no file paths, so a
472
+ // picture is posted whole, as base64, to zero:stage-attachment. 8 MB of
473
+ // that was a 6 MB photo; an iPhone's is often more (2026-10-12). The
474
+ // relay passes up to 32 MB (relay/worker.mjs).
475
+ if (body.length > 32 * 1024 * 1024) reject(new Error('Body too large'));
330
476
  });
331
477
  req.on('end', () => resolve(body));
332
478
  req.on('error', reject);