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
@@ -1,3 +1,4 @@
1
+ import { harnessFor } from './harnesses.mjs';
1
2
  // SETTING UP SOMEBODY'S PLAN FOR THEM (w-9f6975906c, picked 2026-10-05).
2
3
  //
3
4
  // A person with a Claude or ChatGPT plan and no Claude Code or Codex used to
@@ -23,29 +24,18 @@
23
24
 
24
25
  import { spawn as nodeSpawn } from 'node:child_process';
25
26
  import { terminalEnv } from './task-terminals.mjs';
27
+ import { codexLaunchEnv } from './codex-launch-env.mjs';
26
28
 
27
- const INSTALLERS = {
28
- claude: { shell: '/bin/bash', url: 'https://claude.ai/install.sh', pipe: 'bash' },
29
- codex: { shell: '/bin/sh', url: 'https://chatgpt.com/codex/install.sh', pipe: 'sh' },
30
- };
31
-
32
- export function installCommand(engine) {
33
- const i = INSTALLERS[engine];
34
- return { file: i.shell, args: ['-c', `curl -fsSL ${i.url} | ${i.pipe}`] };
35
- }
29
+ export function installCommand(engine) { return harnessFor(engine).install(); }
36
30
 
37
31
  /** The plan, never the API console: `--claudeai` is Claude Code's own name for it. */
38
32
  export function signInCommand(engine, bin) {
39
- return engine === 'claude'
40
- ? { file: bin, args: ['auth', 'login', '--claudeai'] }
41
- : { file: bin, args: ['login'] };
33
+ return harnessFor(engine).signIn(bin);
42
34
  }
43
35
 
44
36
  /** Exit 0 is signed in and 1 is not, for both (measured 2026-10-05). */
45
37
  export function statusCommand(engine, bin) {
46
- return engine === 'claude'
47
- ? { file: bin, args: ['auth', 'status', '--json'] }
48
- : { file: bin, args: ['login', 'status'] };
38
+ return harnessFor(engine).signInStatus(bin);
49
39
  }
50
40
 
51
41
  const LOG_LIMIT = 64 * 1024;
@@ -57,7 +47,8 @@ function realSpawn(file, args, { env, onData, keepStdin = false } = {}) {
57
47
  let child;
58
48
  try {
59
49
  child = nodeSpawn(file, args, { env, stdio: [keepStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'] });
60
- } catch {
50
+ } catch (error) {
51
+ onData?.(`${error.message}\n`);
61
52
  return { exit: Promise.resolve(127), kill() {} };
62
53
  }
63
54
  const feed = (d) => onData?.(String(d));
@@ -65,7 +56,7 @@ function realSpawn(file, args, { env, onData, keepStdin = false } = {}) {
65
56
  child.stderr?.on('data', feed);
66
57
  const exit = new Promise((resolve) => {
67
58
  child.on('exit', (code) => resolve(code ?? 1));
68
- child.on('error', () => resolve(127));
59
+ child.on('error', (error) => { onData?.(`${error.message}\n`); resolve(127); });
69
60
  });
70
61
  return { exit, kill: () => { try { child.kill('SIGTERM'); } catch { /* already gone */ } } };
71
62
  }
@@ -82,6 +73,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
82
73
  */
83
74
  export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env = process.env, poll = POLL, onSignIn = () => {} } = {}) {
84
75
  const runEnv = terminalEnv(env);
76
+ const toolEnv = (engine, bin) => engine === 'codex' ? codexLaunchEnv(bin, runEnv) : runEnv;
85
77
  const jobs = new Map();
86
78
 
87
79
  const fresh = (engine) => ({ engine, phase: 'idle', error: null, log: '', running: null, login: null, run: 0 });
@@ -91,20 +83,26 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
91
83
 
92
84
  async function signedIn(j, bin) {
93
85
  const { file, args } = statusCommand(j.engine, bin);
94
- const p = spawn(file, args, { env: runEnv });
86
+ let output = '';
87
+ const p = spawn(file, args, { env: toolEnv(j.engine, bin), onData: (d) => { output = (output + String(d).replace(ANSI, '')).slice(-LOG_LIMIT); } });
95
88
  const TIMED_OUT = Symbol('no answer');
96
89
  const code = await Promise.race([p.exit, wait(STATUS_TIMEOUT).then(() => { p.kill(); return TIMED_OUT; })]);
97
90
  // TOLD TO THE APP, which routes on it (w-d5d632e503): a Claude Code that
98
91
  // is signed out beside a Codex that is signed in is a Mac that runs on
99
92
  // Codex. Only a real yes or no; a check that never answered says nothing.
100
93
  if (code === 0 || code === 1) onSignIn(j.engine, code === 0);
101
- return code === 0;
94
+ if (code === 0 || code === 1) return code === 0;
95
+ if (output) say(j, output);
96
+ const name = j.engine === 'codex' ? 'Codex' : 'Claude Code';
97
+ throw new Error(code === TIMED_OUT
98
+ ? `${name} did not answer the sign-in check. Try again.`
99
+ : `${name} could not check sign-in: ${lastLine(output) || `exit ${code}`}`);
102
100
  }
103
101
 
104
102
  function startLogin(j, bin) {
105
103
  j.login?.kill();
106
104
  const { file, args } = signInCommand(j.engine, bin);
107
- const login = spawn(file, args, { env: runEnv, keepStdin: true, onData: (d) => say(j, d) });
105
+ const login = spawn(file, args, { env: toolEnv(j.engine, bin), keepStdin: true, onData: (d) => say(j, d) });
108
106
  j.login = login;
109
107
  return login;
110
108
  }
@@ -130,18 +128,19 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
130
128
  if (!live()) return;
131
129
  j.phase = 'signing-in';
132
130
  let login = startLogin(j, hit.path);
133
- let gaveUp = false;
134
- login.exit.then((code) => { if (login === j.login && code !== 0) gaveUp = true; });
131
+ let ended = false;
132
+ const watchLogin = (loginProcess) => loginProcess.exit.then(() => { if (loginProcess === j.login) ended = true; });
133
+ watchLogin(login);
135
134
  for (;;) {
136
135
  await wait(poll);
137
136
  if (!live()) return;
138
137
  if (await signedIn(j, hit.path)) { if (live()) ready(j); return; }
139
138
  if (j.login !== login) {
140
139
  login = j.login;
141
- gaveUp = false;
142
- login.exit.then((code) => { if (login === j.login && code !== 0) gaveUp = true; });
140
+ ended = false;
141
+ watchLogin(login);
143
142
  }
144
- if (gaveUp) return fail(j, 'The sign-in was closed before it finished.');
143
+ if (ended) return fail(j, `The sign-in was closed before it finished.${lastLine(j.log) ? ` ${lastLine(j.log)}` : ''}`);
145
144
  }
146
145
  }
147
146
 
@@ -182,12 +181,13 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
182
181
  async readiness(engine) {
183
182
  const hit = find(engine);
184
183
  if (!hit?.found) return { engine, found: false, signedIn: false };
185
- return { engine, found: true, signedIn: await signedIn({ engine }, hit.path) };
184
+ try { return { engine, found: true, signedIn: await signedIn({ engine, log: '' }, hit.path) }; }
185
+ catch (error) { return { engine, found: true, signedIn: false, error: error.message }; }
186
186
  },
187
187
  /** The same question against a path already found, so asking it at
188
188
  * launch does not search the Mac again. */
189
189
  signedInNow(engine, bin) {
190
- return signedIn({ engine }, bin);
190
+ return signedIn({ engine, log: '' }, bin).catch(() => false);
191
191
  },
192
192
  };
193
193
  }
package/main/feedback.mjs CHANGED
@@ -5,10 +5,13 @@
5
5
  // to as a secret (`FEEDBACK_TO`, cloud/supabase/functions/feedback), so the
6
6
  // address is in no file of this public repository and no install can show it.
7
7
  //
8
- // WHERE IT GOES IS BAKED IN, like the analytics key (main/analytics.mjs): the
9
- // release writes `bakedFeedbackUrl` into the packaged package.json, a copy run
10
- // from source carries none, and a `feedbackUrl` in zero.config.json outranks
11
- // both. A copy with nowhere to send says so in words rather than pretending.
8
+ // WHERE IT GOES SHIPS IN THE CODE (w-58e758c81e, 2026-10-09). It used to be
9
+ // baked only into the packaged Mac app, so the npm package and a git clone,
10
+ // the two installs the README offers, told everyone who pressed Send that
11
+ // feedback was not set up, and lost what they wrote. FEEDBACK_ADDRESS is the
12
+ // server function, not an inbox. A `feedbackUrl` in zero.config.json, the
13
+ // FEEDBACK_URL variable, or a `bakedFeedbackUrl` in a packaged package.json
14
+ // still outrank it.
12
15
  //
13
16
  // It is not behind the diagnostics switch. That switch is about what Agentbox
14
17
  // sends on its own; this is something a person typed and pressed Send on.
@@ -16,11 +19,13 @@ import { createRequire } from 'node:module';
16
19
  import { checkFeedback } from '../shared/feedback.mjs';
17
20
  import { Name, readEnv } from '../shared/product-name.mjs';
18
21
 
22
+ export const FEEDBACK_ADDRESS = 'https://lywvthwkhkfzhqivvond.supabase.co/functions/v1/feedback';
23
+
19
24
  function bakedUrl() {
20
25
  try {
21
- return createRequire(import.meta.url)('../package.json')?.bakedFeedbackUrl ?? null;
26
+ return createRequire(import.meta.url)('../package.json')?.bakedFeedbackUrl ?? FEEDBACK_ADDRESS;
22
27
  } catch {
23
- return null;
28
+ return FEEDBACK_ADDRESS;
24
29
  }
25
30
  }
26
31
 
@@ -96,7 +96,7 @@ export function openFreshUser({
96
96
  const notes = [];
97
97
  if (withTools && !prepared.claudeBin) notes.push('Claude Code is not at ~/.local/bin/claude, so agents will not run in it.');
98
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 Mac, so it will have no agents to offer.');
99
+ if (withAgents && !prepared.agents) notes.push('There is no ~/.claude on this computer, so it will have no agents to offer.');
100
100
 
101
101
  return { ok: true, home: prepared.home, agents: prepared.agents, notes, pid: child?.pid ?? null };
102
102
  }
@@ -42,7 +42,10 @@ const MAX_ROWS_PER_FILE = 20_000;
42
42
  const NOT_CODE = /(^|\/)(node_modules|\.git)\//;
43
43
 
44
44
  function git(cwd, args, { maxBuffer = 64 * 1024 * 1024 } = {}) {
45
- return execFileSync('git', ['--no-pager', ...args], {
45
+ // A user's `diff.mnemonicPrefix` rewrites a/ and b/ into c/ i/ o/ w/.
46
+ // The reader below only knows one prefix. Turn the setting off for this
47
+ // process so a checkout reads the same on every machine.
48
+ return execFileSync('git', ['--no-pager', '-c', 'diff.mnemonicPrefix=false', ...args], {
46
49
  cwd,
47
50
  encoding: 'utf8',
48
51
  maxBuffer,
@@ -189,7 +192,10 @@ export function filesFromPatch(patch) {
189
192
  const nameOn = (line, side) => {
190
193
  const name = line.slice(4).trim();
191
194
  if (name === '/dev/null') return null;
192
- return name.replace(side === 'old' ? /^a\// : /^b\//, '');
195
+ // a/ and b/ are the ordinary prefixes. c/ i/ o/ w/ are mnemonicPrefix,
196
+ // and 1/ 2/ are `git diff --no-index` under that same setting. One
197
+ // prefix comes off; a file that is itself named `b/foo` stays `b/foo`.
198
+ return name.replace(/^[abiowc12]\//, '');
193
199
  };
194
200
  for (const line of String(patch ?? '').split('\n')) {
195
201
  if (line.startsWith('diff --git ')) { closeHunk(); file = null; oldName = null; continue; }
@@ -0,0 +1,208 @@
1
+ import { hookFailure } from '../../shared/hook-failure.mjs';
2
+ import { SAYING_CAP } from '../codex.mjs';
3
+
4
+ // The stream's own metadata, kept on the session object: the session id (what
5
+ // a personal reply resumes) and the final result message (what the founder
6
+ // reads). Recorded for every session because it is one parse we are already
7
+ // paying for; acted on where someone needs it.
8
+ export function captureStream(session, line) {
9
+ try {
10
+ const obj = JSON.parse(line);
11
+ if (obj.session_id) session.sessionId = obj.session_id;
12
+ if (obj.type === 'result') {
13
+ session.result = String(obj.result ?? '');
14
+ session.resultIsError = !!obj.is_error;
15
+ // A run cannot end with helpers still out. Cleared here as well as on
16
+ // exit so the number can never outlive the thing it counts. Unless
17
+ // something is still out in the background: then this was a turn
18
+ // ending, not the run, and the session stays open for it.
19
+ if (!session.child?.waitingOn?.().length) session.helperIds?.clear();
20
+ }
21
+ noteClaimAnswer(session, obj);
22
+ countHelpers(session, obj);
23
+ } catch {}
24
+ }
25
+
26
+ // Whether this run was refused its own row, off the claim call and its reply.
27
+ // `closingMessageIsTheAnswer` reads it: a refused worker is told to end saying
28
+ // nothing, and whatever it does say must not land on a row someone else holds.
29
+ // A later claim that succeeds clears it.
30
+ function noteClaimAnswer(session, obj) {
31
+ const blocks = Array.isArray(obj?.message?.content) ? obj.message.content : [];
32
+ for (const b of blocks) {
33
+ if (obj.type === 'assistant' && b?.type === 'tool_use' && /claim_work_item$/.test(b.name ?? '')
34
+ && (!b.input?.id || b.input.id === session.itemId)) {
35
+ (session.claimCalls ??= new Set()).add(b.id);
36
+ }
37
+ if (obj.type === 'user' && b?.type === 'tool_result' && session.claimCalls?.has(b.tool_use_id)) {
38
+ const text = typeof b.content === 'string' ? b.content
39
+ : (b.content ?? []).map((c) => c?.text ?? '').join('');
40
+ if (/"claimed"\s*:\s*false/.test(text)) session.claimRefused = true;
41
+ else if (/"claimed"\s*:\s*true/.test(text)) session.claimRefused = false;
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * `--include-partial-messages` puts `stream_event` frames on the same stdout
48
+ * the finished messages come down. This keeps the CURRENT text block on the
49
+ * session as it grows, and nothing else: no thinking, no tool arguments, and
50
+ * nothing written to disk. It dies with the process, exactly like `tail`.
51
+ *
52
+ * ONE BLOCK AT A TIME, AND THAT IS THE POINT. A finished message becomes ONE
53
+ * trace line per text block (`traceStreamLine`), so holding exactly one block
54
+ * here means the live string and the traced string are the same string, and
55
+ * the thread can drop the live copy the moment the traced one arrives without
56
+ * guessing (`itemThread`, renderer/src/item-thread.ts).
57
+ *
58
+ * IT IS NOT CLEARED WHEN THE MESSAGE FINISHES, and that is deliberate. The
59
+ * trace is written on one channel and read on another, so clearing it here
60
+ * would blink the sentence off her screen for however long the read takes and
61
+ * then put it back. The renderer suppresses it the moment the thread has it,
62
+ * which is a comparison rather than a race.
63
+ *
64
+ * Returns true when the string moved, which is what earns a push.
65
+ */
66
+ export function streamingText(session, line) {
67
+ try {
68
+ const obj = JSON.parse(line);
69
+ if (obj.type !== 'stream_event') return false;
70
+ const event = obj.event ?? {};
71
+ // A new message. Whatever the last one ended on is the last one's, and the
72
+ // trace has it by now.
73
+ if (event.type === 'message_start') {
74
+ session.saying = '';
75
+ session.sayingAt = 0;
76
+ return true;
77
+ }
78
+ if (event.type === 'content_block_start') {
79
+ // Only a text block resets it. A tool call or a thought starting means
80
+ // this message has moved on from prose, and the block already typed
81
+ // stays on screen until the trace delivers the same words.
82
+ if (event.content_block?.type !== 'text') return false;
83
+ session.saying = '';
84
+ session.sayingAt = Date.now();
85
+ return true;
86
+ }
87
+ if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
88
+ const piece = String(event.delta.text ?? '');
89
+ if (!piece) return false;
90
+ if (!session.sayingAt) session.sayingAt = Date.now();
91
+ const next = (session.saying ?? '') + piece;
92
+ // Past the cap this stops being a sentence she is reading. Dropped
93
+ // rather than cut, because a cut string never matches the traced one and
94
+ // would sit under it on her screen forever.
95
+ session.saying = next.length > SAYING_CAP ? '' : next;
96
+ return true;
97
+ }
98
+ return false;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ // HOW MANY HELPERS THIS SESSION HAS OUT RIGHT NOW.
105
+ //
106
+ // THE ONLY TWO EVENTS THAT MATTER, and they bracket a helper exactly. Measured
107
+ // on Claude Code 2.1.246 over a real four-helper run (the probe is written up
108
+ // in decisions.md, 08-26): `task_started` carries a fresh `task_id`, and
109
+ // `task_updated` carries a `patch.status` of `completed` for that same id. Four
110
+ // starts, four completions, no id seen twice and none left open. So a set of
111
+ // live ids, and its size is the number.
112
+ //
113
+ // A SET AND NOT A COUNTER, because a counter cannot survive a duplicate: the
114
+ // stream is read line by line off a pipe, and one repeated line would leave the
115
+ // page saying five helpers are working for the rest of the run with nothing
116
+ // able to correct it. Adding an id twice is free.
117
+ //
118
+ // NESTED HELPERS ARE COUNTED THE SAME. `spawn_depth` says how deep a helper
119
+ // sits, and this deliberately ignores it: a helper's helper is still a
120
+ // subagent working on her task.
121
+ //
122
+ // THE LEAD IS NOT IN THIS NUMBER AND NEVER HAS BEEN, which is what the line
123
+ // drawn off it was getting wrong until 2026-08-27. Only a `task_started` id is
124
+ // ever added, and the lead has no `task_started` of its own.It is, so
125
+ // `shortWord` in renderer/src/live-line.ts now says Subagents, unhyphenated on
126
+ // her call of 2026-08-27 and on a count of the spelling in the Claude Code
127
+ // build she runs. If this ever starts counting the lead as well, that word has
128
+ // to move with it. DID THIS RUN LEAVE HER ANYTHING TO READ?
129
+ //
130
+ // The rule that divides the two writers, kept in one named place because
131
+ // getting it wrong is silence, and silence is the failure that matters most:
132
+ //
133
+ // - said nothing at all -> the supervisor speaks for it (sayTheRunDied)
134
+ // - said something wrong -> the supervisor speaks for it, in OUR words
135
+ // - said something real -> it speaks for itself, untouched
136
+ //
137
+ // The middle line is the one that was missing. Both writers used to exclude an
138
+ // error result: `speaksForTheSession` because raw CLI text must never reach her
139
+ // row, and this one because it only looked for a result that was absent. Each
140
+ // was right on its own and together they left the case that matters most with
141
+ // no writer at all.
142
+ export function saidNothingSheCanUse(session) {
143
+ return session?.result == null || !!session.resultIsError;
144
+ }
145
+
146
+ export function countHelpers(session, obj) {
147
+ if (obj?.type !== 'system') return;
148
+ if (obj.subtype === 'task_started' && obj.task_id) {
149
+ (session.helperIds ??= new Set()).add(obj.task_id);
150
+ } else if (obj.subtype === 'task_updated' && obj.task_id) {
151
+ // Anything that is not still running ends it. The measured value is
152
+ // `completed`, and a helper that fails or is cancelled has equally stopped
153
+ // working, so the test is what it is NOT rather than a list of endings we
154
+ // would have to keep in step with the CLI.
155
+ const status = obj.patch?.status;
156
+ if (status && status !== 'in_progress' && status !== 'running') session.helperIds?.delete(obj.task_id);
157
+ }
158
+ }
159
+
160
+ // The persisted trace line: fuller than the tail. Tool calls carry their key
161
+ // input (the file written, the command run), because "tool: Bash" answers
162
+ // nothing when the founder asks what a session actually did.
163
+ export function traceStreamLine(line) {
164
+ try {
165
+ const obj = JSON.parse(line);
166
+ const at = new Date().toISOString().slice(11, 19);
167
+ const hook = hookFailure(obj);
168
+ if (hook) return `${at} ${hook}`;
169
+ if (obj.type === 'assistant' && obj.message?.content) {
170
+ const parts = [];
171
+ for (const c of obj.message.content) {
172
+ if (c.type === 'text' && c.text?.trim()) parts.push(c.text.trim());
173
+ if (c.type === 'tool_use') {
174
+ const input = c.input ?? {};
175
+ const hint = input.file_path ?? input.path ?? input.command ?? input.title ?? input.description ?? input.id ?? '';
176
+ parts.push(`[${c.name}] ${String(hint).slice(0, 200)}`);
177
+ }
178
+ }
179
+ return parts.length ? parts.map((p) => `${at} ${p}`).join('\n') : null;
180
+ }
181
+ if (obj.type === 'result') {
182
+ const flavor = `${obj.subtype ?? 'ok'}${obj.is_error ? ' ERROR' : ''} · ${obj.num_turns ?? '?'} turns`;
183
+ return `\n${at} == RESULT (${flavor}) ==\n${String(obj.result ?? '').slice(0, 4000)}`;
184
+ }
185
+ return null;
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+
191
+ // Stream-json lines are verbose; the In Progress tail wants the gist.
192
+ export function summarizeStreamLine(line) {
193
+ try {
194
+ const obj = JSON.parse(line);
195
+ const hook = hookFailure(obj);
196
+ if (hook) return hook;
197
+ if (obj.type === 'assistant' && obj.message?.content) {
198
+ const text = obj.message.content.filter((c) => c.type === 'text').map((c) => c.text).join(' ');
199
+ const tools = obj.message.content.filter((c) => c.type === 'tool_use').map((c) => c.name);
200
+ if (tools.length) return `tool: ${tools.join(', ')}`;
201
+ if (text) return text.slice(0, 300);
202
+ }
203
+ if (obj.type === 'result') return `done: ${String(obj.result ?? '').slice(0, 200)}`;
204
+ return null;
205
+ } catch {
206
+ return line.slice(0, 200);
207
+ }
208
+ }
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { harnessDefinition } from '../../shared/harness-definitions.mjs';
6
+ import { EFFORT_LEVELS } from '../../shared/effort-levels.mjs';
7
+ import { storeRootEnv } from '../store/home.mjs';
8
+ import { resolveClaudeBin } from '../claude-bin.mjs';
9
+ import { claudeModelRows } from '../claude-models.mjs';
10
+ import { linkAccountTooling } from '../account-tooling.mjs';
11
+ import { streamInputArgs, attachClaudeInput } from '../claude-input.mjs';
12
+ import { claudeLaunchEnv } from '../claude-launch-env.mjs';
13
+ import { claudeActivity } from '../agent-activity.mjs';
14
+ import { captureStream, streamingText, summarizeStreamLine, traceStreamLine } from './claude-stream.mjs';
15
+ import { definitionMethods, commonUpdatePlan } from './common.mjs';
16
+ const definition = harnessDefinition('claude');
17
+
18
+ export const claudeHarness = {
19
+ ...definitionMethods(definition),
20
+ discover: resolveClaudeBin,
21
+ launchEnv: claudeLaunchEnv,
22
+ models: ({ bin } = {}) => claudeModelRows({ bin: bin ?? null }).map(m => ({ id: m.alias, label: m.label })),
23
+ defaultModel: () => null,
24
+ workspaceModel: () => null,
25
+ profileHome: () => null,
26
+ effortLevels: () => EFFORT_LEVELS.map(e => e.id),
27
+ profiles: sup => sup._profiles(),
28
+ signedIn: () => true,
29
+ profileEnv: (env, profile) => profile === 'default' ? env : { ...env, [definition.homeEnv]: profile },
30
+ signInFiles({ folder, home }) {
31
+ if (folder) return definition.credentialFiles.map(file => path.join(folder, file));
32
+ const dir = path.join(home, '.claude');
33
+ return [path.join(home, '.claude.json'), ...definition.credentialFiles.map(file => path.join(dir, file))];
34
+ },
35
+ tooling: profile => linkAccountTooling(profile),
36
+ spawn(sup, plan, { cwd, item, profile, env }) {
37
+ // The binary is the one this definition names, not a field spelled out
38
+ // here: that is the last place an engine of this family needed code.
39
+ return spawn(sup.config[definition.binKey], streamInputArgs(definition.run, plan.args), {
40
+ cwd,
41
+ env: { ...claudeLaunchEnv(sup.config[definition.binKey], env), STORE_ACCOUNT_ID: sup.config.accountId, ...storeRootEnv(sup.config.storeRoot),
42
+ ...(profile !== 'default' ? { [definition.homeEnv]: profile } : {}),
43
+ ZERO_PRODUCT: item.product, ZERO_ITEM: item.id, ...sup.memoryGateEnv(item) },
44
+ stdio: ['pipe', 'pipe', 'pipe'],
45
+ });
46
+ },
47
+ readers: () => ({ capture: captureStream, stream: streamingText, summarize: summarizeStreamLine, trace: traceStreamLine, activity: claudeActivity }),
48
+ subscribe(child, _session, absorb) {
49
+ let buffer = '';
50
+ child.stdout.on('data', data => {
51
+ buffer += data.toString();
52
+ let index;
53
+ while ((index = buffer.indexOf('\n')) >= 0) {
54
+ const line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
55
+ if (line.trim()) absorb(line);
56
+ }
57
+ });
58
+ },
59
+ attachInput: child => attachClaudeInput(child),
60
+ transcript(sup, rec) {
61
+ if (!rec?.sessionId) return null;
62
+ 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;
72
+ },
73
+ change: () => null,
74
+ release: () => {},
75
+ memoryGate: (sup, item) => sup.memoryGateEnv(item),
76
+ smallModelArgs: (prompt, model) => ['-p', prompt, '--model', model],
77
+ updateSettings: ({ home }) => {
78
+ try { return JSON.parse(fs.readFileSync(path.join(process.env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'), 'settings.json'), 'utf8')); }
79
+ catch { return {}; }
80
+ },
81
+ updatePlan: options => commonUpdatePlan(definition, options, ({ bin, real, home, channel, name }) =>
82
+ real.startsWith(path.join(home, '.local/share/claude/versions') + '/')
83
+ ? { name, file: bin, args: ['update'], feed: `https://downloads.claude.ai/claude-code-releases/${channel}`, format: 'text' } : null),
84
+ };
@@ -0,0 +1,65 @@
1
+ import path from 'node:path';
2
+ import { harnessDefinition } from '../../shared/harness-definitions.mjs';
3
+ import { resolveCodexBin } from '../codex-bin.mjs';
4
+ import { codexLaunchEnv } from '../codex-launch-env.mjs';
5
+ import { codexModels, codexDefaultModel, codexModelLevels } from '../codex-models.mjs';
6
+ import { codexTranscriptFile, workerThreadParams } from '../codex-session.mjs';
7
+ import { compactCodexThread } from '../codex-compaction.mjs';
8
+ import { captureCodexEvent, codexStreamingText, summarizeCodexEvent, traceCodexEvent, rememberCodexChange, changeFromCodexTurn } from '../codex.mjs';
9
+ import { codexActivity } from '../agent-activity.mjs';
10
+ import { runCodexCommand } from '../provider-commands.mjs';
11
+ import { definitionMethods, commonUpdatePlan } from './common.mjs';
12
+ const definition = harnessDefinition('codex');
13
+
14
+ export const codexHarness = {
15
+ ...definitionMethods(definition),
16
+ discover: resolveCodexBin,
17
+ launchEnv: codexLaunchEnv,
18
+ models: (options = {}) => codexModels(options),
19
+ defaultModel: (options = {}) => codexDefaultModel(options),
20
+ workspaceModel: sup => sup._codexWorkspaceModel(),
21
+ profileHome: (sup, profile) => sup._codexProfileHome(profile),
22
+ effortLevels: (model, options = {}) => codexModelLevels(model, options),
23
+ profiles: sup => sup._codexProfiles(),
24
+ signedIn: (sup, profile) => sup._codexSignedIn(profile),
25
+ profileEnv: (env, profile, sup) => ({ ...env, [definition.homeEnv]: sup._codexProfileHome(profile) }),
26
+ tooling: () => null,
27
+ // Codex carries the person's standing instructions as a thread's
28
+ // `developerInstructions`, so it needs neither a flag nor a file. It says so
29
+ // rather than inheriting the refusal meant for an engine that has nowhere to
30
+ // put them (main/harnesses/instructions-file.mjs).
31
+ instructions: () => ({ by: 'thread' }),
32
+ spawn: (sup, plan, context) => sup._spawnCodexWorker(plan, context),
33
+ readers: () => ({ capture: captureCodexEvent, stream: codexStreamingText, summarize: summarizeCodexEvent, trace: traceCodexEvent, activity: codexActivity }),
34
+ subscribe(child, session, absorb) {
35
+ child.on('event', (method, params) => { rememberCodexChange(session, method, params); absorb(method, params); });
36
+ },
37
+ attachInput: () => {},
38
+ transcript: (sup, rec) => rec?.sessionId ? codexTranscriptFile(rec.sessionId, { home: sup._codexProfileHome(rec.profile) }) : null,
39
+ change: (session, options) => changeFromCodexTurn(session.codexChange ?? [], options),
40
+ release: (sup, profile) => sup._releaseIdleCodex(profile),
41
+ usage: sup => sup.codexUsage(),
42
+ async compact(sup, rec, cwd) {
43
+ const { client, handshake } = sup._codexServer(sup._codexProfileHome(rec.profile ?? 'default'));
44
+ const mcpServers = await sup._codexIsolation(client, handshake);
45
+ const params = workerThreadParams({ cwd, mcpServers, storeServer: null });
46
+ return compactCodexThread({ server: client, threadId: rec.sessionId, threadParams: params });
47
+ },
48
+ settleApproval: (sup, ...args) => sup.settleCodexApproval(...args),
49
+ approvalChanged: (sup, ...args) => sup.codexCardChanged(...args),
50
+ memoryGate: sup => sup.codexMemoryGateEnv(),
51
+ async runCommand(sup, { home, ...operation }) {
52
+ const { client, handshake } = sup._codexServer(home);
53
+ await handshake;
54
+ return runCodexCommand({ server: client, ...operation });
55
+ },
56
+ updateSettings: () => ({}),
57
+ updatePlan: options => commonUpdatePlan(definition, options, ({ bin, real, name }) => {
58
+ const standalone = real.match(/^(.*)\/packages\/standalone\/(?:releases|current)\//);
59
+ return standalone ? { name, file: '/bin/bash',
60
+ args: ['-o', 'pipefail', '-c', '/usr/bin/curl -fsSL --connect-timeout 10 --max-time 60 https://chatgpt.com/codex/install.sh | /bin/sh'],
61
+ env: { CODEX_HOME: standalone[1], CODEX_INSTALL_DIR: path.dirname(bin) },
62
+ feed: 'https://releases.openai.com/codex/channels/latest', format: 'codex' } : null;
63
+ }),
64
+ smallModelArgs: prompt => ['exec', '--skip-git-repo-check', '--ephemeral', '--ignore-user-config', '--ignore-rules', '-s', 'read-only', '-c', 'model_reasoning_effort=low', prompt],
65
+ };
@@ -0,0 +1,78 @@
1
+ import path from 'node:path';
2
+ import { instructionsFor } from './instructions-file.mjs';
3
+
4
+ /**
5
+ * THE COMMAND LINE THAT STARTS A WORKER, BUILT FROM THE DEFINITION'S `run`.
6
+ *
7
+ * Every flag name here used to be a literal in main/supervisor.mjs, so the
8
+ * supervisor knew one engine's CLI by heart and a second one of the same family
9
+ * could not be added without editing it. The order is the order that shipped,
10
+ * and `tests/a-second-stream-json-engine-is-only-a-definition` holds it there,
11
+ * because flag order is the kind of thing a refactor silently rewrites.
12
+ *
13
+ * ABSENT MEANS CANNOT, not "default". An engine whose `run` has no `effort` is
14
+ * handed no effort word even when the task names one, which is how a harness
15
+ * says it lacks a capability without the call site asking who it is. `fork` is
16
+ * the one pair: a fork is a fork OF something, so it needs a resume as well.
17
+ */
18
+ export function workerArgsFrom(run, { prompt, grants = [], effort = null, system = null, resumeId = null, fork = false } = {}) {
19
+ if (!run) return [];
20
+ const args = [];
21
+ if (run.print) args.push(run.print, prompt);
22
+ if (run.outputFormat) args.push(run.outputFormat.flag, run.outputFormat.value);
23
+ if (run.stream) args.push(...run.stream);
24
+ args.push(...grants);
25
+ if (run.effort && effort) args.push(run.effort, effort);
26
+ if (run.system && system) args.push(run.system, system);
27
+ if (run.resume && resumeId) args.push(run.resume, resumeId);
28
+ if (run.fork && fork && resumeId) args.push(run.fork);
29
+ return args;
30
+ }
31
+
32
+ export function definitionMethods(definition) {
33
+ const scrub = definition.scrubPatterns.map(pattern => new RegExp(pattern));
34
+ return {
35
+ ...definition,
36
+ binary: config => config?.[definition.binKey] ?? null,
37
+ workerArgs: options => workerArgsFrom(definition.run, options),
38
+ // WHERE THE PERSON'S STANDING INSTRUCTIONS GO on this engine. A flag for
39
+ // the one that has one, a file named in the definition for the ones that
40
+ // do not, and a refusal for an engine with neither — because that engine
41
+ // would answer against rules it was never given, and nothing on screen
42
+ // would say so.
43
+ instructions: context => instructionsFor(definition, context),
44
+ launchEnv: (_bin, env) => ({ ...env }),
45
+ install: () => ({ file: definition.installer.shell, args: ['-c', `curl -fsSL ${definition.installer.url} | ${definition.installer.pipe}`] }),
46
+ signIn: bin => ({ file: bin, args: [...definition.loginArgs] }),
47
+ signInStatus: bin => ({ file: bin, args: [...definition.statusArgs] }),
48
+ accountKey: profile => `${definition.accountPrefix}${profile}`,
49
+ workerEnv(input = process.env) {
50
+ const env = { ...input };
51
+ for (const key of Object.keys(env)) if (scrub.some(pattern => pattern.test(key))) delete env[key];
52
+ return env;
53
+ },
54
+ signInFiles: ({ folder }) => folder ? definition.credentialFiles.map(file => path.join(folder, file)) : [],
55
+ };
56
+ }
57
+
58
+ // Installation-specific work remains in each adapter. The common installers
59
+ // use the same prefix and cask checks for every registered tool.
60
+ export function commonUpdatePlan(definition, { bin, real, home, channel = 'latest' }, native) {
61
+ if (!path.isAbsolute(bin || '') || !real) return null;
62
+ const name = definition.label;
63
+ const brew = real.match(/^(\/opt\/homebrew|\/usr\/local)\/Caskroom\/([^/]+)\//);
64
+ if (brew && definition.casks.includes(brew[2])) return {
65
+ name, file: `${brew[1]}/bin/brew`, args: ['upgrade', '--cask', brew[2]],
66
+ feed: `https://formulae.brew.sh/api/cask/${brew[2]}.json`, format: 'brew',
67
+ };
68
+ const tail = `/lib/node_modules/${definition.package}/`;
69
+ const at = real.indexOf(tail);
70
+ if (at > 0) {
71
+ const prefix = real.slice(0, at);
72
+ return { name, file: path.join(prefix, 'bin/npm'),
73
+ args: ['install', '--global', '--prefix', prefix, `${definition.package}@${channel}`],
74
+ env: { PATH: `${prefix}/bin:${process.env.PATH || ''}` },
75
+ feed: `https://registry.npmjs.org/${definition.package}/${channel}`, format: 'npm' };
76
+ }
77
+ return native?.({ bin, real, home, channel, name }) ?? null;
78
+ }