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,240 @@
1
+ // THE APP UPDATES ITSELF, so nobody is ever asked to download it twice.
2
+ //
3
+ // There is a signed, notarised build on astral.ac now, so the horse is in
4
+ // front.
5
+ //
6
+ // WHAT IT DOES, in the order it happens. A few seconds after the window is up
7
+ // it asks GitHub whether there is a newer Agentbox. If there is, it downloads it
8
+ // quietly in the background, and when the download finishes it says so in
9
+ // Settings and in ⌘K. Nothing restarts under her. The restart is the button she
10
+ // pressed, which is the half she asked for by name.
11
+ //
12
+ // FOUR THINGS ABOUT THE SHAPE, each load-bearing:
13
+ //
14
+ // 1. IT NEVER OPENS A DIALOG AND NEVER STEALS FOCUS. electron-updater will pop
15
+ // a native box on some paths if you let it; every one of those is off here.
16
+ // An app that interrupts her to tell her it is healthy is the thing her
17
+ // whole inbox exists to stop.
18
+ // 2. IT IS SILENT WHEN IT FAILS. A laptop on a plane, a GitHub outage, a
19
+ // corporate proxy: none of those are her problem and none of them get a
20
+ // banner. The failure is recorded, `state` reports it, and the Settings row
21
+ // is the one place that ever says so, because that is the screen you open
22
+ // when you want to know something.
23
+ // 3. IT DOES NOTHING AT ALL FROM SOURCE. `autoUpdater.isUpdaterActive` is false
24
+ // when `app.isPackaged` is false, so a dev run skips the check rather than
25
+ // erroring against a feed it has no business reading. We report that state
26
+ // as `unsupported` with a reason rather than pretending to be idle.
27
+ // 4. IT INSTALLS ON QUIT TOO. If she never presses the button and just quits,
28
+ // the downloaded update is applied then, so the next launch is current. The
29
+ // button is the fast path, not the only path.
30
+ //
31
+ // WHERE THE UPDATE COMES FROM. `Astral-Agent/astral-releases`, the same public
32
+ // repo astral.ac/download already redirects to, configured in package.json's
33
+ // `build.publish`. electron-updater reads `releases/latest`, then
34
+ // `latest-mac.yml` on that release, and compares its `version` against this
35
+ // build's. It downloads the `.zip`, NOT the `.dmg`: Squirrel. Mac swaps an .app
36
+ // bundle and cannot read a disk image, which is why `build.mac.target` gained a
37
+ // zip beside the dmg and why a release that ships only a dmg fails with
38
+ // ERR_UPDATER_ZIP_FILE_NOT_FOUND. `scripts/publish-download.mjs` is what puts
39
+ // all three files on the release together.
40
+ //
41
+ // AND THE ONE THING THAT IS NOT TRUE ANY MORE. `main/analytics.mjs` opens by
42
+ // calling itself the only place anything leaves this machine. Since this file
43
+ // exists that is no longer so: an update check is a request to github.com. It
44
+ // carries no identity, no key and no account, and it is not routed through
45
+ // PostHog, but it IS traffic, and `legal/privacy.html` should say so in a
46
+ // sentence. That page is in the website repo, not this one.
47
+
48
+ import { createRequire } from 'node:module';
49
+ import { Name } from '../shared/product-name.mjs';
50
+
51
+ const require = createRequire(import.meta.url);
52
+
53
+ // How long after launch the first check happens. Long enough that it is never
54
+ // competing with the window painting or the supervisor waking its sessions up.
55
+ const FIRST_CHECK_MS = 20_000;
56
+ // And then this often, for the app somebody leaves open for days, which is the
57
+ // normal way Agentbox is used.
58
+ const EVERY_MS = 6 * 60 * 60 * 1000;
59
+
60
+ /**
61
+ * PURE, and the whole of what the screens are allowed to know.
62
+ *
63
+ * Kept separate from the class below so the states can be tested without an
64
+ * Electron app, a network, or a packaged build.
65
+ */
66
+ export function describe(state) {
67
+ const { phase, currentVersion, newVersion, percent, error, checkedAt, readyAt } = state;
68
+ return {
69
+ phase,
70
+ currentVersion,
71
+ newVersion: newVersion ?? null,
72
+ // Whole percent, because a progress number with decimals on it reads as
73
+ // machinery rather than as an answer.
74
+ percent: typeof percent === 'number' ? Math.max(0, Math.min(100, Math.round(percent))) : null,
75
+ error: error ?? null,
76
+ checkedAt: checkedAt ?? null,
77
+ // WHEN THE DOWNLOAD FINISHED, which is the moment the news is true. The
78
+ // inbox row this feeds wears it at its right end like every other row, and
79
+ // every other row's right end answers "how old is this". Built off the
80
+ // clock instead it would read "now" forever, which is the app looking busy
81
+ // about something that has been sitting on the disk since lunchtime.
82
+ readyAt: phase === 'ready' ? (readyAt ?? null) : null,
83
+ // The one question every screen actually asks. A screen should not have to
84
+ // learn which phase strings mean "there is a button to press".
85
+ ready: phase === 'ready',
86
+ };
87
+ }
88
+
89
+ export function createUpdater({ app, log = console, onChanged = () => {} }) {
90
+ let state = {
91
+ phase: 'idle',
92
+ currentVersion: app.getVersion(),
93
+ newVersion: null,
94
+ percent: null,
95
+ error: null,
96
+ checkedAt: null,
97
+ readyAt: null,
98
+ };
99
+ let timer = null;
100
+ let stopped = false;
101
+
102
+ const set = (next) => {
103
+ state = { ...state, ...next };
104
+ try { onChanged(describe(state)); } catch {}
105
+ };
106
+
107
+ // Loaded lazily and inside a try, so a build that somehow shipped without the
108
+ // dependency degrades to "no updates" instead of failing to boot at all. The
109
+ // app not starting is a strictly worse outcome than the app not updating.
110
+ let autoUpdater = null;
111
+ try {
112
+ ({ autoUpdater } = require('electron-updater'));
113
+ } catch (e) {
114
+ set({ phase: 'unsupported', error: 'This build has no updater in it.' });
115
+ log.warn?.('[updater] electron-updater is not available:', e?.message ?? e);
116
+ }
117
+
118
+ if (autoUpdater) {
119
+ // Quiet by construction. `autoDownload` is the only one of these that is
120
+ // already the default; the rest are here because their defaults interrupt.
121
+ autoUpdater.autoDownload = true;
122
+ autoUpdater.autoInstallOnAppQuit = true;
123
+ autoUpdater.allowPrerelease = false;
124
+ // electron-updater logs to console by default, which is fine, but its
125
+ // `logger` is also what it uses to report errors it swallows. Ours keeps
126
+ // them out of the crash reporter and in the terminal where a build is
127
+ // being watched.
128
+ autoUpdater.logger = {
129
+ info: (...a) => log.info?.('[updater]', ...a),
130
+ warn: (...a) => log.warn?.('[updater]', ...a),
131
+ error: (...a) => log.error?.('[updater]', ...a),
132
+ debug: () => {},
133
+ };
134
+
135
+ autoUpdater.on('checking-for-update', () => set({ phase: 'checking', error: null }));
136
+ autoUpdater.on('update-not-available', () => set({
137
+ phase: 'current', newVersion: null, percent: null, error: null, checkedAt: Date.now(),
138
+ }));
139
+ autoUpdater.on('update-available', (info) => set({
140
+ phase: 'downloading', newVersion: info?.version ?? null, percent: 0, error: null, checkedAt: Date.now(),
141
+ }));
142
+ autoUpdater.on('download-progress', (p) => set({ phase: 'downloading', percent: p?.percent ?? null }));
143
+ // STAMPED ONCE, not on every redraw: a second `update-downloaded` for the
144
+ // same version would otherwise make the row young again and move it back to
145
+ // the top of her list for news she has already read.
146
+ autoUpdater.on('update-downloaded', (info) => set({
147
+ phase: 'ready', newVersion: info?.version ?? state.newVersion, percent: 100, error: null,
148
+ readyAt: state.phase === 'ready' && state.readyAt ? state.readyAt : Date.now(),
149
+ }));
150
+ // SILENT ON PURPOSE. See point 2 at the top of this file.
151
+ autoUpdater.on('error', (e) => set({
152
+ phase: 'error',
153
+ // The message only. electron-updater puts a whole stack on some of these
154
+ // and Settings shows this string to a person.
155
+ error: shortError(e),
156
+ checkedAt: Date.now(),
157
+ }));
158
+ }
159
+
160
+ async function check({ manual = false } = {}) {
161
+ if (stopped || !autoUpdater) return describe(state);
162
+ if (!autoUpdater.isUpdaterActive()) {
163
+ // A build run from source. Say which it is rather than reporting a
164
+ // healthy "up to date" that was never checked.
165
+ set({
166
+ phase: 'unsupported',
167
+ error: `${Name} only updates itself when it is installed as an app.`,
168
+ checkedAt: Date.now(),
169
+ });
170
+ return describe(state);
171
+ }
172
+ // Once it is downloaded there is nothing left to check for; checking again
173
+ // would walk the state backwards under a button that says Restart.
174
+ if (state.phase === 'ready') return describe(state);
175
+ if (state.phase === 'checking' || (state.phase === 'downloading' && !manual)) return describe(state);
176
+ try {
177
+ await autoUpdater.checkForUpdates();
178
+ } catch (e) {
179
+ set({ phase: 'error', error: shortError(e), checkedAt: Date.now() });
180
+ }
181
+ return describe(state);
182
+ }
183
+
184
+ // THE BUTTON. Quit, swap the app, come back up on the new one.
185
+ function install() {
186
+ if (!autoUpdater || state.phase !== 'ready') return false;
187
+ // `isSilent` true so macOS does not put its own progress window over her
188
+ // screen; `isForceRunAfter` true so the app comes back, which is the whole
189
+ // point of pressing a button that says Restart.
190
+ setImmediate(() => autoUpdater.quitAndInstall(true, true));
191
+ return true;
192
+ }
193
+
194
+ function start() {
195
+ if (stopped || timer) return;
196
+ const tick = () => { void check(); };
197
+ setTimeout(tick, FIRST_CHECK_MS).unref?.();
198
+ timer = setInterval(tick, EVERY_MS);
199
+ timer.unref?.();
200
+ }
201
+
202
+ function stop() {
203
+ stopped = true;
204
+ if (timer) clearInterval(timer);
205
+ timer = null;
206
+ }
207
+
208
+ return { start, stop, check, install, state: () => describe(state) };
209
+ }
210
+
211
+ // electron-updater's errors arrive as Error objects whose `message` is
212
+ // occasionally the entire server response. One line, and a sentence a person
213
+ // can read, for the few we can name.
214
+ export function shortError(e) {
215
+ const raw = String(e?.message ?? e ?? 'Something went wrong.').split('\n')[0].trim();
216
+ // AND THE NAME IS ON `code`, NOT IN THE MESSAGE. electron-updater builds
217
+ // these through builder-util-runtime's `newError`, which puts the
218
+ // ERR_UPDATER_ name on the error's `code` and leaves the message as prose
219
+ // with the whole URL in it. Matching the message alone reached none of them.
220
+ // Measured 2026-08-29 in a packaged build against the real GitHub feed: the
221
+ // published v0.1.0 carries only the .dmg, so the check 404s on
222
+ // latest-mac.yml and Settings read "Cannot find latest-mac.yml in the latest
223
+ // release artifacts (https://github.com/Astral-Agent/astral-releases/
224
+ // releases/download/v0.1.0/latest-mac.yml): HttpErr…" to her. The sentence
225
+ // for exactly that failure was already on the line below it.
226
+ const named = `${e?.code ?? ''} ${raw}`;
227
+ if (/ERR_UPDATER_ZIP_FILE_NOT_FOUND/.test(named)) return 'That release has no app in it to install.';
228
+ if (/ERR_UPDATER_CHANNEL_FILE_NOT_FOUND/.test(named)) return 'That release is missing its update file.';
229
+ if (/ERR_UPDATER_NO_PUBLISHED_VERSIONS|LATEST_VERSION_NOT_FOUND/.test(named)) return 'No published version to update to yet.';
230
+ if (/ENOTFOUND|EAI_AGAIN|ENETDOWN|ETIMEDOUT|ECONNREFUSED|ECONNRESET/.test(raw)) return 'Could not reach the internet.';
231
+ // AND THE SAME FAILURE UNDER CHROMIUM'S NAME FOR IT. The line above catches
232
+ // Node's codes, and electron-updater does not always use Node: the check
233
+ // runs through Electron's own `net` module, whose errors read
234
+ // `net::ERR_CONNECTION_REFUSED`. That is not `ECONNREFUSED`, so it fell
235
+ // through to the last line and Settings said `net::ERR_CONNECTION_REFUSED`
236
+ // to her. Measured on 2026-08-29, in a packaged build with the update feed
237
+ // switched off mid-run: that exact string was on the screen.
238
+ if (/^net::ERR_/.test(raw)) return 'Could not reach the internet.';
239
+ return raw.length > 160 ? `${raw.slice(0, 157)}…` : raw;
240
+ }
@@ -0,0 +1,54 @@
1
+ // Which keystrokes mean zoom. Kept apart from the menu so it imports without
2
+ // Electron and can be tested for real rather than asserted against source text.
3
+ //
4
+ // Her window has been sitting at zoom level 1 (1.2x) since before it was
5
+ // launched, because Chromium stores zoom per page URL in the profile and hands
6
+ // it back on every load. The chords are the only way out of that, so they have
7
+ // to arrive.
8
+ //
9
+ // "Cmd +" is really ⇧⌘= on a US keyboard, which reports key '+' and code
10
+ // 'Equal'. Matching on BOTH key and code is what makes the shifted form, the
11
+ // unshifted form and the numpad all land on the same command.
12
+
13
+ const IN = { Equal: 0.5, NumpadAdd: 0.5, Minus: -0.5, NumpadSubtract: -0.5, Digit0: 0, Numpad0: 0 };
14
+ const KEYS = { '=': 0.5, '+': 0.5, '-': -0.5, _: -0.5, 0: 0 };
15
+
16
+ /**
17
+ * The zoom step a before-input-event describes, or null if it is not a zoom
18
+ * chord. 0.5 in, -0.5 out, 0 actual size. The app is a real answer here, so
19
+ * callers must test for null, not for falsiness.
20
+ */
21
+ export function zoomDeltaFor(input) {
22
+ if (!input || input.type !== 'keyDown') return null;
23
+ if (!(input.meta || input.control)) return null;
24
+ if (input.alt) return null; // ⌥⌘- and friends belong to whoever else wants them
25
+ const byCode = IN[String(input.code ?? '')];
26
+ if (byCode !== undefined) return byCode;
27
+ const byKey = KEYS[String(input.key ?? '')];
28
+ return byKey === undefined ? null : byKey;
29
+ }
30
+
31
+ // WHICH CONTENTS GETS ZOOMED, and why this is not just `getFocusedWebContents`.
32
+ //
33
+ // That is the CLICK, not the keystroke, and it rules out the accelerator being
34
+ // the only fault. A click had no contents of its own and asked for the focused
35
+ // one, which returns null whenever nothing on screen claims focus, and on macOS
36
+ // the menu itself holds the keyboard while she is choosing from it. Null was a
37
+ // silent no-op, and the silent no-op is the whole complaint.
38
+ //
39
+ // Electron hands a menu click the window it was chosen from, so the click now
40
+ // carries a target and never has to guess. This is the same move `Reload` made
41
+ // on 2026-08-06; zoom was the last item in this menu still guessing.
42
+
43
+ /**
44
+ * The web contents a zoom should act on, first non-destroyed of: the one the
45
+ * caller named (the keystroke's own contents, or the window a menu click came
46
+ * from), then whatever is focused, then the app's own window. Returns null only
47
+ * if there is genuinely nothing on screen.
48
+ */
49
+ export function zoomTarget({ named = null, focused = null, appWindow = null } = {}) {
50
+ for (const contents of [named, focused, appWindow]) {
51
+ if (contents && !contents.isDestroyed?.()) return contents;
52
+ }
53
+ return null;
54
+ }
@@ -0,0 +1,20 @@
1
+ // What the zoom readout says. Kept apart from the menu, like zoom-keys.mjs, so
2
+ // it imports without Electron and the ladder can be tested for real.
3
+ //
4
+ // So this file computes one number and nothing else. Chromium's zoom factor is
5
+ // 1.2 to the power of the level, which is why her ladder reads 76, 83, 91, 100,
6
+ // 110, 120 rather than the round numbers other apps step through. Moving the
7
+ // ladder onto 75/80/90/100/110/125 was offered and she did not ask for it, so
8
+ // the steps stay exactly as they were and only the readout is new.
9
+
10
+ const CHROMIUM_ZOOM_BASE = 1.2;
11
+
12
+ /**
13
+ * The percentage to show for a Chromium zoom level. Level 0 is 100%, level 1 is
14
+ * 120%, level -0.5 is 91%. Returns null for a level that is not a number, so a
15
+ * caller can tell "no readout" from a readout of 0.
16
+ */
17
+ export function zoomPercentFor(level) {
18
+ if (typeof level !== 'number' || !Number.isFinite(level)) return null;
19
+ return Math.round(CHROMIUM_ZOOM_BASE ** level * 100);
20
+ }
@@ -0,0 +1,34 @@
1
+ #!/bin/sh
2
+ # Launcher for this app's own store server.
3
+ #
4
+ # It exists because node is installed through nvm on this machine and is NOT on
5
+ # a login shell's PATH, so an MCP client spawning `node mcp/stdio.mjs` gets
6
+ # "command not found" and reports the server as failed with nothing useful in
7
+ # it. Hard-coding the current nvm path would work until the next node upgrade
8
+ # silently moved it, so resolve it at launch instead.
9
+ #
10
+ # STDOUT IS THE JSON-RPC STREAM. Anything nvm prints on the way in would be
11
+ # parsed as protocol and corrupt the session, hence the redirects: every setup
12
+ # step here sends its output to stderr or /dev/null, and the only thing that
13
+ # ever writes to stdout is the server itself.
14
+
15
+ if ! command -v node >/dev/null 2>&1; then
16
+ export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
17
+ # shellcheck disable=SC1091
18
+ [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" >/dev/null 2>&1
19
+ fi
20
+
21
+ if ! command -v node >/dev/null 2>&1; then
22
+ # Last resort: the newest version nvm has on disk, without sourcing anything.
23
+ for bin in "$HOME"/.nvm/versions/node/*/bin; do
24
+ [ -x "$bin/node" ] && PATH="$bin:$PATH"
25
+ done
26
+ export PATH
27
+ fi
28
+
29
+ if ! command -v node >/dev/null 2>&1; then
30
+ echo "[store] cannot find node (looked on PATH and in $HOME/.nvm)" >&2
31
+ exit 1
32
+ fi
33
+
34
+ exec node "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/stdio.mjs" "$@"
@@ -0,0 +1,77 @@
1
+ // Which founder's store are we operating on, and which product.
2
+ //
3
+ // The app answers this from a live auth session. The MCP server has no session:
4
+ // it is a child process spawned by whatever agent wanted it, so the account is
5
+ // configuration, resolved once at startup.
6
+
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { legacyRoot, setAccountScope, dataRoot, listProjects, sanitizeName } from '../../main/store/project.mjs';
10
+ import { envName } from '../../shared/product-name.mjs';
11
+
12
+ /**
13
+ * Resolve the account whose store this server serves.
14
+ *
15
+ * STORE_ACCOUNT_ID wins. Failing that, if exactly one account exists on disk
16
+ * we use it, because that is unambiguous and asking would be pedantic. If
17
+ * several exist we REFUSE rather than pick: silently operating on the wrong
18
+ * founder's store would be discovered late and by writing to it.
19
+ */
20
+ export function resolveAccount({ accountId = process.env.STORE_ACCOUNT_ID } = {}) {
21
+ const accountsDir = path.join(legacyRoot(), 'accounts');
22
+
23
+ if (accountId) {
24
+ const dir = path.join(accountsDir, accountId);
25
+ if (!fs.existsSync(dir)) throw new Error(`no such account on disk: ${accountId} (looked in ${accountsDir})`);
26
+ setAccountScope(accountId);
27
+ return { accountId, root: dataRoot() };
28
+ }
29
+
30
+ let found = [];
31
+ try {
32
+ found = fs.readdirSync(accountsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
33
+ } catch {
34
+ throw new Error(`no accounts found at ${accountsDir}; is ${envName('HOME')} set correctly?`);
35
+ }
36
+
37
+ if (found.length === 1) {
38
+ setAccountScope(found[0]);
39
+ return { accountId: found[0], root: dataRoot() };
40
+ }
41
+ if (!found.length) throw new Error(`no accounts found at ${accountsDir}`);
42
+ throw new Error(
43
+ `several accounts on disk (${found.join(', ')}); set STORE_ACCOUNT_ID so this server cannot write to the wrong one`,
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Find a product by id, slug, or name. Agents refer to products the way a
49
+ * person would ("kestrel"), not by generated id, so accept all three and say
50
+ * clearly when a name is ambiguous rather than guessing which one was meant.
51
+ */
52
+ export function resolveProduct(ref) {
53
+ const wanted = String(ref ?? '').trim();
54
+ if (!wanted) throw new Error('which product? pass its name or id (list_products shows them)');
55
+ const projects = listProjects();
56
+
57
+ const exact = projects.find((p) => p.id === wanted) ?? projects.find((p) => path.basename(p.dir) === wanted);
58
+ if (exact) return exact;
59
+
60
+ const lower = wanted.toLowerCase();
61
+ const byName = projects.filter((p) => (p.name ?? '').toLowerCase() === lower);
62
+ if (byName.length === 1) return byName[0];
63
+ if (byName.length > 1) throw new Error(`several products are called "${wanted}"; use its id instead (${byName.map((p) => p.id).join(', ')})`);
64
+
65
+ // THE NAME IT WAS MADE WITH. A rename changes the label and never the folder,
66
+ // and the folder is that first name slugged, so an agent that still says the
67
+ // old name is pointing at this folder. After the current names, so a name she
68
+ // has since given another project wins.
69
+ const madeAs = projects.find((p) => path.basename(p.dir) === sanitizeName(wanted));
70
+ if (madeAs) return madeAs;
71
+
72
+ const partial = projects.filter((p) => (p.name ?? '').toLowerCase().includes(lower) || path.basename(p.dir).includes(lower));
73
+ if (partial.length === 1) return partial[0];
74
+ if (partial.length > 1) throw new Error(`"${wanted}" matches several products: ${partial.map((p) => p.name ?? p.id).join(', ')}`);
75
+
76
+ throw new Error(`no product called "${wanted}"; list_products shows what exists`);
77
+ }
@@ -0,0 +1,61 @@
1
+ // Past in-app conversations, read-only.
2
+ //
3
+ // This is continuity, not a feature: a founder and the in-app agent have already
4
+ // decided things in these logs that never made it into a document, and an
5
+ // external agent picking up a product months later should be able to find out
6
+ // why something is the way it is instead of re-deciding it.
7
+ //
8
+ // Strictly read-only, deliberately. The MCP has no business appending to a
9
+ // conversation the app owns and renders live; a message written here would
10
+ // appear in a chat with no session behind it, which reads to the founder as the
11
+ // agent talking to itself.
12
+
13
+ import { listChats, readChatLog } from '../../main/store/chats.mjs';
14
+ import { resolveProduct } from './account.mjs';
15
+
16
+ const DEFAULT_LIMIT = 100;
17
+ const MAX_LIMIT = 500;
18
+ const MAX_TEXT = 4000;
19
+
20
+ export function listProductChats(ref) {
21
+ const project = resolveProduct(ref);
22
+ return listChats(project.dir).map((c) => ({
23
+ id: c.id,
24
+ title: c.title,
25
+ createdAt: c.createdAt ?? null,
26
+ updatedAt: c.updatedAt ?? null,
27
+ }));
28
+ }
29
+
30
+ /**
31
+ * The tail of one conversation. Tail rather than head because the recent end is
32
+ * almost always what a reader wants, and a long chat would otherwise spend the
33
+ * whole budget on its opening.
34
+ */
35
+ export function readProductChat(ref, { chatId = null, limit = DEFAULT_LIMIT } = {}) {
36
+ const project = resolveProduct(ref);
37
+ const chats = listChats(project.dir);
38
+ if (!chats.length) return { chatId: null, title: null, messages: [] };
39
+
40
+ const chat = chatId ? chats.find((c) => c.id === chatId) : chats[0]; // newest by default
41
+ if (!chat) throw new Error(`no chat ${chatId} in ${project.name} (list_chats shows what exists)`);
42
+
43
+ const n = Math.min(Math.max(1, Number(limit) || DEFAULT_LIMIT), MAX_LIMIT);
44
+ const log = readChatLog(project.dir, chat.id);
45
+
46
+ return {
47
+ chatId: chat.id,
48
+ title: chat.title,
49
+ total: log.length,
50
+ messages: log.slice(-n).map((m) => ({
51
+ role: m.role,
52
+ // Truncated per message rather than per response: one enormous message
53
+ // must not crowd out the twenty around it that give it meaning.
54
+ text: typeof m.text === 'string' && m.text.length > MAX_TEXT ? `${m.text.slice(0, MAX_TEXT)}\n[truncated]` : (m.text ?? ''),
55
+ ts: m.ts ?? null,
56
+ // An unanswered question from a past session is a live loose end, so it
57
+ // stays visible rather than being flattened into prose.
58
+ ask: m.ask ? { status: m.ask.status, questions: (m.ask.questions ?? []).map((q) => q.question) } : undefined,
59
+ })),
60
+ };
61
+ }