agentbox-app 0.1.11 → 0.1.13

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 (239) hide show
  1. package/README.md +24 -16
  2. package/bin/agentbox.mjs +18 -9
  3. package/briefs/message-rules.md +15 -4
  4. package/main/account-tooling.mjs +2 -1
  5. package/main/acp.mjs +296 -0
  6. package/main/agent-homes.mjs +158 -0
  7. package/main/agent-sessions.mjs +57 -20
  8. package/main/agent-updates.mjs +9 -20
  9. package/main/analytics.mjs +30 -0
  10. package/main/approvals.mjs +14 -5
  11. package/main/claude-bin.mjs +45 -310
  12. package/main/claude-input.mjs +9 -3
  13. package/main/claude-launch-env.mjs +54 -0
  14. package/main/claude-models.mjs +39 -7
  15. package/main/claude-plan.mjs +14 -2
  16. package/main/claude-usage.mjs +82 -25
  17. package/main/codex-bin.mjs +51 -248
  18. package/main/codex-launch-env.mjs +31 -0
  19. package/main/codex-session.mjs +38 -3
  20. package/main/codex-threads.mjs +41 -8
  21. package/main/codex-watch.mjs +67 -3
  22. package/main/config.mjs +40 -13
  23. package/main/corner-tag.mjs +489 -0
  24. package/main/crash-report.mjs +26 -3
  25. package/main/demo.mjs +17 -14
  26. package/main/engine-bin.mjs +595 -0
  27. package/main/engine-setup.mjs +27 -27
  28. package/main/feedback.mjs +11 -6
  29. package/main/fresh-user.mjs +1 -1
  30. package/main/git-change.mjs +8 -2
  31. package/main/harnesses/claude-stream.mjs +208 -0
  32. package/main/harnesses/claude.mjs +84 -0
  33. package/main/harnesses/codex.mjs +65 -0
  34. package/main/harnesses/common.mjs +78 -0
  35. package/main/harnesses/instructions-file.mjs +116 -0
  36. package/main/harnesses/opencode.mjs +234 -0
  37. package/main/harnesses.mjs +65 -0
  38. package/main/importable-threads.mjs +38 -0
  39. package/main/instruction-settings.mjs +47 -11
  40. package/main/ipc.mjs +175 -22
  41. package/main/local-app-frames.mjs +82 -0
  42. package/main/machine.mjs +1 -1
  43. package/main/main.mjs +120 -5
  44. package/main/memory-gate-history.mjs +51 -12
  45. package/main/memory-gate-server.mjs +67 -11
  46. package/main/memory-gate.mjs +49 -8
  47. package/main/memory-log.mjs +124 -0
  48. package/main/notify.mjs +48 -5
  49. package/main/opencode-server.mjs +271 -0
  50. package/main/opencode.mjs +287 -0
  51. package/main/phone-link.mjs +551 -0
  52. package/main/refresh-agent-models.mjs +2 -1
  53. package/main/relay-address.mjs +28 -0
  54. package/main/relay-client.mjs +246 -0
  55. package/main/row-label.mjs +8 -7
  56. package/main/runs-on.mjs +90 -0
  57. package/main/serve.mjs +124 -10
  58. package/main/settings.mjs +171 -11
  59. package/main/shell-path.mjs +86 -0
  60. package/main/ship-queue.mjs +25 -7
  61. package/main/sign-in-files.mjs +3 -7
  62. package/main/source-updater.mjs +27 -1
  63. package/main/start-hold.mjs +73 -0
  64. package/main/store/home.mjs +37 -5
  65. package/main/store/project.mjs +15 -19
  66. package/main/store.mjs +10 -4
  67. package/main/supervisor.mjs +1030 -447
  68. package/main/task-commands.mjs +13 -17
  69. package/main/task-folders-offthread.mjs +6 -1
  70. package/main/task-folders.mjs +491 -82
  71. package/main/task-terminals.mjs +49 -8
  72. package/main/team/index.mjs +2 -2
  73. package/main/team/projects.mjs +1 -1
  74. package/main/updater.mjs +71 -9
  75. package/main/worktree-include.mjs +494 -0
  76. package/mcp/core/page.mjs +1 -1
  77. package/mcp/core/work.mjs +34 -3
  78. package/mcp/tools.mjs +2 -1
  79. package/package.json +10 -1
  80. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  81. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  82. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  83. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  84. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  85. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  86. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  87. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  88. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  89. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  90. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  91. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  92. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  93. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  94. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  95. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  96. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  97. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  98. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  99. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  100. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  101. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  102. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  103. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  104. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  105. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  106. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  107. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  108. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  109. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  110. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  111. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  112. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  113. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  114. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  115. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  116. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  117. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  118. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  119. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  120. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  121. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  122. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  123. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  124. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  125. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  126. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  127. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  128. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  129. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  130. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  131. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  132. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  133. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  134. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  135. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  136. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  137. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  138. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  139. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  140. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  141. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  142. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  143. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  144. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  145. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  146. package/renderer/dist/corner-tag.html +13 -0
  147. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  148. package/renderer/dist/emulator/data/compression/README.md +11 -0
  149. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  150. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  151. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  152. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  153. package/renderer/dist/emulator/data/cores/README.md +22 -0
  154. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  155. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  156. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  157. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  158. package/renderer/dist/emulator/data/cores/package.json +65 -0
  159. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  160. package/renderer/dist/emulator/data/emulator.css +1614 -0
  161. package/renderer/dist/emulator/data/loader.js +170 -0
  162. package/renderer/dist/emulator/data/localization/README.md +63 -0
  163. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  164. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  165. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  166. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  167. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  168. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  169. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  170. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  171. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  172. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  173. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  174. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  175. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  176. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  177. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  178. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  179. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  180. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  181. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  182. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  183. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  184. package/renderer/dist/emulator/data/src/compression.js +142 -0
  185. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  186. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  187. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  188. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  189. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  190. package/renderer/dist/emulator/data/src/storage.js +125 -0
  191. package/renderer/dist/emulator/data/version.json +5 -0
  192. package/renderer/dist/emulator/host.js +297 -0
  193. package/renderer/dist/emulator/index.html +28 -0
  194. package/renderer/dist/emulator/provenance.json +6 -0
  195. package/renderer/dist/index.html +16 -3
  196. package/scripts/after-install.mjs +14 -0
  197. package/scripts/approval-server.sh +31 -7
  198. package/scripts/before-publish.mjs +12 -10
  199. package/shared/agent-import.mjs +3 -1
  200. package/shared/agents.d.mts +1 -0
  201. package/shared/bridge-map.mjs +19 -0
  202. package/shared/claude-commands.generated.mjs +8 -6
  203. package/shared/claude-models.generated.mjs +10 -8
  204. package/shared/codex-modes.mjs +1 -1
  205. package/shared/corner-tag.d.mts +14 -0
  206. package/shared/corner-tag.mjs +141 -0
  207. package/shared/demo-world.mjs +1 -1
  208. package/shared/engines.mjs +20 -5
  209. package/shared/fresh-user-home.mjs +12 -1
  210. package/shared/harness-definitions.mjs +103 -0
  211. package/shared/instruction-defaults.json +1 -1
  212. package/shared/notify-rules.d.mts +25 -0
  213. package/shared/notify-rules.mjs +19 -0
  214. package/shared/project-folder-check.mjs +3 -3
  215. package/shared/provider-commands.mjs +10 -2
  216. package/shared/rank.d.mts +2 -0
  217. package/shared/rank.mjs +50 -0
  218. package/shared/relay-address.mjs +6 -0
  219. package/shared/relay-crypto.mjs +165 -0
  220. package/shared/runs-on.d.mts +10 -0
  221. package/shared/runs-on.mjs +55 -0
  222. package/shared/shortcuts.mjs +11 -5
  223. package/shared/spawn-trouble.mjs +58 -7
  224. package/shared/terminal-input.d.mts +1 -0
  225. package/shared/terminal-input.mjs +30 -0
  226. package/shared/terminal-keys.d.mts +1 -0
  227. package/shared/terminal-keys.mjs +16 -0
  228. package/shared/terminal-size.d.mts +4 -0
  229. package/shared/terminal-size.mjs +29 -0
  230. package/shared/usage.d.mts +3 -0
  231. package/shared/usage.mjs +7 -0
  232. package/shared/where-it-runs-from.d.mts +43 -0
  233. package/shared/where-it-runs-from.mjs +155 -0
  234. package/shared/work-items.mjs +28 -2
  235. package/shared/work-lines.mjs +1 -1
  236. package/shared/xterm-invalid-range.d.mts +2 -0
  237. package/shared/xterm-invalid-range.mjs +58 -0
  238. package/renderer/dist/assets/index-B-2Az952.css +0 -32
  239. package/renderer/dist/assets/index-D6qncgai.js +0 -1168
@@ -10,7 +10,8 @@
10
10
  // by reading it every few minutes and everything is lost by reading it on a
11
11
  // timer the window waits for.
12
12
  //
13
- // SO IT IS A CACHE WITH A BACKGROUND REFRESH, and never a call anything awaits:
13
+ // SO IT IS A CACHE WITH A BACKGROUND REFRESH. Only the explicit Settings
14
+ // refresh waits for a result; snapshots always return immediately:
14
15
  //
15
16
  // - `read` answers instantly, always, from the last measurement or with null.
16
17
  // It never blocks and never throws.
@@ -33,7 +34,7 @@ import fs from 'node:fs';
33
34
  import os from 'node:os';
34
35
  import path from 'node:path';
35
36
  import { readUsage } from '../shared/claude-usage.mjs';
36
- import { localZone } from '../shared/usage.mjs';
37
+ import { localZone, USAGE_UNREADABLE } from '../shared/usage.mjs';
37
38
 
38
39
  // WHERE THE READING RUNS, AND WHY IT IS NOT "WHEREVER THE APP IS". An app
39
40
  // opened from Finder has `/` as its working directory, and this used to spawn
@@ -76,20 +77,36 @@ export class ClaudeUsage {
76
77
  * is, asked each time rather than captured: it can be installed while the
77
78
  * app is running, and the settings screen already promises that works.
78
79
  * @param { => void} [how.onChange] Told when a new reading lands, so the
79
- * window redraws. Never called for a refresh that changed nothing.
80
+ * window redraws, including a new checked time on unchanged percentages.
81
+ * @param { => { profile: string, env?: object }} [how.account] WHOSE LIMIT
82
+ * IS READ, asked each time. It used to be nobody's in particular: the
83
+ * command ran with the app's own environment, which is the default login,
84
+ * so a Mac with a second account picked as In use showed "No usage reading
85
+ * yet" for good, because the default one was at its limit and Claude Code
86
+ * printed no limits for it (2026-10-08, Claude Code 2.1.293). `profile` is
87
+ * the account's name ('default' or its CLAUDE_CONFIG_DIR) and `env` is the
88
+ * environment to run it in. Supervisor.usageAccount is the real one.
80
89
  */
81
- constructor({ bin, onChange = () => {}, run = execFile, now = () => Date.now(), where = usageCwd } = {}) {
90
+ constructor({ bin, onChange = () => {}, run = execFile, now = () => Date.now(), where = usageCwd, account = () => ({ profile: 'default' }) } = {}) {
82
91
  this._bin = bin;
92
+ this._selectedProfile = null;
93
+ this._generation = 0;
94
+ this._pending = null;
83
95
  this._onChange = onChange;
84
96
  this._run = run;
85
97
  this._now = now;
86
98
  this._where = where;
87
- /** @type {{ limits: any[], at: number, zone: string|null }|null} */
99
+ this._account = account;
100
+ /** @type {{ limits: any[], at: number, zone: string|null, profile: string }|null} */
88
101
  this._last = null;
89
102
  this._busy = false;
90
103
  this._missedUntil = 0;
91
104
  }
92
105
 
106
+ _who() {
107
+ try { return this._account() ?? { profile: 'default' }; } catch { return { profile: 'default' }; }
108
+ }
109
+
93
110
  /**
94
111
  * THE LAST READING, INSTANTLY, and a refresh started if it has gone stale.
95
112
  *
@@ -98,13 +115,28 @@ export class ClaudeUsage {
98
115
  * than a pill full of dashes.
99
116
  */
100
117
  read() {
118
+ this._syncProfile();
101
119
  this._maybeRefresh();
102
- return this._last ? { limits: this._last.limits, at: this._last.at } : null;
120
+ return this.peek();
103
121
  }
104
122
 
105
123
  // The secondary subscription can show its last reading without starting
106
124
  // another coding process merely to populate the sidebar.
107
- peek() { return this._last ? { limits: this._last.limits, at: this._last.at } : null; }
125
+ peek() {
126
+ this._syncProfile();
127
+ return this._last ? { limits: this._last.limits, at: this._last.at, profile: this._last.profile } : null;
128
+ }
129
+
130
+ _syncProfile() {
131
+ const account = this._who().profile || 'default';
132
+ if (account !== this._selectedProfile) {
133
+ this._selectedProfile = account;
134
+ this._generation++;
135
+ this._last = null;
136
+ this._missedUntil = 0;
137
+ }
138
+ return account;
139
+ }
108
140
 
109
141
  _maybeRefresh() {
110
142
  const now = this._now();
@@ -114,33 +146,58 @@ export class ClaudeUsage {
114
146
  }
115
147
 
116
148
  /** Ask again now, whatever the cache says. The settings screen's own button. */
117
- refreshNow() {
149
+ async refreshNow() {
150
+ this._syncProfile();
118
151
  this._missedUntil = 0;
119
- if (!this._busy) this._refresh();
152
+ // Join an existing reading, including one begun by the background poll.
153
+ // If it belongs to the previous login, wait for it and then ask this one.
154
+ const generation = this._generation;
155
+ const pendingGeneration = this._pendingGeneration;
156
+ const result = await (this._pending ?? this._refresh());
157
+ this._syncProfile();
158
+ if (generation !== this._generation) return { ok: false, error: 'The account changed. Refresh usage again.' };
159
+ if (pendingGeneration != null && pendingGeneration !== generation) return this.refreshNow();
160
+ return result;
120
161
  }
121
162
 
122
163
  _refresh() {
164
+ const profile = this._syncProfile();
165
+ const generation = this._generation;
123
166
  const bin = (() => { try { return this._bin(); } catch { return null; } })();
124
- if (!bin) { this._missed(); return; }
167
+ const failure = () => ({ ok: false, error: USAGE_UNREADABLE });
168
+ if (!bin) { this._missed(); return Promise.resolve(failure()); }
125
169
  this._busy = true;
126
- this._run(
170
+ this._pendingGeneration = generation;
171
+ let resolve;
172
+ const pending = new Promise(done => { resolve = done; });
173
+ this._pending = pending;
174
+ const finish = (err, stdout) => {
175
+ this._busy = false;
176
+ this._pending = null;
177
+ this._pendingGeneration = null;
178
+ this._syncProfile();
179
+ if (generation !== this._generation) { resolve(failure()); this._onChange(); return; }
180
+ // Failed or unreadable output cannot acquire a fresh checked time.
181
+ const limits = err ? [] : readUsage(stdout, this._now(), localZone());
182
+ if (!limits.length) { this._missed(); resolve(failure()); return; }
183
+ this._last = { limits, at: this._now(), zone: localZone(), profile };
184
+ resolve({ ok: true, reading: this.peek() });
185
+ // Even unchanged percentages now have a newly checked time to draw.
186
+ this._onChange();
187
+ };
188
+ const env = { ...(this._who().env ?? process.env) };
189
+ for (const key of Object.keys(env)) {
190
+ if (/^ANTHROPIC_|^CLAUDE_CODE_/.test(key) || ['CLAUDECODE', 'CLAUDE_PID', 'CLAUDE_EFFORT', 'CLAUDE_CONFIG_DIR'].includes(key)) delete env[key];
191
+ }
192
+ if (profile !== 'default') env.CLAUDE_CONFIG_DIR = profile;
193
+ try { this._run(
127
194
  bin,
128
195
  ['-p', '/usage', '--output-format', 'text'],
129
196
  // `cwd` is the whole fix for the permission panels; see usageCwd above.
130
- { cwd: this._where(), timeout: TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, encoding: 'utf8' },
131
- (err, stdout) => {
132
- this._busy = false;
133
- // A COMMAND THAT FAILED LEAVES THE OLD READING ALONE. It is stale rather
134
- // than wrong, and a corner that empties itself every time the network
135
- // hiccups is worse than one that is five minutes behind.
136
- if (err && !stdout) { this._missed(); return; }
137
- const limits = readUsage(stdout, this._now(), localZone());
138
- if (!limits.length) { this._missed(); return; }
139
- const before = JSON.stringify(this._last?.limits ?? null);
140
- this._last = { limits, at: this._now(), zone: localZone() };
141
- if (JSON.stringify(limits) !== before) this._onChange();
142
- },
143
- );
197
+ { cwd: this._where(), env, timeout: TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, encoding: 'utf8' },
198
+ finish,
199
+ ); } catch (err) { finish(err, ''); }
200
+ return pending;
144
201
  }
145
202
 
146
203
  _missed() {
@@ -1,278 +1,81 @@
1
1
  // Where Codex actually is, and whether we are entitled to say it is not here.
2
2
  //
3
- // The sibling of main/claude-bin.mjs, and it keeps that file's rule rather than
4
- // re-arguing it.
3
+ // THIS FILE IS NOW THE NAMES AND NOTHING ELSE. The search, the registry entry
4
+ // that declares Codex, and every measurement behind its paths live in
5
+ // main/engine-bin.mjs, which does the same job for every coding agent the app
6
+ // can run. Until 2026-10-08 this was 279 lines beside main/claude-bin.mjs's 338,
7
+ // and the two were copies of one idea: three of the seven commits they ever had
8
+ // are literally "one more install location".
5
9
  //
6
- // So `found` decides which binary would be spawned and `certain` decides
7
- // whether a screen may SAY there is nothing here, and they are never the same
8
- // value.
10
+ // WHAT IS KEPT HERE is the vocabulary the rest of the app already imports --
11
+ // main/config.mjs, main/settings.mjs and main/codex-launch-env.mjs, which builds
12
+ // the PATH a Codex opened from the desktop needs out of `candidatePaths` and
13
+ // `versionedPaths`.
14
+ //
15
+ // `found` decides which binary would be spawned and `certain` decides whether a
16
+ // screen may SAY there is nothing here, and they are never the same value.
9
17
  //
10
18
  // ONE DIFFERENCE FROM CLAUDE CODE, AND IT CHANGES THE SCREEN. Claude Code is
11
- // required: the app will not open an inbox without it. Codex is optional, and
12
- // an absence is the ordinary case rather than a fault. So nothing here ever
13
- // produces a warning. All a missing Codex does is leave the second engine out
14
- // of the map that shared/engines.mjs reads, which is silence rather than a
15
- // sentence about software she never asked for.
19
+ // required: the app will not open an inbox without it. Codex is optional, and an
20
+ // absence is the ordinary case rather than a fault. So nothing here ever produces
21
+ // a warning. All a missing Codex does is leave the second engine out of the map
22
+ // that shared/engines.mjs reads, which is silence rather than a sentence about
23
+ // software nobody asked for.
16
24
  //
17
25
  // NOTHING HERE SPAWNS ANYTHING, AND main/config.mjs IS WHAT CALLS IT.
26
+ // `resolveCodexBin` is what answers "is there a Codex on this machine", and its
27
+ // answer is the fact `engineFor` refuses on. Before it was wired in, a `codexBin`
28
+ // in zero.config.json arrived on `config` through the unknown-key spread and went
29
+ // to `spawn` with no finder and no existence check behind it.
18
30
  //
19
- // It was inert when it was written -- "no config key, no snapshot field, no
20
- // Settings row" -- and that stopped being true the moment the supervisor
21
- // started spawning `config.codexBin`. Until this was wired in, that path
22
- // arrived on `config` through the unknown-key spread in main/config.mjs and
23
- // went to `spawn` with no finder and no existence check behind it, which is the
24
- // one path in the second engine that becomes a process and was the only one
25
- // with no guard on it. `resolveCodexBin` is now what answers "is there a Codex
26
- // on this Mac", and its answer is the fact `engineFor` refuses on.
27
- //
28
- // There is still no Settings row, which is a real gap and not a claim of one:
29
- // she can set `codexBin` in zero.config.json and nothing on screen says whether
30
- // it was found.
31
- //
32
- // ------------------------------------------------------------------------
33
- // MEASURED ON THIS MAC 2026-09-04, BECAUSE THE 2026-08-25 ORIGINAL WAS WRITTEN
34
- // AGAINST codex-cli 0.144.0 AND SIX OF ITS ASSUMPTIONS HAVE MOVED SINCE.
35
- //
36
- // Installed here: codex-cli 0.148.0 at /opt/homebrew/bin/codex. That is a
37
- // Homebrew CASK, not a formula as the original said, and the bin entry is a
38
- // SYMLINK into /opt/homebrew/Caskroom/codex/0.148.0/bin/codex. `brew info
39
- // --cask codex` the same minute reports 0.148.0 installed against 0.152.1
40
- // available, so the moment mid-upgrade where that link points at nothing is a
41
- // live state on this machine and not a hypothetical. It is the same shape the
42
- // dangling-symlink rule was written for in the sibling file.
43
- //
44
- // THE INSTALL METHOD THE DOCS NOW LEAD WITH DID NOT EXIST IN THE ORIGINAL.
45
- // `https://chatgpt.com/codex/install.sh`, read 2026-09-04:
46
- //
47
- // BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}"
48
- // CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}"
49
- // STANDALONE_ROOT="$CODEX_HOME_DIR/packages/standalone"
50
- //
51
- // It links ~/.local/bin/codex at a versioned file under that root and swaps the
52
- // file on every update. Three things follow, and all three are changes from the
53
- // original: that path goes FIRST (the tool's own answer settles a machine set
54
- // up the documented way in one stat call, exactly as ~/.local/bin/claude does
55
- // next door), the standalone store's own bin is worth listing for the case
56
- // where CODEX_INSTALL_DIR put the link somewhere no list covers, and CODEX_HOME
57
- // can move every trace below out from under us.
58
- //
59
- // AND THE STORE KEEPS THE BINARY IN TWO PLACES, WHICH THE FIRST VERSION OF THIS
60
- // FILE MISSED. Same script, read the same day:
61
- //
62
- // line 951: ln -sf "bin/codex" "$stage_release/codex"
63
- // line 1021: version_from_binary "$release_dir/bin/codex" \
64
- // || version_from_binary "$release_dir/codex"
65
- //
66
- // Every staged release carries the binary at `<release>/bin/codex` AND at
67
- // `<release>/codex`, and the installer accepts either when it goes looking for
68
- // a version. `current` is a symlink at the live release, so BOTH
69
- // `current/bin/codex` and `current/codex` resolve on a machine set up the
70
- // documented way. Each is one stat call, so listing both costs nothing, and
71
- // listing only one is how the app tells somebody their Codex is missing while
72
- // they are looking at it.
73
- //
74
- // WHAT ~/.codex REALLY HOLDS NOW: 38 entries. auth.json, config.toml, sessions/
75
- // and history.jsonl are all still there, so the original's evidence list is not
76
- // wrong. It is INCOMPLETE, and it is aging in a specific way. The directory
77
- // also holds state_5.sqlite, thread_history_1.sqlite, logs_2.sqlite,
78
- // memories_1.sqlite, queue_1.sqlite, goals_1.sqlite, models_cache.json,
79
- // installation_id and shell_snapshots/, and the split matters: sessions/ holds
80
- // 1691 rollout files with the newest written 2026-09-03, while history.jsonl
81
- // has not been touched since 2026-06-03. The live record moved into sqlite
82
- // files whose names carry a schema number that increments, which is a name no
83
- // list can hold. That is why the directory itself stays on the list, last and
84
- // broad, and why nothing below tries to enumerate what is inside it.
85
- //
86
- // AND THE LINK MOVED. The original's install URL, developers.openai.com/codex/
87
- // cli, answers 308 to learn.chatgpt.com/docs/codex/cli.
88
- //
89
- // The npm package is unchanged and still real (@openai/codex, 0.153.0 on
90
- // 2026-09-04), so the package-manager paths and the per-Node directories below
91
- // are carried over exactly as they were.
31
+ // AND THERE IS A SETTINGS ROW NOW (w-ce8c1a9275): somebody whose Codex the
32
+ // search misses can point the app at the binary themselves, which is written
33
+ // back as `codexBin` and wins over everything below.
92
34
 
93
- import fs from 'node:fs';
94
35
  import os from 'node:os';
95
- import path from 'node:path';
96
- import { askShell } from './claude-bin.mjs';
36
+ import {
37
+ ENGINES, findEngine, resolveEngine, forgetEngine, installEvidence as evidenceFor,
38
+ versionedBinPaths,
39
+ } from './engine-bin.mjs';
97
40
 
98
- // In order, and the first that exists wins. The shell installer's own path is
99
- // first for the same reason its sibling's is: it is the answer the tool itself
100
- // produces. Everything after it is a package manager's default bin directory,
101
- // because Codex ships as a Homebrew cask and as an npm package rather than with
102
- // an installer that owns a folder.
103
- export function candidatePaths(home = os.homedir()) {
104
- return [
105
- path.join(home, '.local/bin/codex'), // the shell installer's link
106
- path.join(home, '.codex/packages/standalone/current/bin/codex'),
107
- path.join(home, '.codex/packages/standalone/current/codex'),
108
- '/opt/homebrew/bin/codex', // the Homebrew cask, Apple Silicon
109
- '/usr/local/bin/codex', // Homebrew on Intel, and npm -g
110
- path.join(home, '.bun/bin/codex'),
111
- path.join(home, '.volta/bin/codex'),
112
- path.join(home, '.asdf/shims/codex'),
113
- path.join(home, '.npm-global/bin/codex'), // the two npm prefixes people
114
- path.join(home, '.npm-packages/bin/codex'), // actually write in a guide
115
- path.join(home, '.yarn/bin/codex'),
116
- path.join(home, 'Library/pnpm/codex'), // pnpm's own bin on macOS
117
- path.join(home, '.deno/bin/codex'),
118
- path.join(home, 'bin/codex'), // a hand-rolled ~/bin on PATH
119
- '/opt/local/bin/codex', // MacPorts
120
- '/usr/bin/codex',
121
- ];
122
- }
41
+ const CODEX = ENGINES.codex;
123
42
 
124
- // One bin directory per installed Node, under a name no list can hold. A global
125
- // npm install under nvm, fnm, n or asdf lands there and nowhere else, and a
126
- // double-clicked Electron app inherits launchd's PATH, which has never heard of
127
- // any of them.
128
- export function versionedPaths(home = os.homedir(), readdir = defaultReaddir) {
129
- const under = (dir, tail) => readdir(path.join(home, dir)).map((v) => path.join(home, dir, v, tail));
130
- return [
131
- ...under('.nvm/versions/node', 'bin/codex'),
132
- ...under('.local/share/fnm/node-versions', 'installation/bin/codex'),
133
- ...under('Library/Application Support/fnm/node-versions', 'installation/bin/codex'),
134
- ...under('n/versions/node', 'bin/codex'),
135
- ...under('.asdf/installs/nodejs', 'bin/codex'),
136
- ];
43
+ export const INSTALL_URL = CODEX.installUrl;
44
+
45
+ /** Every normal home for this binary, in order; the first that exists wins. */
46
+ export function candidatePaths(home = os.homedir()) {
47
+ return CODEX.candidates(home, () => []);
137
48
  }
138
49
 
139
- function defaultReaddir(dir) {
140
- try { return fs.readdirSync(dir); } catch { return []; }
50
+ /** One bin directory per installed Node, read rather than guessed. */
51
+ export function versionedPaths(home = os.homedir(), readdir = undefined) {
52
+ return versionedBinPaths('codex', home, readdir);
141
53
  }
142
54
 
143
- // THE COPY THE CHATGPT APP CARRIES INSIDE ITSELF. Somebody who has only ever
144
- // used Codex in the ChatGPT app has this and nothing else (w-9f6975906c,
145
- // measured 2026-10-05: codex-cli 0.158.0-alpha.2, sharing the CLI's sign-in).
146
- // Searched LAST, after the shell, because it moves with the app's releases.
147
- //
148
- // AND THE ONE THE CODEX DESKTOP APP CARRIES (w-d5d632e503). Its reported place,
149
- // not measured on a Mac here: none of the Macs this was written on had the app.
150
- // A path that is not there costs one missed `exists`.
55
+ /** The copies the ChatGPT and Codex desktop apps carry inside themselves. */
151
56
  export function appCopyPaths(home = os.homedir()) {
152
- const inside = ['ChatGPT.app/Contents/Resources/codex-cli/bin/codex', 'Codex.app/Contents/Resources/codex'];
153
- return inside.flatMap((p) => [path.join('/Applications', p), path.join(home, 'Applications', p)]);
57
+ return CODEX.appCopies(home);
154
58
  }
155
59
 
156
- // WHAT CODEX LEAVES BEHIND. None of these is a binary and none is ever spawned.
157
- // They exist only to stop the app claiming an absence it has not established.
158
- //
159
- // Both homes are looked in when CODEX_HOME is set, and that is on purpose: a
160
- // CODEX_HOME inherited from somewhere else is not proof the ordinary directory
161
- // is gone, and the whole job of this list is to make an absence hard to claim
162
- // rather than easy.
60
+ /** What Codex leaves behind. None of it is ever spawned. */
163
61
  export function evidencePaths(home = os.homedir(), env = process.env) {
164
- const homes = [env?.CODEX_HOME, path.join(home, '.codex')].filter(Boolean);
165
- const out = [];
166
- for (const dir of new Set(homes)) {
167
- out.push(
168
- path.join(dir, 'auth.json'), // it has been logged in
169
- path.join(dir, 'config.toml'),
170
- path.join(dir, 'sessions'), // one folder tree of past runs
171
- path.join(dir, 'history.jsonl'),
172
- path.join(dir, 'packages/standalone/releases'), // the installer's version store
173
- dir, // and the directory itself, last
174
- );
175
- }
176
- return out;
62
+ return CODEX.evidence(home, env);
177
63
  }
178
64
 
179
- // A path that is a symlink to nothing. `existsSync` follows the link and says
180
- // false, which is indistinguishable from nothing being there at all, and the
181
- // two mean opposite things. Both of Codex's normal installs are a symlink into
182
- // a versioned directory that gets swapped on every update, so this is the
183
- // ordinary shape of a Codex that is being upgraded rather than an odd one.
184
- function defaultDangles(p) {
185
- try { return !!fs.lstatSync(p) && !fs.existsSync(p); } catch { return false; }
65
+ export function installEvidence({ home = os.homedir(), env = process.env, exists, dangles } = {}) {
66
+ return evidenceFor('codex', { home, env, exists, dangles });
186
67
  }
187
68
 
188
- export function installEvidence({
189
- home = os.homedir(), env = process.env, exists = fs.existsSync, dangles = defaultDangles,
190
- } = {}) {
191
- for (const p of evidencePaths(home, env)) if (exists(p)) return p;
192
- for (const p of candidatePaths(home)) if (dangles(p)) return p;
193
- return null;
69
+ /** The whole search. See main/engine-bin.mjs for what each answer means. */
70
+ export function findCodexBin(where = {}) {
71
+ return findEngine('codex', where);
194
72
  }
195
73
 
196
- export const INSTALL_URL = 'https://learn.chatgpt.com/docs/codex/cli';
197
-
198
- /**
199
- * The whole search, a pure function of its inputs so a test can put Codex
200
- * anywhere without touching the machine it runs on.
201
- *
202
- * Returns the same shape its sibling does: { path, found, certain, from,
203
- * searched, evidence }, so a caller can hold the two side by side without
204
- * learning two vocabularies.
205
- */
206
- export function findCodexBin({
207
- configured = null,
208
- home = os.homedir(),
209
- env = process.env,
210
- exists = fs.existsSync,
211
- shellLookup = () => askShell({ bin: 'codex' }),
212
- readdir = defaultReaddir,
213
- dangles = defaultDangles,
214
- } = {}) {
215
- if (configured) {
216
- return {
217
- path: configured, found: exists(configured), certain: true, from: 'settings',
218
- searched: [], evidence: null,
219
- };
220
- }
221
-
222
- const searched = [...candidatePaths(home), ...versionedPaths(home, readdir)];
223
- for (const candidate of searched) {
224
- if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'disk', searched, evidence: null };
225
- }
226
-
227
- const fromShell = shellSaid(shellLookup());
228
- if (fromShell.path) return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
229
-
230
- for (const candidate of appCopyPaths(home)) {
231
- if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'app', searched, evidence: null };
232
- }
233
-
234
- // NOT FOUND, AND WHETHER THAT IS KNOWN. Every cheap path missed and every
235
- // shell we could reach missed, so the last question is whether this Mac shows
236
- // any sign of Codex anyway.
237
- const evidence = installEvidence({ home, env, exists, dangles });
238
- return {
239
- path: null,
240
- found: false,
241
- certain: fromShell.answered && !evidence,
242
- from: null,
243
- searched,
244
- evidence,
245
- };
246
- }
247
-
248
- // askShell used to answer with a path or null. Anything handing back the old
249
- // shape is read as an answer, because that is what it always meant.
250
- function shellSaid(result) {
251
- if (typeof result === 'string') return { path: result, answered: true };
252
- if (!result || typeof result !== 'object') return { path: null, answered: true };
253
- return { path: result.path ?? null, answered: result.answered !== false };
254
- }
255
-
256
- /* ------------------------------- the cache -------------------------------- */
257
- // Only the SHELL step is remembered, for the same thirty seconds and the same
258
- // reason: the path checks are stat calls, and caching those is how a settings
259
- // screen ends up still saying "not found" after somebody has installed it.
260
-
261
- const SHELL_TTL = 30_000;
262
- let shellAnswer = null;
263
- let shellAskedAt = 0;
264
-
265
- export function resolveCodexBin(configured = null, { now = Date.now(), shellLookup = () => askShell({ bin: 'codex' }), ...where } = {}) {
266
- const remembered = () => {
267
- if (shellAskedAt && now - shellAskedAt < SHELL_TTL) return shellAnswer;
268
- shellAnswer = shellLookup();
269
- shellAskedAt = now;
270
- return shellAnswer;
271
- };
272
- return findCodexBin({ ...where, configured, shellLookup: remembered });
74
+ /** The same search with the shell step held for thirty seconds. */
75
+ export function resolveCodexBin(configured = null, where = {}) {
76
+ return resolveEngine('codex', configured, where);
273
77
  }
274
78
 
275
79
  export function forgetCodexBin() {
276
- shellAnswer = null;
277
- shellAskedAt = 0;
80
+ forgetEngine('codex');
278
81
  }
@@ -0,0 +1,31 @@
1
+ // An npm Codex executable starts with `#!/usr/bin/env node`. Finding that
2
+ // executable does not put its Node on a desktop app's launchd PATH.
3
+ // Resolve from disk, without running shell profiles or changing the app's env.
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { candidatePaths, versionedPaths } from './codex-bin.mjs';
8
+
9
+ function executable(file) {
10
+ try {
11
+ fs.accessSync(file, fs.constants.X_OK);
12
+ return fs.statSync(file).isFile();
13
+ } catch { return false; }
14
+ }
15
+
16
+ export function codexLaunchEnv(bin, source = process.env, {
17
+ home = source.HOME || os.homedir(), exists = executable, readdir,
18
+ } = {}) {
19
+ if (!bin) return { ...source };
20
+ const binDir = path.dirname(bin);
21
+ const inherited = (source.PATH || '').split(path.delimiter).filter(Boolean);
22
+ // Prefer the Node beside this Codex, then the user's existing PATH. A custom
23
+ // npm prefix can hold Codex while its Node lives under nvm, fnm, or asdf.
24
+ const directories = [...new Set([
25
+ binDir, ...inherited,
26
+ ...candidatePaths(home).map(path.dirname),
27
+ ...versionedPaths(home, readdir).map(path.dirname),
28
+ ])];
29
+ const nodeDir = directories.find(dir => exists(path.join(dir, 'node')));
30
+ return { ...source, PATH: [...new Set([binDir, ...(nodeDir ? [nodeDir] : []), ...inherited])].join(path.delimiter) };
31
+ }
@@ -519,9 +519,10 @@ export function createCodexWorker({
519
519
  // read loop, so anything the server packs in BEHIND the response is caught,
520
520
  // and a `ready` that somehow came in AHEAD of it would belong to no watched
521
521
  // thread and be dropped -- costing a healthy worker its run at the deadline.
522
- // The resume road has no such window, because `resumeThread` watches before
523
- // it sends. If a Codex release ever reorders that, this is the sentence that
524
- // says where to look.
522
+ // The resume road watches before sending, but an already-loaded thread does
523
+ // not repeat startup notifications. On 2026-10-07, 11 resumes of one loaded
524
+ // thread died at this deadline. Check that thread's current MCP connection
525
+ // on resume as well as listening for startup changes.
525
526
  let storeIsUp = !requireMcpServer;
526
527
  let storeTrouble = '';
527
528
  const waiting = [];
@@ -581,6 +582,39 @@ export function createCodexWorker({
581
582
  const timer = setTimeout(() => done(false), mcpReadyMs);
582
583
  timer?.unref?.();
583
584
  waiting.push(done);
585
+ if (resumeThreadId) {
586
+ const checkExistingStore = async () => {
587
+ let cursor;
588
+ const seen = new Set();
589
+ do {
590
+ if (ended || settled || storeIsUp) return;
591
+ const inventory = await server.request('mcpServerStatus/list', {
592
+ threadId: worker.threadId,
593
+ serverName: requireMcpServer,
594
+ detail: 'toolsAndAuthOnly',
595
+ ...(cursor ? { cursor } : {}),
596
+ });
597
+ if (ended || settled || storeIsUp) return;
598
+ const store = inventory?.data?.find(s => s.name === requireMcpServer);
599
+ // Newer Codex reports connection state. Older versions only offer
600
+ // discovered tools; a configured name alone is never proof.
601
+ if (store?.runtimeStatus === 'connected'
602
+ || (store && store.runtimeStatus == null && Object.keys(store.tools ?? {}).length > 0)) {
603
+ readStoreStatus({ name: requireMcpServer, status: 'ready' });
604
+ return;
605
+ }
606
+ if (store?.runtimeStatus) storeTrouble = `its connection is ${store.runtimeStatus}`;
607
+ cursor = inventory?.nextCursor;
608
+ if (cursor && seen.has(cursor)) throw new Error('store inventory repeated its pagination cursor');
609
+ if (cursor) seen.add(cursor);
610
+ } while (cursor);
611
+ };
612
+ // The existing deadline still owns the wait, even if this RPC hangs.
613
+ // A startup notification can also settle it while inventory is read.
614
+ checkExistingStore().catch(err => {
615
+ if (!ended && !settled && !storeIsUp) storeTrouble = err?.message ?? String(err);
616
+ });
617
+ }
584
618
  });
585
619
  };
586
620
 
@@ -595,6 +629,7 @@ export function createCodexWorker({
595
629
  const end = (code, signal = null, how = null) => {
596
630
  if (ended) return;
597
631
  ended = true;
632
+ for (const wake of waiting.splice(0)) wake(false);
598
633
  if (worker.threadId) { try { server.unwatch(worker.threadId); } catch { /* already gone */ } }
599
634
  worker.emit('exit', code, signal, how);
600
635
  };