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
@@ -0,0 +1,345 @@
1
+ // the app's config: one JSON file, all paths explicit, no discovery magic.
2
+ //
3
+ // The store's modules used to be another system's, loaded out of a private repo
4
+ // by absolute path, and this file was where that coupling was declared. They are
5
+ // Agentbox's own now (main/store/*), so the only outside thing left here is the
6
+ // claude binary.
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import os from 'node:os';
11
+ import crypto from 'node:crypto';
12
+ import { resolveClaudeBin, candidatePaths } from './claude-bin.mjs';
13
+ import { resolveCodexBin } from './codex-bin.mjs';
14
+ import { Name } from '../shared/product-name.mjs';
15
+ import { readPlans, slotsForPlans } from './claude-plan.mjs';
16
+ import { machineSlots } from './machine.mjs';
17
+ import { effectiveProfiles } from './account-discovery.mjs';
18
+ import { runningAsAFreshUser } from '../shared/fresh-user-home.mjs';
19
+ import { runningAsADemo } from '../shared/demo-world.mjs';
20
+
21
+ const CONFIG_NAME = 'zero.config.json';
22
+
23
+ /**
24
+ * HOW MANY SESSIONS ONE ACCOUNT RUNS WHEN NOBODY HAS SAID ANYTHING AT ALL.
25
+ *
26
+ * Exported because two places need the same number and a second copy of it is a
27
+ * number that drifts: `DEFAULTS` below, and `Supervisor#_slotsPerAccount`, which
28
+ * reaches for it as CODEX's cap when the number on the config is one a CLAUDE
29
+ * plan lowered. A Claude plan's rate limit says nothing about an OpenAI
30
+ * subscription, and there is no plan tier for Codex anywhere to read, so the
31
+ * honest answer for Codex is the one every engine gets when nobody has said --
32
+ * this, rather than a Codex-shaped number nobody measured.
33
+ */
34
+ export const DEFAULT_SESSIONS_AT_ONCE = 3;
35
+
36
+ const DEFAULTS = {
37
+ // Where the inbox's own files live. WHICH FOLDER THIS SHOULD BE ON A
38
+ // STRANGER'S MAC IS NOT DECIDED HERE: it is the open question on.
39
+ //
40
+ // IT IS THE APP'S OWN NAME SINCE 2026-09-22, where it used to be an older
41
+ // product's. A real store is always named in the config, so this default is
42
+ // only ever the answer for an install carrying none, and renaming it moved
43
+ // no files: the folder it used to point at is untouched and still on disk.
44
+ storeRoot: path.join(os.homedir(), Name),
45
+ // ONE ACCOUNT FOLDER PER INSTALL, AND NOBODY ELSE'S.
46
+ //
47
+ // This was a literal id for most of the app's life, which meant every copy
48
+ // ever downloaded filed its work under a folder named after the machine the
49
+ // app was written on. `null` is the honest default: `loadConfig` mints a
50
+ // fresh one on first run and writes it into the config, so an id exists from
51
+ // then on and is this install's own. An id already in the config always wins,
52
+ // so nothing that is already running moves.
53
+ accountId: null,
54
+ // The brain. Sessions run headless on the founder's subscription. Left unset
55
+ // on purpose: where Claude Code lives depends on how it was installed, so the
56
+ // answer is searched for at load (main/claude-bin.mjs) rather than assumed.
57
+ // Setting it here in zero.config.json still wins over the search.
58
+ claudeBin: null,
59
+ // How many sessions run at once. The queue absorbs the rest; when the plan
60
+ // window caps out, a longer queue is the visible, benign failure mode.
61
+ //
62
+ // THIS IS THE NUMBER FOR A MAX SUBSCRIPTION, which is the only kind this was
63
+ // ever chosen on. When nobody has set one, loadConfig below reads the plan
64
+ // Claude Code is signed in on and lowers this for a smaller one.
65
+ maxConcurrentSessions: DEFAULT_SESSIONS_AT_ONCE,
66
+ // What spawned sessions are allowed to do. Left unset on purpose, because the
67
+ // right default now depends on whether a store MCP server is present: see
68
+ // Supervisor#sessionArgsFor. Setting it here still wins over that, which is
69
+ // how letting workers build stays the founder's explicit opt-in.
70
+ sessionArgs: null,
71
+ // An OPTIONAL stdio MCP server giving spawned workers tools to read and write
72
+ // the store. Agentbox ships none, so this is null and a worker runs with no store
73
+ // tools at all. Point it at an executable to give workers their hands back.
74
+ storeMcpCommand: null,
75
+ // Products where agent-filed TASK items spawn without waiting for the
76
+ // founder's approval (questions and reviews still hold for them). Empty by
77
+ // default: autonomy here is granted per product, deliberately, in the
78
+ // config file, the same way sessionArgs is.
79
+ autonomousProducts: [],
80
+ // PERSONAL PROJECTS AND THE DRIVE WERE BOTH DECLARED HERE AND BOTH ARE GONE
81
+ // (w-d19d6d387c, 2026-09-22). A personal project ran its sessions on the
82
+ // founder's message and nothing else, with no brief and no store; the drive
83
+ // asked "what next?" by itself on a project that had gone quiet. Hers, on
84
+ // the same day: "I would get rid of 'personal' as well. I think there is no
85
+ // case where we should ever have that", and "we should get rid of the code
86
+ // that self-drives the product entirely. That should never have existed
87
+ // anyway, especially because we're open sourcing it soon."
88
+ //
89
+ // A config still carrying `personalProducts` or `driveEnabled` is not an
90
+ // error; nothing reads either key now.
91
+ // Products the app drives: slugs under the account root. Empty means all
92
+ // non-archived products found on disk.
93
+ products: [],
94
+ // WHETHER THE SECOND ENGINE MAY BE CHOSEN AT ALL, AND FROM WHEN. Null, so it
95
+ // may not, and that is the whole of the default: every row runs Claude Code,
96
+ // including one she marked `engine: "codex"` in August, exactly as it did
97
+ // before this key existed. There is no Settings row and no picker. This is a
98
+ // line somebody writes in zero.config.json on purpose, which is the point of
99
+ // it -- it is the config half of the capability gate in shared/engines.mjs,
100
+ // and `main/supervisor.mjs#_engineFor` is the one place that reads it and
101
+ // hands the gate's token over.
102
+ //
103
+ // IT IS THE MOMENT SHE OPENED IT AND NOT A `true`. The reasoning is in
104
+ // shared/engines.mjs, beside the function that parses this key, and is not
105
+ // repeated here; in one sentence, `engine` survived the 2026-08-27 removal on
106
+ // the work-item contract, so her ledger may still carry rows marked `codex`
107
+ // from 08-25 to 08-27, and a boolean would put every one of them onto a
108
+ // second engine the instant it was written, with no picker, no byline naming
109
+ // the engine and no warning. A moment means "Codex may be chosen from now on"
110
+ // instead.
111
+ //
112
+ // Written as an ISO date or date-time: "2026-09-04", or
113
+ // "2026-09-04T09:30:00Z". Anything else -- true, "yes", "1", or a shape
114
+ // `Date.parse` would merely guess at -- is read as off.
115
+ engineChoice: null,
116
+ };
117
+
118
+ /**
119
+ * `home` is the machine's home folder and exists so a test can be run against
120
+ * a laid-out one rather than against whoever is logged in. Nothing in the app
121
+ * passes it. */
122
+ export function loadConfig(appDir, { home = os.homedir() } = {}) {
123
+ const file = path.join(appDir, CONFIG_NAME);
124
+ let overrides = {};
125
+ // WHETHER THIS MAC HAS EVER RUN AGENTBOX BEFORE. There is exactly one honest
126
+ // marker of that and this is it: no config file at all. A file that exists
127
+ // but happens not to mention a setting is somebody who has been here for
128
+ // weeks, and treating them as new is how a default silently changes under
129
+ // an install that was working.
130
+ //
131
+ // NOTHING DECIDES A DEFAULT ON THIS ANY MORE. It used to pick the
132
+ // `outsideAgents` default, and that split was the bug: the same machine got
133
+ // two different answers about the same setting depending on whether a file
134
+ // happened to exist. There is one default now, in main/settings.mjs. This
135
+ // stays as a plain fact about the machine, read by nobody today, because the
136
+ // next thing that wants it will want it honest.
137
+ let fresh = false;
138
+ try {
139
+ overrides = JSON.parse(fs.readFileSync(file, 'utf8'));
140
+ } catch (err) {
141
+ if (err.code === 'ENOENT') fresh = true;
142
+ else console.warn(`zero: could not read ${file}: ${err.message}`);
143
+ }
144
+ const config = { ...DEFAULTS, ...overrides, freshInstall: fresh };
145
+ // An older alias for `storeRoot` was read here until 2026-09-22. A config
146
+ // still carrying only the old key falls through to the default now, which is
147
+ // why the key was renamed in zero.config.json in the same step and not after
148
+ // it: the two are one move, and doing them apart points the app at an empty
149
+ // folder.
150
+ // The account id, minted once per install. Written back immediately, because
151
+ // a generated id that is not saved is a different id next launch and the
152
+ // store would move out from under everything.
153
+ if (typeof config.accountId !== 'string' || !config.accountId) {
154
+ config.accountId = crypto.randomUUID();
155
+ try {
156
+ fs.mkdirSync(path.dirname(file), { recursive: true });
157
+ fs.writeFileSync(file, `${JSON.stringify({ ...overrides, accountId: config.accountId }, null, 2)}\n`);
158
+ } catch (err) {
159
+ console.warn(`zero: could not write the new account id to ${file}: ${err.message}`);
160
+ }
161
+ }
162
+ config.accountRoot = path.join(config.storeRoot, 'accounts', config.accountId);
163
+ // Where Claude Code is on THIS machine. Her setting wins; otherwise we look
164
+ // (main/claude-bin.mjs). What she SET is kept separately from what we FOUND,
165
+ // because `claudeBin` ends up filled in either way, and a later search that
166
+ // reads its own last answer back as a setting would never search again.
167
+ //
168
+ // When nothing is found at all, `claudeBin` falls back to the installer's
169
+ // path, so anything that tries to spawn it fails the way it always did, with
170
+ // a real path in the message, rather than on a null. `claudeFound` is the
171
+ // honest answer to whether it is there, and it is carried here so no screen
172
+ // has to re-derive it.
173
+ config.claudeBinConfigured = typeof overrides.claudeBin === 'string' && overrides.claudeBin
174
+ ? overrides.claudeBin
175
+ : null;
176
+ const claude = resolveClaudeBin(config.claudeBinConfigured);
177
+ config.claudeBin = claude.path ?? candidatePaths()[0];
178
+ config.claudeFound = claude.found;
179
+ config.claudeCertain = claude.certain !== false;
180
+ config.claudeFrom = claude.from;
181
+ // AND WHERE CODEX IS, THROUGH THE SAME DOOR AND FOR A SHARPER REASON.
182
+ //
183
+ // `codexBin` used to arrive on `config` by the unknown-key spread above: this
184
+ // file keeps whatever is in `zero.config.json` so a setting nobody has taught
185
+ // the app about survives a round trip, which is right for a setting and wrong
186
+ // for A PATH THAT GETS SPAWNED. `this.config.codexBin` reaches `spawn` in the
187
+ // supervisor raw, the file is gitignored and sits in the app directory, and
188
+ // every worker on this machine has a broad Bash allow. So the one path in the
189
+ // second engine that becomes a process was the one with no finder, no
190
+ // existence check and no `configured` split -- while `claudeBin`, three lines
191
+ // up, has had all three since it was written. main/codex-bin.mjs is that
192
+ // finder; until now nothing imported it.
193
+ //
194
+ // THE ONE ASYMMETRY IS DELIBERATE. Claude Code is required, so a miss falls
195
+ // back to the installer's path and anything spawning it fails with a real
196
+ // path in the message. Codex is OPTIONAL, so a miss is null: null is exactly
197
+ // what `engineFor` and `_capacityFor` read as "this Mac cannot run the second
198
+ // engine", and inventing a path there would advertise an engine that is not
199
+ // installed and turn every Codex row into an ENOENT.
200
+ config.codexBinConfigured = typeof overrides.codexBin === 'string' && overrides.codexBin
201
+ ? overrides.codexBin
202
+ : null;
203
+ const codex = resolveCodexBin(config.codexBinConfigured);
204
+ config.codexBin = codex.found ? codex.path : null;
205
+ config.codexFound = codex.found;
206
+ config.codexCertain = codex.certain !== false;
207
+ config.codexFrom = codex.from;
208
+ // HOW MANY AGENTS AT ONCE, WHEN NOBODY HAS SAID, IS THE PLAN AND THE MACHINE
209
+ // TOGETHER. Somebody on a smaller plan gets several sessions started for them,
210
+ // they all spend the same allowance, and what they see is agents that are slow
211
+ // and rows that fail. The founder's own bullet off a tester's onboarding was
212
+ // "not on max plan?". So a plan that is not Max answers 1 and that stands
213
+ // whatever the hardware is.
214
+ //
215
+ // HER OWN NUMBER ALWAYS WINS, including when it is the same as the default:
216
+ // this only fills in a number nobody set. And a Mac whose plan cannot be read
217
+ // keeps whatever the hardware suggests, so this can make an install smaller
218
+ // but never breaks one. `planSlotsFrom` is the account it came from, or null,
219
+ // so the Agents page can say why the number is what it is instead of leaving
220
+ // somebody to wonder why theirs says one.
221
+ // AND WHAT THIS MAC SUGGESTS IS WHAT THE PLAN FALLS BACK TO. The arithmetic
222
+ // and the measurement behind it are in main/machine.mjs; the short version is
223
+ // one agent per 4 GB of memory, which is the founder's own two anchors.
224
+ //
225
+ // THE MACHINE IS THE FALLBACK RATHER THAN A SECOND CAP, and the difference is
226
+ // the whole reason this reads the way it does. `slotsForPlans` answers 1 for a
227
+ // small subscription and its fallback for everything else, so handing it the
228
+ // machine number composes the two correctly in one expression: a Pro plan gets
229
+ // one at a time whatever the hardware, and everybody else gets what the
230
+ // hardware suggests. Taking `Math.min` of a flat 3 and the machine instead,
231
+ // which is what shipped first, held every Mac at 3 or below and so could never
232
+ // do the thing she asked for, which is scale up: "For 24 GB, have it scale
233
+ // accordingly."
234
+ //
235
+ // IT IS NOT A CAP. Nothing downstream clamps to this; the stepper and the
236
+ // writer both use the control's own range, the same on every machine. A number
237
+ // already in zero.config.json is untouched, for the same reason the plan cap
238
+ // leaves it alone.
239
+ config.machineSlots = machineSlots().slots;
240
+ if (typeof overrides.maxConcurrentSessions !== 'number') {
241
+ const plans = readPlans(effectiveProfiles(config.authProfiles, { home }), { home });
242
+ const slots = slotsForPlans(plans, config.machineSlots);
243
+ config.maxConcurrentSessions = slots;
244
+ config.planSlotsFrom = slots === config.machineSlots
245
+ ? null
246
+ : (plans.find((p) => p.known && !p.max)?.label ?? null);
247
+ } else {
248
+ config.planSlotsFrom = null;
249
+ }
250
+ // WHOSE MACHINE THIS IS, carried so nothing downstream has to ask the OS a
251
+ // second time and so a test can lay out a home of its own. The accounts the
252
+ // fleet runs on are read from it (main/account-discovery.mjs).
253
+ config.home = home;
254
+ // Where the file is, so anything that edits a setting can write it back
255
+ // without being handed appDir a second time.
256
+ config.appDir = appDir;
257
+ return config;
258
+ }
259
+
260
+ export function configFile(appDir) {
261
+ return path.join(appDir, CONFIG_NAME);
262
+ }
263
+
264
+ /**
265
+ * WHICH FOLDER `zero.config.json` IS READ FROM, and the whole of it is one
266
+ * rule: A COPY PRETENDING TO BE SOMEBODY ELSE MAY NOT READ THE CHECKOUT'S
267
+ * CONFIG.
268
+ *
269
+ * Running from source the config is the checkout's, which is what every session
270
+ * and every test already assumes. Packaged it cannot be, because the bundle is
271
+ * signed and a write into it breaks the signature notarisation just bought, so
272
+ * it is the user data folder.
273
+ *
274
+ * THE THIRD CASE IS THE ONE THAT WAS MISSING, AND IT IS WHAT SHE SAW. She
275
+ * pressed the ⌘K new user row on 2026-09-27 and the second window opened on HER
276
+ * INBOX, the personal rows among them, with no first run in front of it. A
277
+ * throwaway home moves the user data folder and it does NOT move the checkout,
278
+ * so on a source-run app the copy that was meant to be a stranger read
279
+ * `<checkout>/zero.config.json` and took her store root, her account id, her
280
+ * PostHog key and her session arguments off it. `shared/fresh-user-home.mjs`
281
+ * strips those names out of the child's ENVIRONMENT, which was the other half
282
+ * of the same leak; this is the half a file on disk causes, and no environment
283
+ * variable can fix it.
284
+ *
285
+ * The demo copy goes the same way, for the same reason rather than a second
286
+ * one: it seeds its own small store into the throwaway home and was then
287
+ * pointed straight past it at hers.
288
+ *
289
+ * Both marks are read off the environment the copy was launched with, so this
290
+ * answers correctly inside the child while staying a plain function a test can
291
+ * call with no Electron and no launch.
292
+ */
293
+ export function configDir({ appDir, userData, packaged = false, env = process.env } = {}) {
294
+ if (packaged) return userData;
295
+ return runningAsAFreshUser(env) || runningAsADemo(env) ? userData : appDir;
296
+ }
297
+
298
+ // Change settings from the app, on the founder's own file.
299
+ //
300
+ // Two halves, and both are required. The FILE is rewritten so the choice
301
+ // survives a restart, merging into whatever is on disk right now rather than
302
+ // into what this process read at boot: several sessions edit this repo, and a
303
+ // whole-object overwrite from stale memory is how an unrelated key silently
304
+ // reverts. The LIVE object is then mutated in place, because every consumer
305
+ // (the supervisor's tick, spawnPlan's grants, the digest) reads
306
+ // `config.*` per call, so a setting she changes takes effect on the next tick
307
+ // rather than at the next restart. A running app that ignores its own settings
308
+ // screen until relaunch is the same failure as a fix that is not running.
309
+ //
310
+ // Written through a rename, like the standing instructions: a half-written
311
+ // config is a fleet that boots with no grants at all.
312
+ export function saveConfig(config, patch) {
313
+ const appDir = config.appDir;
314
+ if (!appDir) throw new Error('config has no appDir; cannot save');
315
+ const file = configFile(appDir);
316
+ let onDisk = {};
317
+ try {
318
+ onDisk = JSON.parse(fs.readFileSync(file, 'utf8'));
319
+ } catch (err) {
320
+ if (err.code !== 'ENOENT') throw err;
321
+ }
322
+ const next = { ...onDisk };
323
+ // NOTHING IS PINNED HERE ANY MORE, and the deletion is the point. This used
324
+ // to write `outsideAgents` into a new install's first save, because the
325
+ // default flipped from "only the ones asking" to "every session" the moment a
326
+ // config file existed, and a stranger who had never opened the setting would
327
+ // have been flipped by it. There is one default now for every install, `off`
328
+ // in main/settings.mjs, so there is no flip left to guard against and no
329
+ // setting to write on somebody's behalf.
330
+ for (const [key, value] of Object.entries(patch)) {
331
+ // An explicit undefined REMOVES the key, which is how a per-project
332
+ // override goes back to meaning "same as the workspace" rather than
333
+ // hardening into an empty list that means something else entirely.
334
+ if (value === undefined) delete next[key];
335
+ else next[key] = value;
336
+ }
337
+ const tmp = `${file}.tmp-${process.pid}`;
338
+ fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
339
+ fs.renameSync(tmp, file);
340
+ for (const [key, value] of Object.entries(patch)) {
341
+ if (value === undefined) delete config[key];
342
+ else config[key] = value;
343
+ }
344
+ return next;
345
+ }
@@ -0,0 +1,55 @@
1
+ // "COPY" IN THE RIGHT-CLICK MENU ALWAYS PUTS SOMETHING ON THE CLIPBOARD.
2
+ //
3
+ // The founder, w-f7d2841075 (2026-09-26): right-click a link, press Copy, paste,
4
+ // and what comes out is whatever was on the clipboard before. Third report of
5
+ // the same thing (w-71f790a6eb, w-b108b1d596), and each earlier fix was
6
+ // measured on a bare test page where the copy did land.
7
+ //
8
+ // MEASURED this time on the real app: a seeded message, a real right-click on a
9
+ // link (on a Mac that selects the link's words, so "Copy" is the top item), and
10
+ // the real native menu item pressed through the accessibility API. The click
11
+ // handler ran, `contents.copy()` was called, and the clipboard still held the
12
+ // sentinel written before the click. "Copy Link Address", which writes the
13
+ // clipboard directly from here, landed every time.
14
+ //
15
+ // `contents.copy()` asks the page to run its own copy command, and whether that
16
+ // lands depends on focus and selection state inside the page at the moment the
17
+ // menu closes, which this process cannot see. The words she right-clicked on
18
+ // are already in hand: Chromium hands them over as `params.selectionText` at the
19
+ // moment of the click. So the page still gets the first go, because its copy
20
+ // carries the rich version (pictures inlined, our file links unwrapped, see
21
+ // renderer/src/copy-out.ts), and if nothing has landed shortly after, the words
22
+ // are written directly. Either way the old clipboard is gone.
23
+
24
+ /**
25
+ * Copy what a context menu was raised on.
26
+ *
27
+ * @param {{ copy: () => void }} contents the web contents the menu was raised on
28
+ * @param {string} selectionText `params.selectionText` from the context-menu event
29
+ * @param {{ clear: () => void, availableFormats: () => string[], writeText: (t: string) => void }} clipboard
30
+ * @param {{ waitMs?: number, stepMs?: number, sleep?: (ms: number) => Promise<void> }} [opts]
31
+ * @returns {Promise<'page' | 'direct'>} which of the two put it there
32
+ */
33
+ export async function copySelection(contents, selectionText, clipboard, opts = {}) {
34
+ const waitMs = opts.waitMs ?? 600;
35
+ const stepMs = opts.stepMs ?? 25;
36
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
37
+ // Nothing selected (a Copy in an empty text box) has no words to fall back
38
+ // on, and clearing would throw away her clipboard for nothing.
39
+ if (!selectionText) {
40
+ try { contents.copy(); } catch { /* nothing to copy anyway */ }
41
+ return 'page';
42
+ }
43
+ // Cleared first, so "something is on the clipboard" can only mean this copy
44
+ // put it there. The words are written below if the page does not, so a
45
+ // cleared clipboard never outlives this function.
46
+ clipboard.clear();
47
+ try { contents.copy(); } catch { /* a destroyed contents; the words still go */ }
48
+ for (let waited = 0; waited < waitMs; waited += stepMs) {
49
+ if (clipboard.availableFormats().length) return 'page';
50
+ await sleep(stepMs);
51
+ }
52
+ if (clipboard.availableFormats().length) return 'page';
53
+ clipboard.writeText(selectionText);
54
+ return 'direct';
55
+ }
@@ -0,0 +1,158 @@
1
+ // When Agentbox breaks on a stranger's Mac, somebody has to find out. Today
2
+ // nothing catches anything: the window closes, they stop opening it, and the
3
+ // only trace is one more download. Her yes, 2026-08-19.
4
+ //
5
+ // What this file is: the four ways the app can die, one scrubbed report each,
6
+ // and a queue on disk. What it is NOT, deliberately: a network client. Where
7
+ // reports go is her open pick on (PostHog from the main process, or Aptabase),
8
+ // and picking it here would be an agent choosing the third party that receives
9
+ // a stranger's crashes. So `setTransport` is the seam and until something is
10
+ // passed to it, a report is written to disk and nothing leaves the machine.
11
+ // That is also the honest default for a build we have not shipped yet.
12
+ //
13
+ // Every payload is built by `shared/crash-scrub.mjs`'s allowlist and then
14
+ // checked again by `findPrivate` before it is written. A report that fails the
15
+ // check is not sent minus the bad field; the whole report is dropped and a
16
+ // counter is written instead, because a scrub that quietly half-works is how a
17
+ // promise on the page turns into a lie.
18
+
19
+ import fs from 'node:fs';
20
+ import os from 'node:os';
21
+ import path from 'node:path';
22
+ import { findPrivate, scrubError, scrubText } from '../shared/crash-scrub.mjs';
23
+ import { readInstallId } from '../shared/install-id.mjs';
24
+ import { NAME, nameSlug } from '../shared/product-name.mjs';
25
+
26
+ const MAX_KEPT = 50;
27
+
28
+ let ctx = null; // { home, username, appDir, dir, version, startedAt }
29
+ let transport = null; // set once she picks where reports go
30
+ let installId = null;
31
+ let fired = false; // one hard crash is one report, never a loop
32
+
33
+ // The same random per-install id the approved counts use, and it now lives in
34
+ // ONE file (`shared/install-id.mjs`) because two copies of "it is not a
35
+ // fingerprint" is one copy too many. No account, no email, no machine id.
36
+
37
+ export function report(kind, err, extra = {}) {
38
+ if (!ctx) return null;
39
+ const payload = {
40
+ v: 1,
41
+ kind, // where it died, not what was on screen
42
+ ts: new Date().toISOString(),
43
+ installId,
44
+ app: { name: NAME, version: ctx.version },
45
+ os: { platform: process.platform, release: os.release(), arch: process.arch },
46
+ // How long it had been open. Useful (a crash at 3 seconds is a boot bug and
47
+ // a crash at 3 hours is a leak) and carries nothing about anyone.
48
+ upMs: Date.now() - ctx.startedAt,
49
+ error: scrubError(err, ctx),
50
+ };
51
+ // Extras are scrubbed exactly like the error, and only strings and numbers
52
+ // survive at all, so no future caller can widen this by passing an object.
53
+ for (const [k, v] of Object.entries(extra)) {
54
+ if (typeof v === 'number') payload[k] = v;
55
+ else if (typeof v === 'string') payload[k] = scrubText(v, ctx);
56
+ }
57
+
58
+ const leaks = findPrivate(payload, ctx);
59
+ if (leaks.length) {
60
+ // The scrub failed. Say that we crashed and say nothing else.
61
+ return write({ v: 1, kind: 'redacted', ts: payload.ts, installId, app: payload.app, os: payload.os, reason: 'scrub check failed', fields: leaks.length });
62
+ }
63
+ return write(payload);
64
+ }
65
+
66
+ function write(payload) {
67
+ try {
68
+ fs.mkdirSync(ctx.dir, { recursive: true });
69
+ const file = path.join(ctx.dir, `crash-${payload.ts.replace(/[:.]/g, '-')}-${Math.floor(Math.random() * 1e6)}.json`);
70
+ fs.writeFileSync(file, JSON.stringify(payload, null, 2));
71
+ prune();
72
+ // No await, no retry, no queue drain here: this runs while the process is
73
+ // dying. Delivery is the transport's problem, on the next boot if it has to be.
74
+ if (transport) { try { transport(payload); } catch {} }
75
+ return file;
76
+ } catch { return null; }
77
+ }
78
+
79
+ function prune() {
80
+ try {
81
+ const files = fs.readdirSync(ctx.dir).filter((f) => f.startsWith('crash-')).sort();
82
+ for (const f of files.slice(0, Math.max(0, files.length - MAX_KEPT))) {
83
+ fs.unlinkSync(path.join(ctx.dir, f));
84
+ }
85
+ } catch {}
86
+ }
87
+
88
+ // Her pick on lands here and nowhere else.
89
+ export function setTransport(fn) { transport = typeof fn === 'function' ? fn : null; }
90
+
91
+ export function pending() {
92
+ try {
93
+ return fs.readdirSync(ctx.dir).filter((f) => f.startsWith('crash-')).map((f) => path.join(ctx.dir, f));
94
+ } catch { return []; }
95
+ }
96
+
97
+ export function installCrashReports({ app, appDir, dir, version, crashReporter } = {}) {
98
+ ctx = {
99
+ home: os.homedir(),
100
+ username: (() => { try { return os.userInfo().username; } catch { return null; } })(),
101
+ // realpath, because macOS hands `/tmp` back as `/private/tmp` and a stack
102
+ // frame written the other way then reads as somebody else's code and gets
103
+ // dropped. The frames inside our bundle are the entire point of the report.
104
+ appDir: (() => { const d = appDir || process.cwd(); try { return fs.realpathSync(d); } catch { return d; } })(),
105
+ dir: dir || path.join(os.tmpdir(), `${nameSlug}-crash-reports`),
106
+ version: version || '0.0.0',
107
+ startedAt: Date.now(),
108
+ };
109
+ installId = readInstallId(ctx.dir);
110
+
111
+ // Native crashes (the renderer's own process dying, a GPU fault) leave a
112
+ // minidump rather than a JS stack. `uploadToServer: false` is not a setting
113
+ // that could be flipped by accident: with no destination decided there is no
114
+ // submit URL to flip it to, so the dumps sit in the app's own folder.
115
+ if (crashReporter) {
116
+ try { crashReporter.start({ uploadToServer: false, compress: true, ignoreSystemCrashHandler: false }); } catch {}
117
+ }
118
+
119
+ const onUncaught = (err) => {
120
+ if (fired) return;
121
+ fired = true;
122
+ report('main-uncaught', err);
123
+ // Put the default back rather than living on in a state Electron never
124
+ // designed for. A reporter that silently swallows a fatal error turns "it
125
+ // crashed" into "it froze", which is worse for the person holding the Mac.
126
+ process.removeListener('uncaughtException', onUncaught);
127
+ throw err;
128
+ };
129
+ process.on('uncaughtException', onUncaught);
130
+
131
+ const onRejection = (reason) => {
132
+ report('main-rejection', reason instanceof Error ? reason : new Error(String(reason)));
133
+ };
134
+ process.on('unhandledRejection', onRejection);
135
+
136
+ if (app?.on) {
137
+ // The window's process died. `details.reason` is Electron's own vocabulary
138
+ // (`crashed`, `oom`, `killed`) and carries nothing of theirs.
139
+ app.on('render-process-gone', (_e, _wc, details = {}) => {
140
+ report('renderer-gone', new Error('render process gone'), { reason: String(details.reason || 'unknown'), exitCode: Number(details.exitCode ?? -1) });
141
+ });
142
+ app.on('child-process-gone', (_e, details = {}) => {
143
+ report('child-gone', new Error('child process gone'), { reason: String(details.reason || 'unknown'), processType: String(details.type || 'unknown'), exitCode: Number(details.exitCode ?? -1) });
144
+ });
145
+ }
146
+ return { report, pending, dir: ctx.dir };
147
+ }
148
+
149
+ // The renderer's uncaught errors arrive here over IPC. They are the ones most
150
+ // likely to carry a task title inside the message, which is why they go through
151
+ // exactly the same scrub as everything else and get no shortcut of their own.
152
+ export function reportFromRenderer(payload = {}) {
153
+ return report('renderer-error', {
154
+ name: typeof payload.name === 'string' ? payload.name : 'Error',
155
+ message: typeof payload.message === 'string' ? payload.message : 'renderer error',
156
+ stack: typeof payload.stack === 'string' ? payload.stack : '',
157
+ });
158
+ }