agentbox-app 0.1.13 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
@@ -66,10 +66,10 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
66
66
  /**
67
67
  * One setup per tool, held for the life of the app.
68
68
  *
69
- * `find(engine)` is the finder, looking again from scratch (the caller passes
70
- * `recheckClaude` / `recheckCodex`, which also move the config so the rest of
71
- * the app runs on what was just installed). `spawn` and `wait` are seams for
72
- * the tests; nothing in the app passes them.
69
+ * `find(engine)` is the finder, looking again from scratch, and may answer
70
+ * with a promise (the caller passes `lookAgainFor`, which also moves the
71
+ * config so the rest of the app runs on what was just installed). `spawn` and
72
+ * `wait` are seams for the tests; nothing in the app passes them.
73
73
  */
74
74
  export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env = process.env, poll = POLL, onSignIn = () => {} } = {}) {
75
75
  const runEnv = terminalEnv(env);
@@ -109,7 +109,11 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
109
109
 
110
110
  async function go(j, run) {
111
111
  const live = () => j.run === run;
112
- let hit = find(j.engine);
112
+ let hit = await find(j.engine);
113
+ if (!live()) return;
114
+ // The card's first row says Found or Install off this, because it ticks
115
+ // that row either way.
116
+ j.already = !!hit?.found;
113
117
  if (!hit?.found) {
114
118
  j.phase = 'installing';
115
119
  const { file, args } = installCommand(j.engine);
@@ -119,7 +123,7 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
119
123
  j.running = null;
120
124
  if (!live()) return;
121
125
  if (code !== 0) return fail(j, `The installer stopped: ${lastLine(j.log) || `exit ${code}`}`);
122
- hit = find(j.engine);
126
+ hit = await find(j.engine);
123
127
  if (!hit?.found) return fail(j, 'The installer finished, but the app still cannot find it.');
124
128
  }
125
129
  j.bin = hit.path;
@@ -140,7 +144,15 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
140
144
  ended = false;
141
145
  watchLogin(login);
142
146
  }
143
- if (ended) return fail(j, `The sign-in was closed before it finished.${lastLine(j.log) ? ` ${lastLine(j.log)}` : ''}`);
147
+ // ASK ONCE MORE BEFORE CALLING IT CLOSED. A sign-in that finishes while
148
+ // the check above is running exits before that check answers, and the
149
+ // check, asked a moment too early, says no. Without this, somebody who
150
+ // had just approved it was told it was closed.
151
+ if (ended) {
152
+ if (await signedIn(j, hit.path)) { if (live()) ready(j); return; }
153
+ if (!live()) return;
154
+ return fail(j, `The sign-in was closed before it finished.${lastLine(j.log) ? ` ${lastLine(j.log)}` : ''}`);
155
+ }
144
156
  }
145
157
  }
146
158
 
@@ -172,14 +184,14 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
172
184
  },
173
185
  status(engine) {
174
186
  const j = job(engine);
175
- return { engine, phase: j.phase, error: j.error, log: j.log };
187
+ return { engine, phase: j.phase, error: j.error, log: j.log, bin: j.bin ?? null, already: !!j.already };
176
188
  },
177
189
  /**
178
190
  * WHETHER THIS TOOL CAN RUN AN AGENT RIGHT NOW: found, and signed in by its
179
191
  * own account. The walk asks before it asks anything, so a Mac that is
180
192
  * already set up never sees the plan question. */
181
193
  async readiness(engine) {
182
- const hit = find(engine);
194
+ const hit = await find(engine);
183
195
  if (!hit?.found) return { engine, found: false, signedIn: false };
184
196
  try { return { engine, found: true, signedIn: await signedIn({ engine, log: '' }, hit.path) }; }
185
197
  catch (error) { return { engine, found: true, signedIn: false, error: error.message }; }
package/main/feedback.mjs CHANGED
@@ -16,8 +16,9 @@
16
16
  // It is not behind the diagnostics switch. That switch is about what Agentbox
17
17
  // sends on its own; this is something a person typed and pressed Send on.
18
18
  import { createRequire } from 'node:module';
19
- import { checkFeedback } from '../shared/feedback.mjs';
20
- import { Name, readEnv } from '../shared/product-name.mjs';
19
+ import { readEnv } from '../shared/product-name.mjs';
20
+
21
+ export { sendFeedback } from '../shared/feedback.mjs';
21
22
 
22
23
  export const FEEDBACK_ADDRESS = 'https://lywvthwkhkfzhqivvond.supabase.co/functions/v1/feedback';
23
24
 
@@ -35,26 +36,3 @@ const clean = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
35
36
  export function feedbackUrl(config, baked = BAKED, env = process.env) {
36
37
  return clean(config?.feedbackUrl) ?? clean(readEnv('FEEDBACK_URL', env)) ?? clean(baked);
37
38
  }
38
-
39
- // Answers { ok: true } or { ok: false, error } with the error as a sentence.
40
- // It never throws, so the card always has something to say.
41
- export async function sendFeedback({ url, payload, fetchImpl = globalThis.fetch, timeoutMs = 60_000 }) {
42
- if (!url) return { ok: false, error: `Feedback is not set up in this copy of ${Name}.` };
43
- // Checked again here, off the bytes that would actually travel, since the
44
- // renderer is not the last word on what leaves this machine.
45
- const sized = (payload?.files ?? []).map((f) => ({ size: Math.floor((String(f?.data ?? '').length * 3) / 4) }));
46
- const check = checkFeedback({ text: payload?.text, files: sized });
47
- if (!check.ok) return { ok: false, error: check.reason };
48
- try {
49
- const res = await fetchImpl(url, {
50
- method: 'POST',
51
- headers: { 'content-type': 'application/json' },
52
- body: JSON.stringify(payload),
53
- signal: AbortSignal.timeout(timeoutMs),
54
- });
55
- if (res?.ok) return { ok: true };
56
- return { ok: false, error: 'It did not send. Try again in a moment.' };
57
- } catch {
58
- return { ok: false, error: 'It did not send. Check your connection and try again.' };
59
- }
60
- }
@@ -17,10 +17,10 @@ import { servable } from './img-scheme.mjs';
17
17
 
18
18
  const PICTURE = /\.(png|jpe?g|gif|webp|avif)$/i;
19
19
 
20
- export function folderPictures({ src, product, products = [], accountRoot, storeRoot, limit = 60, fs = fsDefault }) {
20
+ export async function folderPictures({ src, product, products = [], accountRoot, storeRoot, limit = 60, fs = fsDefault }) {
21
21
  if (typeof src !== 'string' || !src.trim()) return { ok: false, error: 'No folder was named.' };
22
22
  const named = src.replace(/\/+$/, '');
23
- const found = resolveArtifact({ src: named, product, products, accountRoot, storeRoot, fs });
23
+ const found = await resolveArtifact({ src: named, product, products, accountRoot, storeRoot, fs });
24
24
  if (!found.ok) return { ok: false, error: found.error };
25
25
  const own = products.find((p) => p.slug === product) ?? null;
26
26
  const roots = [accountRoot, own?.dir, own?.repoPath].filter(Boolean);
@@ -0,0 +1,86 @@
1
+ // THE OTHER END OF main/freeze-recorder.mjs: the watcher, on its own thread.
2
+ //
3
+ // The profile is rotated every `rotateMs` so it never grows, and at the end of
4
+ // each freeze so the freeze is in the one it hands back. The one before is
5
+ // kept too, because a freeze can start before a rotation and end after it.
6
+ import { Session } from 'node:inspector';
7
+ import { parentPort, workerData } from 'node:worker_threads';
8
+ import { freezeFrom, writeFreeze } from './freeze-recorder.mjs';
9
+
10
+ const { beat, dir, appDir, thresholdMs, rotateMs, intervalUs } = workerData;
11
+ const stamp = new BigInt64Array(beat);
12
+ const nowUs = () => Number(process.hrtime.bigint() / 1000n);
13
+ // The main thread asleep rather than stuck (App Nap timers, say) has an idle
14
+ // profile. Below this share of busy samples it is not a freeze.
15
+ const BUSY_ENOUGH = 0.5;
16
+
17
+ const session = new Session();
18
+ const post = (method, params = {}) => new Promise((resolve, reject) => {
19
+ session.post(method, params, (err, out) => (err ? reject(err) : resolve(out)));
20
+ });
21
+
22
+ let running = null; // { anchorUs } of the profile being taken
23
+ let previous = null; // { profile, offsetUs } of the last one stopped
24
+ let frozeAt = null; // the last stamp before the freeze in progress, in µs
25
+ const seen = []; // freezes ended and not yet cut out of a profile
26
+ let busy = false;
27
+
28
+ async function start() {
29
+ const before = nowUs();
30
+ await post('Profiler.start');
31
+ running = { anchorUs: (before + nowUs()) / 2 };
32
+ }
33
+
34
+ async function rotate() {
35
+ const { profile } = await post('Profiler.stop');
36
+ const stopped = { profile, offsetUs: profile.startTime - running.anchorUs };
37
+ await start();
38
+ const profiles = [previous, stopped].filter(Boolean);
39
+ previous = stopped;
40
+ for (const f of seen.splice(0)) {
41
+ const read = freezeFrom(profiles, { fromUs: f.fromUs, toUs: f.toUs, appDir });
42
+ if (read.busy < BUSY_ENOUGH) continue;
43
+ const line = { at: new Date(Date.now() - (nowUs() - f.fromUs) / 1000).toISOString(), ms: Math.round((f.toUs - f.fromUs) / 1000), ...read };
44
+ writeFreeze(dir, line);
45
+ parentPort.postMessage({ freeze: { at: line.at, ms: line.ms, where: line.where } });
46
+ }
47
+ }
48
+
49
+ // A freeze that ends while a rotation is under way goes round again.
50
+ async function turn() {
51
+ if (busy || !running) return;
52
+ busy = true;
53
+ try {
54
+ do { await rotate(); } while (seen.length);
55
+ } catch (err) { quit(err); } finally { busy = false; }
56
+ }
57
+
58
+ function watch() {
59
+ const last = Number(Atomics.load(stamp, 0) / 1000n);
60
+ if (nowUs() - last > thresholdMs * 1000) { frozeAt ??= last; return; }
61
+ if (frozeAt === null) return;
62
+ seen.push({ fromUs: frozeAt, toUs: last });
63
+ frozeAt = null;
64
+ void turn();
65
+ }
66
+
67
+ let watching = null;
68
+ let rotating = null;
69
+ function quit(err) {
70
+ clearInterval(watching);
71
+ clearInterval(rotating);
72
+ try { session.disconnect(); } catch { /* already gone */ }
73
+ parentPort.postMessage({ gaveUp: String(err?.message ?? err) });
74
+ }
75
+
76
+ // The timers go first: an inspector reply does not keep this thread alive.
77
+ watching = setInterval(watch, 50);
78
+ rotating = setInterval(() => { if (frozeAt === null) void turn(); }, rotateMs);
79
+ try {
80
+ session.connectToMainThread();
81
+ await post('Profiler.enable');
82
+ await post('Profiler.setSamplingInterval', { interval: intervalUs });
83
+ await start();
84
+ } catch (err) {
85
+ quit(err);
86
+ }
@@ -0,0 +1,146 @@
1
+ // WHAT THE MAIN THREAD WAS DOING WHEN THE APP FROZE.
2
+ //
3
+ // The spinning wheel is this thread not answering, and it comes when nobody is
4
+ // profiling. So a thread of its own watches all the time: the main thread
5
+ // stamps a clock every 100 ms, the
6
+ // watcher keeps a low-rate CPU profile of it running through the inspector,
7
+ // and when the stamps stop for longer than `thresholdMs` it cuts that stretch
8
+ // out of the profile and writes one line saying which of our functions held
9
+ // it, to `freeze-<day>.jsonl` beside the memory log, kept a week.
10
+ //
11
+ // It stays on this computer. Nothing here is sent anywhere.
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { Worker } from 'node:worker_threads';
16
+
17
+ export const THRESHOLD_MS = 500;
18
+ export const KEEP_DAYS = 7;
19
+ const BEAT_MS = 100;
20
+ const FILE = /^freeze-(\d{4}-\d{2}-\d{2})\.jsonl$/;
21
+ const IDLE = new Set(['(idle)', '(program)']);
22
+ const GC = '(garbage collector)';
23
+ const FRAMES_KEPT = 12;
24
+
25
+ export const dayOf = (ms) => {
26
+ const d = new Date(ms);
27
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
28
+ };
29
+
30
+ function fileOf(url, appDirs) {
31
+ if (!url) return '';
32
+ let file = url;
33
+ if (url.startsWith('file://')) { try { file = fileURLToPath(url); } catch { /* keep the url */ } }
34
+ const appDir = appDirs.find((d) => file.startsWith(d + path.sep));
35
+ if (appDir) return file.slice(appDir.length + 1);
36
+ if (file.startsWith('node:') || !path.isAbsolute(file)) return file;
37
+ // Never a home path in the log: a file outside the app is named by its last parts.
38
+ return `…/${file.split(path.sep).slice(-2).join('/')}`;
39
+ }
40
+
41
+ // The profile names a file by its real path, which on macOS is /private/var for /var.
42
+ const realPath = (dir) => { try { return dir ? fs.realpathSync(dir) : ''; } catch { return ''; } };
43
+
44
+ const ours = (frame) => /\.(m?js|cjs|ts)$/.test(frame.file) && !/^(node:|…\/)/.test(frame.file) && !frame.file.includes('node_modules');
45
+ const named = (f) => (f.file ? `${f.fn} ${f.file}:${f.line}` : f.fn);
46
+
47
+ /**
48
+ * What held the thread between `fromUs` and `toUs`, read off one or more CPU
49
+ * profiles. Times are on the watcher's clock; `offsetUs` is each profile's
50
+ * clock minus that one. `appDir` is one path or several. Pure.
51
+ */
52
+ export function freezeFrom(profiles, { fromUs, toUs, appDir = '' }) {
53
+ const appDirs = [appDir].flat().filter(Boolean);
54
+ const groups = new Map();
55
+ let total = 0;
56
+ let idle = 0;
57
+ let gc = 0;
58
+ for (const { profile, offsetUs = 0 } of profiles) {
59
+ if (!profile?.nodes?.length || !profile.samples?.length) continue;
60
+ const byId = new Map(profile.nodes.map((n) => [n.id, n]));
61
+ const parent = new Map();
62
+ for (const n of profile.nodes) for (const c of n.children ?? []) parent.set(c, n.id);
63
+ const stacks = new Map();
64
+ const stackOf = (id) => {
65
+ if (stacks.has(id)) return stacks.get(id);
66
+ const frames = [];
67
+ for (let at = id; at != null; at = parent.get(at)) {
68
+ const cf = byId.get(at)?.callFrame;
69
+ if (!cf || cf.functionName === '(root)') continue;
70
+ frames.push({ fn: cf.functionName || '(anonymous)', file: fileOf(cf.url, appDirs), line: cf.url ? cf.lineNumber + 1 : 0 });
71
+ }
72
+ const leaf = frames[0]?.fn ?? '(program)';
73
+ const kind = frames.length <= 1 && IDLE.has(leaf) ? 'idle' : leaf === GC ? 'gc' : 'busy';
74
+ const first = frames.findIndex(ours);
75
+ const mine = first < 0 ? [] : frames.slice(first);
76
+ const s = { kind, frames, where: mine[0] ? named(mine[0]) : leaf, key: `${leaf}|${mine.slice(0, 10).map(named).join('<')}` };
77
+ stacks.set(id, s);
78
+ return s;
79
+ };
80
+ let t = profile.startTime - offsetUs;
81
+ for (let i = 0; i < profile.samples.length; i += 1) {
82
+ t += profile.timeDeltas?.[i] ?? 0;
83
+ if (t < fromUs || t > toUs) continue;
84
+ total += 1;
85
+ const s = stackOf(profile.samples[i]);
86
+ if (s.kind === 'idle') { idle += 1; continue; }
87
+ if (s.kind === 'gc') gc += 1;
88
+ const g = groups.get(s.key) ?? { samples: 0, s };
89
+ g.samples += 1;
90
+ groups.set(s.key, g);
91
+ }
92
+ }
93
+ const busy = total - idle;
94
+ const top = [...groups.values()].sort((a, b) => b.samples - a.samples).slice(0, 3);
95
+ return {
96
+ samples: total,
97
+ busy: total ? Math.round((busy / total) * 100) / 100 : 0,
98
+ gc: busy ? Math.round((gc / busy) * 100) / 100 : 0,
99
+ where: top[0]?.s.where ?? null,
100
+ stacks: top.map((g) => ({ share: Math.round((g.samples / busy) * 100) / 100, frames: g.s.frames.slice(0, FRAMES_KEPT).map(named) })),
101
+ };
102
+ }
103
+
104
+ /** One freeze's line, appended to today's file; older files than a week go. */
105
+ export function writeFreeze(dir, line, { keepDays = KEEP_DAYS } = {}) {
106
+ fs.mkdirSync(dir, { recursive: true });
107
+ const at = Date.parse(line.at) || Date.now();
108
+ fs.appendFileSync(path.join(dir, `freeze-${dayOf(at)}.jsonl`), `${JSON.stringify(line)}\n`);
109
+ const oldest = dayOf(new Date(at).setDate(new Date(at).getDate() - (keepDays - 1)));
110
+ for (const f of fs.readdirSync(dir)) {
111
+ const m = f.match(FILE);
112
+ if (m && m[1] < oldest) fs.rmSync(path.join(dir, f), { force: true });
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Starts the watcher for this thread. `onFreeze` hears each line as it is
118
+ * written. Never throws: a recorder that breaks the app is worse than a gap.
119
+ */
120
+ export function startFreezeRecorder({ dir, appDir = '', thresholdMs = THRESHOLD_MS, rotateMs = 60_000, intervalUs = 10_000, onFreeze = () => {} } = {}) {
121
+ const beat = new SharedArrayBuffer(8);
122
+ const stamp = new BigInt64Array(beat);
123
+ const tick = () => Atomics.store(stamp, 0, process.hrtime.bigint());
124
+ tick();
125
+ const timer = setInterval(tick, BEAT_MS);
126
+ timer.unref?.();
127
+ let worker = null;
128
+ try {
129
+ worker = new Worker(new URL('./freeze-recorder-thread.mjs', import.meta.url), {
130
+ workerData: { beat, dir, appDir: [appDir, realPath(appDir)], thresholdMs, rotateMs, intervalUs },
131
+ });
132
+ } catch (err) {
133
+ clearInterval(timer);
134
+ console.warn(`zero: freezes will not be recorded: ${err.message}`);
135
+ return { stop() {} };
136
+ }
137
+ worker.unref();
138
+ worker.on('message', (m) => {
139
+ if (m?.freeze) { try { onFreeze(m.freeze); } catch { /* the line is already on disk */ } }
140
+ if (m?.gaveUp) console.warn(`zero: freezes will not be recorded: ${m.gaveUp}`);
141
+ });
142
+ worker.on('error', (err) => { clearInterval(timer); console.warn(`zero: the freeze recorder stopped: ${err.message}`); });
143
+ return {
144
+ stop() { clearInterval(timer); worker?.terminate().catch(() => {}); },
145
+ };
146
+ }
@@ -44,6 +44,22 @@ export function relaunchCommand({ execPath = process.execPath, argv = process.ar
44
44
  return { bin: execPath, args: appPath ? [appPath] : [] };
45
45
  }
46
46
 
47
+ /**
48
+ * THE NOTES ARE THE HONEST PART. Each is something a fresh copy can be missing
49
+ * without looking broken, and a copy that quietly cannot run an agent is one
50
+ * that lies, so they are said rather than assumed.
51
+ *
52
+ * Pure, and reads nothing, because what it says about a missing Claude Code
53
+ * otherwise depends on whether the computer running the test has one.
54
+ */
55
+ export function freshUserNotes({ claudeBin, keychain, agents } = {}, { withTools = true, withAgents = true } = {}) {
56
+ const notes = [];
57
+ if (withTools && !claudeBin) notes.push('That copy has no Claude Code of its own, so its setup offers to set one up or to use ChatGPT.');
58
+ if (!keychain) notes.push('There is no ~/Library/Keychains here, so Claude Code will say it is not logged in.');
59
+ if (withAgents && !agents) notes.push('There is no ~/.claude on this computer, so it will have no agents to offer.');
60
+ return notes;
61
+ }
62
+
47
63
  /**
48
64
  * Open one. Returns what happened, in the words the app will show her: this
49
65
  * is reported to the window rather than thrown, because a row in ⌘K that
@@ -90,15 +106,10 @@ export function openFreshUser({
90
106
  return { ok: false, error: `Could not start a second ${NAME}: ${err?.message ?? err}`, home: prepared.home };
91
107
  }
92
108
 
93
- // THE NOTES ARE THE HONEST PART. Both of these are things a fresh copy can
94
- // be missing without looking broken, and a test that quietly cannot run an
95
- // agent is a test that lies. They are said rather than assumed.
96
- const notes = [];
97
- if (withTools && !prepared.claudeBin) notes.push('Claude Code is not at ~/.local/bin/claude, so agents will not run in it.');
98
- if (!prepared.keychain) notes.push('There is no ~/Library/Keychains here, so Claude Code will say it is not logged in.');
99
- if (withAgents && !prepared.agents) notes.push('There is no ~/.claude on this computer, so it will have no agents to offer.');
100
-
101
- return { ok: true, home: prepared.home, agents: prepared.agents, notes, pid: child?.pid ?? null };
109
+ return {
110
+ ok: true, home: prepared.home, agents: prepared.agents, pid: child?.pid ?? null,
111
+ notes: freshUserNotes(prepared, { withTools, withAgents }),
112
+ };
102
113
  }
103
114
 
104
115
  /**
@@ -9,9 +9,11 @@
9
9
  // runs whole inside a worker thread, and if the thread cannot start the job
10
10
  // runs here, the old way.
11
11
  import { Worker } from 'node:worker_threads';
12
- import * as change from './git-change.mjs';
12
+ import * as git from './git-change.mjs';
13
+ import { saveRunChange } from './run-change.mjs';
13
14
 
14
- const JOBS = new Set(['snapshotRepo']);
15
+ const change = { ...git, saveRunChange };
16
+ const JOBS = new Set(['snapshotRepo', 'saveRunChange']);
15
17
 
16
18
  let worker = null;
17
19
  let broken = false;
@@ -55,6 +57,11 @@ function thread() {
55
57
  return worker;
56
58
  }
57
59
 
60
+ /** Settles once no job is waiting on the thread. */
61
+ export async function __gitJobsIdle() {
62
+ while (waiting.size) await new Promise((resolve) => { setTimeout(resolve, 10); });
63
+ }
64
+
58
65
  /** Run one of git-change.mjs's exports without blocking this thread. */
59
66
  export function gitJob(name, ...args) {
60
67
  if (!JOBS.has(name)) return Promise.reject(new Error(`not a git job: ${name}`));
@@ -65,6 +72,13 @@ export function gitJob(name, ...args) {
65
72
  const id = ++seq;
66
73
  waiting.set(id, job);
67
74
  w.ref();
68
- w.postMessage({ id, name, args });
75
+ try {
76
+ w.postMessage({ id, name, args });
77
+ } catch (error) {
78
+ // Arguments a thread cannot be handed (a function, say) run here.
79
+ waiting.delete(id);
80
+ if (!waiting.size) w.unref();
81
+ runHere(job);
82
+ }
69
83
  });
70
84
  }
@@ -1,10 +1,13 @@
1
1
  // THE OTHER END OF main/git-change-offthread.mjs. One job at a time, in order.
2
2
  import { parentPort } from 'node:worker_threads';
3
- import * as change from './git-change.mjs';
3
+ import * as git from './git-change.mjs';
4
+ import { saveRunChange } from './run-change.mjs';
5
+
6
+ const jobs = { ...git, saveRunChange };
4
7
 
5
8
  parentPort.on('message', ({ id, name, args }) => {
6
9
  try {
7
- parentPort.postMessage({ id, ok: true, value: change[name](...args) });
10
+ parentPort.postMessage({ id, ok: true, value: jobs[name](...args) });
8
11
  } catch (error) {
9
12
  parentPort.postMessage({ id, ok: false, error: String(error?.message ?? error) });
10
13
  }
@@ -10,11 +10,20 @@ import { claudeModelRows } from '../claude-models.mjs';
10
10
  import { linkAccountTooling } from '../account-tooling.mjs';
11
11
  import { streamInputArgs, attachClaudeInput } from '../claude-input.mjs';
12
12
  import { claudeLaunchEnv } from '../claude-launch-env.mjs';
13
+ import { cloudClaudeMode, cloudPermissionArgs } from '../cloud-permission.mjs';
13
14
  import { claudeActivity } from '../agent-activity.mjs';
14
15
  import { captureStream, streamingText, summarizeStreamLine, traceStreamLine } from './claude-stream.mjs';
15
16
  import { definitionMethods, commonUpdatePlan } from './common.mjs';
17
+ import { findTranscript } from '../transcript-search.mjs';
16
18
  const definition = harnessDefinition('claude');
17
19
 
20
+ // THE SETTINGS LAYER THAT TAKES A METERED KEY BACK OFF A RUN. Claude Code reads
21
+ // `env.ANTHROPIC_API_KEY` and `apiKeyHelper` out of a person's own and a
22
+ // project's settings files, past any scrub of the environment, and `--settings`
23
+ // outranks both (measured on 2.1.301, w-6d8c543a43). Every Claude Code run the
24
+ // app starts carries it: workers in their settings file, small calls inline.
25
+ export const NO_METERED_KEY = Object.freeze({ env: Object.freeze({ ANTHROPIC_API_KEY: '' }), apiKeyHelper: '' });
26
+
18
27
  export const claudeHarness = {
19
28
  ...definitionMethods(definition),
20
29
  discover: resolveClaudeBin,
@@ -33,7 +42,27 @@ export const claudeHarness = {
33
42
  return [path.join(home, '.claude.json'), ...definition.credentialFiles.map(file => path.join(dir, file))];
34
43
  },
35
44
  tooling: profile => linkAccountTooling(profile),
36
- spawn(sup, plan, { cwd, item, profile, env }) {
45
+ // A cloud run's command line and environment. A kit for the phone is made
46
+ // from the same two (Supervisor#_writeKit), so the cloud's turn is this one.
47
+ cloudLaunch(sup, plan, { item, profile, env }) {
48
+ return {
49
+ args: streamInputArgs(definition.run, cloudPermissionArgs(plan.args, cloudClaudeMode(sup.config.cloudPermission, plan.answerMode))),
50
+ env: { ...env, STORE_ACCOUNT_ID: sup.config.accountId, ...storeRootEnv(sup.config.storeRoot),
51
+ CLAUDE_CONFIG_DIR: sup._claudeConfigDir(profile), ZERO_PRODUCT: item.product, ZERO_ITEM: item.id, ...sup._cloudDocsEnv(item) },
52
+ };
53
+ },
54
+ spawn(sup, plan, { cwd, item, profile, env, cloud = false }) {
55
+ // A CLOUD RUN: THE SAME ARGUMENTS, A DIFFERENT PROGRAM (docs/cloud/PLAN.md).
56
+ // main/cloud/run-cli.mjs runs Claude Code in the sandbox with exactly these
57
+ // arguments and is, to everything above, the worker: its stdout is the
58
+ // agent's stream, its stdin reaches the agent, its exit comes after the
59
+ // agent's work is back in `cwd`. The account folder is always named,
60
+ // because the run unpacks the transcript and tooling into exactly that
61
+ // path. No memory-gate variables: that gate is about this Mac's memory.
62
+ if (cloud) {
63
+ const launch = claudeHarness.cloudLaunch(sup, plan, { item, profile, env });
64
+ return sup._spawnCloudRelay(launch.args, { cwd, env: launch.env });
65
+ }
37
66
  // The binary is the one this definition names, not a field spelled out
38
67
  // here: that is the last place an engine of this family needed code.
39
68
  return spawn(sup.config[definition.binKey], streamInputArgs(definition.run, plan.args), {
@@ -60,20 +89,12 @@ export const claudeHarness = {
60
89
  transcript(sup, rec) {
61
90
  if (!rec?.sessionId) return null;
62
91
  const home = rec.profile && rec.profile !== 'default' ? rec.profile : path.join(os.homedir(), '.claude');
63
- const projects = path.join(home, 'projects');
64
- const slug = String(rec.cwd ?? '').replace(/[^a-zA-Z0-9]/g, '-');
65
- const direct = path.join(projects, slug, `${rec.sessionId}.jsonl`);
66
- if (fs.existsSync(direct)) return direct;
67
- try { for (const dir of fs.readdirSync(projects)) {
68
- const file = path.join(projects, dir, `${rec.sessionId}.jsonl`);
69
- if (fs.existsSync(file)) return file;
70
- } } catch {}
71
- return null;
92
+ return findTranscript(path.join(home, 'projects'), rec.sessionId, rec.cwd, { look: rec.cloud ? 'never' : 'now' });
72
93
  },
73
94
  change: () => null,
74
95
  release: () => {},
75
96
  memoryGate: (sup, item) => sup.memoryGateEnv(item),
76
- smallModelArgs: (prompt, model) => ['-p', prompt, '--model', model],
97
+ smallModelArgs: (prompt, model) => ['-p', prompt, '--model', model, '--settings', JSON.stringify(NO_METERED_KEY)],
77
98
  updateSettings: ({ home }) => {
78
99
  try { return JSON.parse(fs.readFileSync(path.join(process.env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'), 'settings.json'), 'utf8')); }
79
100
  catch { return {}; }
@@ -3,7 +3,8 @@ import { harnessDefinition } from '../../shared/harness-definitions.mjs';
3
3
  import { resolveCodexBin } from '../codex-bin.mjs';
4
4
  import { codexLaunchEnv } from '../codex-launch-env.mjs';
5
5
  import { codexModels, codexDefaultModel, codexModelLevels } from '../codex-models.mjs';
6
- import { codexTranscriptFile, workerThreadParams } from '../codex-session.mjs';
6
+ import { codexTranscriptFile, workerThreadParams, refusedCodexWorker } from '../codex-session.mjs';
7
+ import { codexKeyRefusal } from '../codex-account.mjs';
7
8
  import { compactCodexThread } from '../codex-compaction.mjs';
8
9
  import { captureCodexEvent, codexStreamingText, summarizeCodexEvent, traceCodexEvent, rememberCodexChange, changeFromCodexTurn } from '../codex.mjs';
9
10
  import { codexActivity } from '../agent-activity.mjs';
@@ -29,7 +30,13 @@ export const codexHarness = {
29
30
  // rather than inheriting the refusal meant for an engine that has nowhere to
30
31
  // put them (main/harnesses/instructions-file.mjs).
31
32
  instructions: () => ({ by: 'thread' }),
32
- spawn: (sup, plan, context) => sup._spawnCodexWorker(plan, context),
33
+ // AN API-KEY LOGIN IS REFUSED HERE, before an app-server is asked anything:
34
+ // Codex reads the key out of auth.json itself, past the worker scrub, and
35
+ // bills every call to it (main/codex-account.mjs `codexKeyRefusal`).
36
+ spawn: (sup, plan, context) => {
37
+ const refusal = codexKeyRefusal(sup._codexProfileHome(context?.profile ?? 'default'));
38
+ return refusal ? refusedCodexWorker(refusal) : sup._spawnCodexWorker(plan, context);
39
+ },
33
40
  readers: () => ({ capture: captureCodexEvent, stream: codexStreamingText, summarize: summarizeCodexEvent, trace: traceCodexEvent, activity: codexActivity }),
34
41
  subscribe(child, session, absorb) {
35
42
  child.on('event', (method, params) => { rememberCodexChange(session, method, params); absorb(method, params); });
@@ -17,22 +17,80 @@
17
17
 
18
18
  import os from 'node:os';
19
19
  import { appSessionFolders } from './agent-homes.mjs';
20
- import { RECENT_DAYS, readSessionThreads } from './agent-sessions.mjs';
20
+ import { RECENT_DAYS, readProjectChats, readSessionThreads } from './agent-sessions.mjs';
21
21
  import { readCodexThreads } from './codex-threads.mjs';
22
22
 
23
23
  export const OLDER_DAYS = 90;
24
24
  export const OLDER_MOST = 20;
25
25
 
26
- export function readImportableThreads({ config = null, supervisor = null, env = process.env, shellEnv, home = os.homedir(), now = Date.now() } = {}) {
26
+ // `readCodex` is the app's own reader off the main process (codex-threads.mjs
27
+ // readCodexThreadsOffThread): a large Codex folder takes seconds to read.
28
+ export async function readImportableThreads({ config = null, supervisor = null, env = process.env, shellEnv, home = os.homedir(), now = Date.now(), readCodex = readCodexThreads } = {}) {
27
29
  const { dirs, codexDirs } = appSessionFolders({ config, supervisor, env, home, ...(shellEnv ? { shellEnv } : {}) });
28
- const read = (days, max) => {
30
+ const read = async (days, max) => {
29
31
  let claude = [];
30
32
  let codex = [];
31
33
  try { ({ threads: claude } = readSessionThreads({ home, dirs, now, days, ...(max ? { max } : {}) })); } catch { claude = []; }
32
- try { ({ threads: codex } = readCodexThreads({ home, codexDirs, now, days })); } catch { codex = []; }
34
+ try { ({ threads: codex } = await readCodex({ home, codexDirs, now, days })); } catch { codex = []; }
33
35
  return [...claude, ...codex].sort((a, b) => (b.when ?? 0) - (a.when ?? 0));
34
36
  };
35
- const recent = read(RECENT_DAYS);
37
+ const recent = await read(RECENT_DAYS);
36
38
  if (recent.length) return recent;
37
- return read(OLDER_DAYS, OLDER_MOST).slice(0, OLDER_MOST);
39
+ return (await read(OLDER_DAYS, OLDER_MOST)).slice(0, OLDER_MOST);
40
+ }
41
+
42
+ /** What the watch that brings new chats in reads (main/new-chats-watch.mjs):
43
+ * Claude Code chats in the projects' own folders since `since`, and the last
44
+ * `days` of Codex. */
45
+ export async function readRecentThreads({
46
+ config = null, supervisor = null, env = process.env, home = os.homedir(), now = Date.now(),
47
+ days = 1, since = 0, folders = [], readCodex = readCodexThreads,
48
+ } = {}) {
49
+ const { dirs, codexDirs } = appSessionFolders({ config, supervisor, env, home });
50
+ let claude = [];
51
+ let codex = [];
52
+ try { claude = readProjectChats({ home, dirs, folders, since }); } catch { claude = []; }
53
+ try { ({ threads: codex } = await readCodex({ home, codexDirs, now, days })); } catch { codex = []; }
54
+ return [...claude, ...codex];
55
+ }
56
+
57
+ /* ------------------------ AND THE ONE THE SEARCH USES ---------------------- */
58
+ // THE CARD'S LIST IS TEN DAYS. THE NAME FIELD IS NOT (w-6418318cd5, from
59
+ // GitHub #31). A chat you had in July could not be brought in at all, so a
60
+ // field that only searched the same ten days would be decoration.
61
+ //
62
+ // IT IS A SECOND READ, NOT A WIDER FIRST ONE. The ten-day list is what the
63
+ // card draws the moment it opens and it has to be quick; this one is taken
64
+ // once, lazily, the first time somebody types, and the window keeps it (see
65
+ // `zero:search-agent-threads` in main/ipc.mjs) so every keystroke after that
66
+ // is a filter rather than a disk walk.
67
+ //
68
+ // AND IT CANNOT PROMISE EVERYTHING. A chat's name lives inside its transcript,
69
+ // and the Mac this was written on holds 29,703 of them. So it goes newest
70
+ // first until the cap or the budget stops it and reports `reached`, the moment
71
+ // it gave up at, which the card prints. Eight seconds rather than three: this
72
+ // is the read somebody is waiting on, watching a spinner, having already typed
73
+ // the word they want.
74
+
75
+ export const SEARCH_DAYS = 365;
76
+ const SEARCH_MOST = 6000;
77
+ const SEARCH_BUDGET_MS = 8000;
78
+
79
+ export async function readSearchableThreads({
80
+ config = null, supervisor = null, env = process.env, shellEnv, home = os.homedir(), now = Date.now(),
81
+ days = SEARCH_DAYS, max = SEARCH_MOST, budgetMs = SEARCH_BUDGET_MS, readCodex = readCodexThreads,
82
+ } = {}) {
83
+ const { dirs, codexDirs } = appSessionFolders({ config, supervisor, env, home, ...(shellEnv ? { shellEnv } : {}) });
84
+ let claude = [];
85
+ let codex = [];
86
+ let reached = 0;
87
+ try {
88
+ ({ threads: claude, reached } = readSessionThreads({ home, dirs, now, days, max, budgetMs, ends: false }));
89
+ } catch { claude = []; }
90
+ // Codex keeps its names in an index of its own (main/codex-threads.mjs), so
91
+ // its side costs no transcript reads and never gives up early. Only the
92
+ // Claude side can run out of budget, which is why `reached` comes off it.
93
+ try { ({ threads: codex } = await readCodex({ home, codexDirs, now, days })); } catch { codex = []; }
94
+ const threads = [...claude, ...codex].sort((a, b) => (b.when ?? 0) - (a.when ?? 0));
95
+ return { threads, reached };
38
96
  }