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,206 @@
1
+ // THE ONLY PLACE ANYTHING WE RECEIVE LEAVES THIS MACHINE. One file, the main
2
+ // process, and a list of seven counts it will refuse to grow past.
3
+ //
4
+ // IT IS NOT THE ONLY TRAFFIC ANY MORE, and the header said so until 2026-08-29.
5
+ // `main/updater.mjs` asks github.com a few times a day whether a newer Agentbox
6
+ // has been released, which reaches us not at all and is nothing to do with this
7
+ // file. `legal/privacy.html` 5.5 is where that is written down.
8
+ //
9
+ // Her yes: "yes to posthog, go ahead", on the eight counts she approved the
10
+ // same day and under her rule from 08-18: no code, no prompts, no keys, no
11
+ // paths. `legal/privacy.html` sections 4, 5 and 10 are the promise this file
12
+ // has to keep, so read them before changing anything here.
13
+ //
14
+ // FIVE THINGS ABOUT THE SHAPE, each of them load-bearing:
15
+ //
16
+ // 1. NEVER `posthog-js`, AND NEVER IN THE RENDERER. The browser SDK autocaptures
17
+ // the text of whatever was clicked, and in Agentbox that text is task titles
18
+ // and repo names. `posthog-node` takes only what it is handed. A test fails
19
+ // the build if `posthog-js` ever appears under `renderer/`.
20
+ // 2. The renderer cannot send. It hands a NAME over IPC and this file decides
21
+ // whether that name is one of hers (`shared/analytics-events.mjs`).
22
+ // 3. NOTHING IS SENT WITHOUT A DESTINATION AND THE SWITCH. No key configured
23
+ // means no client is ever constructed. The shipped app carries a key baked
24
+ // into its own package.json at build time; a copy run from source carries
25
+ // none and so sends nothing at all, which is still the honest default.
26
+ // 4. THE SWITCH IS READ AT EVERY SEND, never captured at boot. Section 10 of
27
+ // the privacy page says turning it off "stops sending from that moment", and
28
+ // an `enabled` flag read once at startup would keep sending until the app
29
+ // was restarted, which is that sentence being false for as long as she left
30
+ // the window open.
31
+ // 5. Crash reports come through the seam in `main/crash-report.mjs`, already
32
+ // scrubbed, and they are checked AGAIN here before they go. Two checks, on
33
+ // purpose: the first one runs while the process is dying.
34
+
35
+ import os from 'node:os';
36
+ import fs from 'node:fs';
37
+ import { createRequire } from 'node:module';
38
+ import { sanitize } from '../shared/analytics-events.mjs';
39
+ import { findPrivate } from '../shared/crash-scrub.mjs';
40
+ import { readInstallId } from '../shared/install-id.mjs';
41
+ import { readEnv } from '../shared/product-name.mjs';
42
+
43
+ // PostHog's own default host. Kept here rather than left to the config so a
44
+ // typo cannot silently point a stranger's crashes somewhere else; `posthogHost`
45
+ // exists only for a self-hosted instance.
46
+ const DEFAULT_HOST = 'https://us.i.posthog.com';
47
+
48
+ // ONE SETTING, ON WHEN YOU INSTALL AGENTBOX (privacy page, section 10). Stored as
49
+ // an opt-OUT so that a config she has never touched means on, and so turning it
50
+ // off is a real, visible key in her own file rather than an absence.
51
+ export function diagnosticsOn(config) {
52
+ return config?.diagnostics !== false;
53
+ }
54
+
55
+ // THE KEY THE DOWNLOAD CARRIES. Baked into the packaged `package.json` by
56
+ // `scripts/release.mjs` at build time (electron-builder's `extraMetadata`), so a
57
+ // stranger's install has somewhere to send and a copy run from source still does
58
+ // not. It is a PostHog project key, which is write-only by design: it can add an
59
+ // event and it cannot read one back, which is why it may sit inside a file
60
+ // anybody can unzip. The one that reads, `phx_`, never goes near a build.
61
+ //
62
+ // Measured 2026-08-21: before this, the key came only from the user's own
63
+ // `zero.config.json`, which no download contains, so every install but hers
64
+ // sent nothing and the north star could not be counted.
65
+ function bakedKey() {
66
+ try {
67
+ const pkg = createRequire(import.meta.url)('../package.json');
68
+ return pkg?.bakedPosthogKey ?? null;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ const BAKED = bakedKey();
75
+
76
+ // Where it would go, if it went anywhere. A missing key is not an error and not
77
+ // a warning: it is a copy of Agentbox with nowhere to send to, which is a build
78
+ // run from source.
79
+ //
80
+ // HER OWN FILE STILL WINS. A `posthogKey` in `zero.config.json` outranks the
81
+ // baked one, so pointing a copy at a different project stays a one-line edit,
82
+ // and `diagnostics: false` still turns all of it off at every send.
83
+ export function analyticsKey(config, baked = BAKED) {
84
+ const key = config?.posthogKey ?? readEnv('POSTHOG_KEY') ?? baked ?? null;
85
+ return typeof key === 'string' && key.trim() ? key.trim() : null;
86
+ }
87
+
88
+ export function createAnalytics({ config, version, dir, client, env = process.env } = {}) {
89
+ const key = analyticsKey(config);
90
+ const hasDestination = !!key || !!client;
91
+ // BOTH HALVES, AT EVERY SEND. See note 4 above.
92
+ const on = () => hasDestination && diagnosticsOn(config);
93
+
94
+ // `env` is here for ONE reason: a copy launched into a throwaway home is a
95
+ // first-run test of hers and sends under a fixed id rather than a fresh
96
+ // random one. The decision lives in `shared/install-id.mjs` with the
97
+ // identifier itself, because two copies of it is how the counts and the
98
+ // crash reports start telling different stories.
99
+ const installId = dir ? readInstallId(dir, env) : null;
100
+ // The version pair from section 5.1, on every event, set here and never by a
101
+ // caller. `os.release` is the kernel release, which is what the page means by
102
+ // the version of macOS and carries nothing about the machine or its owner.
103
+ const base = {
104
+ app_version: version || '0.0.0',
105
+ os_platform: process.platform,
106
+ os_release: os.release(),
107
+ os_arch: process.arch,
108
+ };
109
+ // What the second scrub check is run against. Cheap, and it is the only thing
110
+ // standing between a bug in the first check and her promise.
111
+ const ctx = {
112
+ home: os.homedir(),
113
+ username: (() => { try { return os.userInfo().username; } catch { return null; } })(),
114
+ };
115
+
116
+ let posthog = client ?? null;
117
+ const load = () => {
118
+ if (posthog) return posthog;
119
+ try {
120
+ // Required lazily, so a machine with no key and every test in this repo
121
+ // never constructs a client and never opens a socket.
122
+ const { PostHog } = createRequire(import.meta.url)('posthog-node');
123
+ posthog = new PostHog(key, { host: config?.posthogHost || DEFAULT_HOST, flushAt: 20, flushInterval: 10000 });
124
+ } catch (err) {
125
+ console.warn('zero: analytics could not start:', err.message);
126
+ posthog = null;
127
+ }
128
+ return posthog;
129
+ };
130
+
131
+ const send = (event, properties) => {
132
+ if (!on()) return false;
133
+ const ph = load();
134
+ if (!ph) return false;
135
+ try {
136
+ ph.capture({ distinctId: installId, event, properties: { ...base, ...properties } });
137
+ return true;
138
+ } catch (err) {
139
+ // A failed send is never allowed to be a failed app.
140
+ console.warn('zero: analytics send failed:', err.message);
141
+ return false;
142
+ }
143
+ };
144
+
145
+ return {
146
+ get enabled() { return on(); },
147
+ get reason() {
148
+ if (!hasDestination) return 'has nowhere to send to';
149
+ return diagnosticsOn(config) ? 'on' : 'turned off';
150
+ },
151
+ hasDestination,
152
+ installId,
153
+
154
+ // ONE DOOR. `name` must be one of hers, and the properties are filtered to
155
+ // numbers, booleans and one closed enum. Anything else is dropped rather
156
+ // than sent, because the caller is often the renderer.
157
+ track(name, props) {
158
+ if (!on()) return false;
159
+ const clean = sanitize(name, props);
160
+ if (!clean) return false;
161
+ return send(name, clean);
162
+ },
163
+
164
+ // Handed to `setTransport` in `main/crash-report.mjs`. The payload arrives
165
+ // already scrubbed and already checked; it is checked once more here, and a
166
+ // report that fails is dropped whole rather than trimmed, which is the rule
167
+ // the scrub itself follows.
168
+ crashTransport(payload) {
169
+ if (!on()) return false;
170
+ if (!payload || typeof payload !== 'object') return false;
171
+ const leaks = findPrivate(payload, ctx);
172
+ if (leaks.length) {
173
+ return send('crash_report', { crash_kind: 'redacted', dropped_fields: leaks.length });
174
+ }
175
+ return send('crash_report', {
176
+ crash_kind: String(payload.kind ?? 'unknown'),
177
+ up_ms: Number(payload.upMs ?? 0),
178
+ report_version: Number(payload.v ?? 1),
179
+ error: payload.error ?? null,
180
+ happened_at: String(payload.ts ?? ''),
181
+ });
182
+ },
183
+
184
+ // A report is written while the process is dying, so delivery cannot be
185
+ // synchronous: the queue on disk is drained on the NEXT launch. A file is
186
+ // removed only once its send has been accepted, and a file that will not
187
+ // parse is removed rather than retried forever.
188
+ drainCrashes(files = []) {
189
+ if (!on()) return 0;
190
+ let sent = 0;
191
+ for (const file of files) {
192
+ let payload = null;
193
+ try { payload = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { payload = null; }
194
+ if (!payload) { try { fs.unlinkSync(file); } catch {} continue; }
195
+ if (this.crashTransport(payload)) {
196
+ sent += 1;
197
+ try { fs.unlinkSync(file); } catch {}
198
+ }
199
+ }
200
+ return sent;
201
+ },
202
+
203
+ async flush() { try { await posthog?.flush?.(); } catch {} },
204
+ async shutdown() { try { await posthog?.shutdown?.(); } catch {} },
205
+ };
206
+ }
@@ -0,0 +1,206 @@
1
+ // WHAT ONE REPLY MAY DO, KEPT WHERE A WORKER DOES NOT WRITE.
2
+ //
3
+ // When the founder answers a card she can also say what that one run is allowed
4
+ // to do on her Mac. That value becomes `--permission-mode` on a real process,
5
+ // so it is a GRANT and not an opinion about the work.
6
+ //
7
+ // IT USED TO RIDE ON THE LEDGER. `work-items.jsonl` is append-only, every line
8
+ // names its own author as a plain string, and nothing checks that string.
9
+ // Workers append to that file all day through the MCP; on a product with no
10
+ // code repo registered the file also sat in the worker's own cwd. The structured
11
+ // path was closed (an agent line carrying the field was stripped), and the file
12
+ // was not: one hand-written line saying `"source":"founder"` granted its own
13
+ // next run bypassPermissions. Proved on a scratch ledger, 2026-08-24.
14
+ //
15
+ // So the grant is not on the ledger any more. It lives here, in the app's own
16
+ // home, written by main and by nothing else. The MCP server has no code that
17
+ // reaches this file and no tool that names it.
18
+ //
19
+ // WHAT THIS DOES AND WHAT IT DOES NOT DO, said plainly.
20
+ //
21
+ // It does NOT make forging impossible. Anything that can run commands can
22
+ // write any file this user owns, and `~/.astral` is no exception. Claude Code
23
+ // has the identical property: its settings file is as writable as anything
24
+ // else in your home, and it relies on the sandbox and the prompt rather than
25
+ // on the folder.
26
+ //
27
+ // What it changes is the COST. Before: one more line appended to a file the
28
+ // worker was appending to anyway, in a format it writes every few seconds,
29
+ // in its own working directory. After: a deliberate reach into the app's own
30
+ // storage, in a file nothing briefs it about and no tool it holds mentions.
31
+ // That is a real difference and it is the whole claim. It is not a wall.
32
+ //
33
+ // GRANTING IS THE FOUNDER'S, SPENDING ONE IS THE MACHINERY'S, AGENTS GET
34
+ // NEITHER. That rule survives the move intact, and it is now structural rather
35
+ // than enforced: there is no writer here except the app's own main process, so
36
+ // there is no `source` to check and no line for anyone else to forge.
37
+
38
+ import fs from 'node:fs';
39
+ import path from 'node:path';
40
+ import { appHome } from './store/home.mjs';
41
+ import { CLAUDE_PERMISSION_MODES } from '../shared/work-items.mjs';
42
+ import { CODEX_MODE_ORDER } from '../shared/codex-modes.mjs';
43
+
44
+ const FILE = 'answer-modes.json';
45
+
46
+ // HOW LONG A MODE LIVES, NOW THAT IT BELONGS TO A THREAD AND NOT TO ONE SEND.
47
+ //
48
+ // This was 24 hours, and 24 hours was right while the supervisor spent the
49
+ // grant on the first spawn: anything still here the next day had been spent or
50
+ // belonged to a row nobody ever ran. Neither is true any more. A mode she sets
51
+ // on a thread governs every run on that thread until she moves it, so a sweep
52
+ // measured from the moment she picked it would quietly hand a thread back to
53
+ // the fleet's mode overnight, which is the bug this round exists to fix,
54
+ // arriving a day late instead of a run late.
55
+ //
56
+ // So the clock is not "when she picked it" but "when this thread last used
57
+ // it": `touchAnswerMode` below restamps `at` on every run that reads one, and
58
+ // the window is a month. A thread she is working in keeps its mode for as long
59
+ // as she works in it. A row that has been silent for a month loses its grant
60
+ // rather than keeping a bypassPermissions she granted in another quarter, and
61
+ // the file does not grow without limit. Swept on every write.
62
+ const KEEP_MS = 30 * 24 * 60 * 60 * 1000;
63
+
64
+ const file = (home = appHome()) => path.join(home, FILE);
65
+
66
+ function readAll(home) {
67
+ try {
68
+ const raw = JSON.parse(fs.readFileSync(file(home), 'utf8'));
69
+ return raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
70
+ } catch {
71
+ return {};
72
+ }
73
+ }
74
+
75
+ // tmp + rename, so a reader never sees half a file and a crash mid-write leaves
76
+ // the previous one intact.
77
+ function writeAll(home, all) {
78
+ const target = file(home);
79
+ fs.mkdirSync(path.dirname(target), { recursive: true });
80
+ const tmp = `${target}.${process.pid}.tmp`;
81
+ fs.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}\n`);
82
+ fs.renameSync(tmp, target);
83
+ }
84
+
85
+ const keyOf = (slug, id) => `${slug}/${id}`;
86
+
87
+ function sweep(all, now) {
88
+ for (const [key, entry] of Object.entries(all)) {
89
+ if (!entry || typeof entry !== 'object' || !Number.isFinite(entry.at) || now - entry.at > KEEP_MS) {
90
+ delete all[key];
91
+ }
92
+ }
93
+ return all;
94
+ }
95
+
96
+ /**
97
+ * The mode set on one row's reply.
98
+ *
99
+ * `undefined` means she never touched the chip. `null` means she cleared it, or
100
+ * the run that spent it cleared it, and the project's own setting applies. The
101
+ * two are kept apart because the ledger kept them apart and every reader of the
102
+ * old field already knows the difference.
103
+ *
104
+ * A value that is not one of Claude Code's six is dropped rather than returned,
105
+ * so nothing that is not a mode can reach a command line even if this file were
106
+ * edited by hand.
107
+ */
108
+ /**
109
+ * EVERY WORD THIS FILE WILL STORE: Claude Code's six and Codex's three.
110
+ *
111
+ * ONE FIELD FOR BOTH ENGINES, because a row has one engine and the reply box
112
+ * can only ever offer that engine's list. Each consumer then filters by its
113
+ * own vocabulary and ignores the other's: `spawnPlan` keeps only a Claude mode
114
+ * (`CLAUDE_MODES.includes`), `codexThreadParamsFor` keeps only a Codex one
115
+ * (`isCodexMode`). A word meant for the other engine is therefore invisible
116
+ * rather than wrong.
117
+ *
118
+ * `auto` IS ON BOTH LISTS AND THAT IS THE ONE OVERLAP. It means the sensible
119
+ * default on each engine, so the only way to be bitten is to set it on a Codex
120
+ * row and then have the task picked up by Claude Code, which would run Claude
121
+ * Code's auto. Both are the default that engine already wanted; the failure is
122
+ * benign and is written here so nobody spends an afternoon proving it.
123
+ */
124
+ const ALLOWED_MODES = [...CLAUDE_PERMISSION_MODES, ...CODEX_MODE_ORDER];
125
+
126
+ export function answerMode(slug, id, { home = appHome(), now = Date.now() } = {}) {
127
+ const entry = readAll(home)[keyOf(slug, id)];
128
+ if (!entry || typeof entry !== 'object') return undefined;
129
+ if (Number.isFinite(entry.at) && now - entry.at > KEEP_MS) return undefined;
130
+ if (entry.mode === null) return null;
131
+ return ALLOWED_MODES.includes(entry.mode) ? entry.mode : undefined;
132
+ }
133
+
134
+ /** Every row that currently carries one, as `{ "<slug>/<id>": mode }`. */
135
+ export function answerModes({ home = appHome(), now = Date.now() } = {}) {
136
+ const out = {};
137
+ for (const [key, entry] of Object.entries(readAll(home))) {
138
+ if (!entry || typeof entry !== 'object') continue;
139
+ if (Number.isFinite(entry.at) && now - entry.at > KEEP_MS) continue;
140
+ if (entry.mode === null) { out[key] = null; continue; }
141
+ if (ALLOWED_MODES.includes(entry.mode)) out[key] = entry.mode;
142
+ }
143
+ return out;
144
+ }
145
+
146
+ /**
147
+ * Set one, or clear it with null. Called only from the founder's own reply
148
+ * (main/store.mjs, answerItem), which is only reachable from the reply box.
149
+ *
150
+ * A mode that is not one of the six is refused outright rather than stored,
151
+ * which is what the ledger's own check used to do.
152
+ */
153
+ export function setAnswerMode(slug, id, mode, { home = appHome(), now = Date.now() } = {}) {
154
+ if (mode !== null && !ALLOWED_MODES.includes(mode)) return answerMode(slug, id, { home, now });
155
+ const all = sweep(readAll(home), now);
156
+ all[keyOf(slug, id)] = { mode, at: now };
157
+ writeAll(home, all);
158
+ return mode;
159
+ }
160
+
161
+ /**
162
+ * Say that this thread just used its mode, so the sweep above measures from
163
+ * the last run rather than from the moment she picked it.
164
+ *
165
+ * IT RESTAMPS THE MODE THAT IS THERE, NEVER THE ONE THE RUN WAS BUILT WITH.
166
+ * The supervisor reads the mode when it builds the launch and touches on the
167
+ * child's own 'spawn' event, so there is a window in between, and a second
168
+ * answer landing inside it must survive untouched: `used` is compared and a
169
+ * mismatch is left exactly as it stands. Nothing here ever changes a mode, so
170
+ * the worst this call can do is keep a grant she already made alive one more
171
+ * month.
172
+ */
173
+ export function touchAnswerMode(slug, id, used, { home = appHome(), now = Date.now() } = {}) {
174
+ const all = sweep(readAll(home), now);
175
+ const key = keyOf(slug, id);
176
+ const entry = all[key];
177
+ if (!entry || typeof entry !== 'object') return undefined;
178
+ if (used !== undefined && entry.mode !== used) return entry.mode;
179
+ all[key] = { mode: entry.mode, at: now };
180
+ writeAll(home, all);
181
+ return entry.mode;
182
+ }
183
+
184
+ /**
185
+ * Put a row back on the project's setting.
186
+ *
187
+ * THIS IS NO LONGER CALLED ON SPAWN. Until 2026-09-24 the supervisor called it
188
+ * the moment a run came up, which is what made a picked mode last exactly one
189
+ * send; `touchAnswerMode` stands in that place now. The only thing that clears
190
+ * a mode today is she does, through the reply box's own clear row.
191
+ *
192
+ * IT CLEARS THE MODE IT WAS TOLD ABOUT, NEVER WHATEVER HAPPENS TO BE THERE
193
+ * NOW, for the same reason as above. Clearing is not "less": it means back to
194
+ * the project's setting, which can be the wider one.
195
+ */
196
+ export function clearAnswerMode(slug, id, spent, { home = appHome(), now = Date.now() } = {}) {
197
+ const all = sweep(readAll(home), now);
198
+ const key = keyOf(slug, id);
199
+ const current = all[key]?.mode;
200
+ if (spent !== undefined && current !== spent) return current === undefined ? undefined : current;
201
+ all[key] = { mode: null, at: now };
202
+ writeAll(home, all);
203
+ return null;
204
+ }
205
+
206
+ export const _internals = { file, KEEP_MS };
@@ -0,0 +1,160 @@
1
+ // The approvals channel: a tiny MCP server that Claude Code calls (via
2
+ // --permission-prompt-tool) whenever a headless worker wants to do something
3
+ // its standing grants do not cover. The request lands as a file in the spool,
4
+ // the app shows the founder a card, her click writes the answer file, and the
5
+ // worker unblocks in place. Before this, a gated command ended the story:
6
+ // sessions wrote everything and bounced the actual run back to the founder
7
+ // as a paste (2026-08-05).
8
+ //
9
+ // Deny is the default in every failure mode: timeout, spool error, malformed
10
+ // answer. An approval can only come from the founder's explicit file.
11
+ //
12
+ // EVERY TOUCH OF THE SPOOL GOES THROUGH main/approvals.mjs, which is where the
13
+ // rule that this directory is worker-writable is written down and enforced --
14
+ // nothing opened through a link, nothing opened that is not a regular file,
15
+ // every write landed by rename. It used to be bare `fs` here, and the audit
16
+ // line was the worst of it: `log.jsonl` is a fixed name, so a worker that
17
+ // symlinked it at `briefs/founder.md` had a repeatable append of its own
18
+ // `command` string into the standing instructions every session is briefed
19
+ // with. This process runs as the worker's own child, so it is the near half of
20
+ // that attack (2026-09-04).
21
+
22
+ import fs from 'node:fs';
23
+ import crypto from 'node:crypto';
24
+ import readline from 'node:readline';
25
+ import { answerIsHers, appendSpoolLine, cardDigest, readSpoolJson, removeSpoolEntry, spoolEntryExists, writeSpoolFile } from './approvals.mjs';
26
+
27
+ const SPOOL = process.env.ZERO_APPROVALS_DIR;
28
+
29
+ // THE KEY THAT SAYS AN ANSWER IS HERS, AND IT IS SAFE TO HAND US.
30
+ //
31
+ // This process is the worker's own child, so its environment is the worker's to
32
+ // read and anything secret given here is a secret the forger has. This is the
33
+ // PUBLIC half of a pair whose private half never leaves Agentbox's process: it
34
+ // verifies and it cannot sign, which is exactly why it can travel this way.
35
+ // main/approvals.mjs holds the whole argument, including what it does not
36
+ // close.
37
+ const PUBLIC_KEY = process.env.ZERO_APPROVALS_PUBKEY;
38
+
39
+ const TIMEOUT_MS = 15 * 60_000;
40
+ const POLL_MS = 2_000;
41
+
42
+ const send = (msg) => process.stdout.write(JSON.stringify(msg) + '\n');
43
+
44
+ async function decide(args) {
45
+ try {
46
+ if (!SPOOL) return { behavior: 'deny', message: 'approvals spool is not configured' };
47
+ // NO KEY IS A CHANNEL THAT CANNOT TELL HER ANSWER FROM A WORKER'S, and a
48
+ // channel that cannot tell must not guess. It refuses here rather than
49
+ // asking and then denying in fifteen minutes, because nothing that arrives
50
+ // in that quarter of an hour could become an approval anyway.
51
+ if (!PUBLIC_KEY) return { behavior: 'deny', message: 'the approvals channel was given no key to check the founder\'s answer with, so nothing here can be approved' };
52
+ fs.mkdirSync(SPOOL, { recursive: true });
53
+ const id = crypto.randomUUID();
54
+ const request = {
55
+ id,
56
+ at: Date.now(),
57
+ product: process.env.ZERO_PRODUCT ?? null,
58
+ item: process.env.ZERO_ITEM ?? null,
59
+ tool: args.tool_name ?? 'unknown',
60
+ input: args.input ?? {},
61
+ };
62
+ // A question that could not be asked has already been answered.
63
+ if (!writeSpoolFile(SPOOL, `${id}.request.json`, JSON.stringify(request))) {
64
+ return { behavior: 'deny', message: 'the approvals spool would not take this request, so nothing was put in front of the founder' };
65
+ }
66
+ const answerName = `${id}.answer.json`;
67
+ const deadline = Date.now() + TIMEOUT_MS;
68
+ while (Date.now() < deadline) {
69
+ // `lstat` rather than `existsSync`, so this sees exactly what the app's
70
+ // own `listPending` sees: an entry by that name, whatever it is.
71
+ if (spoolEntryExists(SPOOL, answerName)) {
72
+ const answer = readSpoolJson(SPOOL, answerName);
73
+ // AN ANSWER THAT IS NOT HERS IS NOT A DECISION AT ALL, and this is the
74
+ // line the whole file turns on. It used to read `answer?.allow` off
75
+ // whatever was in the directory, so any process that could write here
76
+ // was approved -- another worker's card, or its own.
77
+ //
78
+ // AND IT IS NOT A DENY EITHER, which matters as much: denying on an
79
+ // unreadable answer would let one worker force a No onto another's
80
+ // card, losing her Allow in silence. The file goes and the question
81
+ // goes on waiting, exactly as main/codex-approvals.mjs's own sweep
82
+ // does, and her real answer still lands when she gives it.
83
+ if (!answerIsHers(id, answer, PUBLIC_KEY)) {
84
+ removeSpoolEntry(SPOOL, answerName);
85
+ console.warn(`zero: an answer for approval ${id} that the founder did not write has been deleted; the question is still waiting`);
86
+ await new Promise((r) => setTimeout(r, POLL_MS));
87
+ continue;
88
+ }
89
+ // AND HER DECISION IS ABOUT AN ACTION, NOT ABOUT AN ID. The request
90
+ // file is worker-writable and its name and id survive a rewrite of its
91
+ // contents, so a worker could raise one command, replace the body with
92
+ // a harmless one, and have her Allow on the harmless card honoured
93
+ // against the command THIS PROCESS is still holding. `request` here is
94
+ // that memory: it was built from the tool call and has never been read
95
+ // back off the disk, so comparing it against the digest she signed is
96
+ // the whole check. A mismatch is a deny -- she said yes to something
97
+ // else -- and it is loud, because only a rewrite can produce it.
98
+ const mine = cardDigest(request);
99
+ if (!mine || answer.card !== mine) {
100
+ finish(id, request, { allow: false, swapped: true });
101
+ console.warn(`zero: approval ${id} was answered for a different card than the one this run asked about; the request file was rewritten under it and nothing has been allowed`);
102
+ return {
103
+ behavior: 'deny',
104
+ message: 'The approval request changed between being asked and being answered, so the founder\'s decision was for a different action and nothing was allowed. Do not retry the same request this session: finish what your grants allow, and file the rest as a review with the exact commands.',
105
+ };
106
+ }
107
+ finish(id, request, answer);
108
+ if (answer.allow) return { behavior: 'allow', updatedInput: args.input ?? {} };
109
+ return { behavior: 'deny', message: answer.note || 'The founder declined this action. Work with what you are already allowed to do, or park the remainder as a review.' };
110
+ }
111
+ await new Promise((r) => setTimeout(r, POLL_MS));
112
+ }
113
+ finish(id, request, { allow: false, timeout: true });
114
+ return {
115
+ behavior: 'deny',
116
+ message: 'No answer from the founder within 15 minutes. Do not retry the same request this session: finish what your grants allow, and file the rest as a review with the exact commands.',
117
+ };
118
+ } catch (err) {
119
+ return { behavior: 'deny', message: `approvals channel error: ${err.message}` };
120
+ }
121
+ }
122
+
123
+ // One audit line per decision; the request/answer pair leaves the spool.
124
+ function finish(id, request, answer) {
125
+ appendSpoolLine(SPOOL, 'log.jsonl', `${JSON.stringify({ ...request, answer, decidedAt: Date.now() })}\n`);
126
+ for (const f of [`${id}.request.json`, `${id}.answer.json`]) removeSpoolEntry(SPOOL, f);
127
+ }
128
+
129
+ const rl = readline.createInterface({ input: process.stdin });
130
+ rl.on('line', async (line) => {
131
+ let msg;
132
+ try { msg = JSON.parse(line); } catch { return; }
133
+ const { id, method, params } = msg;
134
+ if (method === 'initialize') {
135
+ send({ jsonrpc: '2.0', id, result: {
136
+ protocolVersion: params?.protocolVersion ?? '2024-11-05',
137
+ capabilities: { tools: {} },
138
+ serverInfo: { name: 'zero-approvals', version: '1.0.0' },
139
+ } });
140
+ } else if (method === 'tools/list') {
141
+ send({ jsonrpc: '2.0', id, result: { tools: [{
142
+ name: 'approval_prompt',
143
+ description: 'Asks the founder, live in her inbox, to approve one gated action. She answers within minutes when present; a 15-minute silence is a deny. Use only when the work genuinely cannot proceed without it.',
144
+ inputSchema: {
145
+ type: 'object',
146
+ properties: {
147
+ tool_name: { type: 'string' },
148
+ input: { type: 'object' },
149
+ tool_use_id: { type: 'string' },
150
+ },
151
+ required: ['tool_name', 'input'],
152
+ },
153
+ }] } });
154
+ } else if (method === 'tools/call' && params?.name === 'approval_prompt') {
155
+ const result = await decide(params.arguments ?? {});
156
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
157
+ } else if (id !== undefined) {
158
+ send({ jsonrpc: '2.0', id, result: {} });
159
+ }
160
+ });