agentbox-app 0.1.12 → 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 (214) hide show
  1. package/main/acp.mjs +296 -0
  2. package/main/agent-homes.mjs +158 -0
  3. package/main/agent-sessions.mjs +57 -20
  4. package/main/agent-updates.mjs +9 -22
  5. package/main/analytics.mjs +30 -0
  6. package/main/claude-bin.mjs +45 -311
  7. package/main/claude-input.mjs +9 -3
  8. package/main/claude-launch-env.mjs +54 -0
  9. package/main/claude-models.mjs +39 -7
  10. package/main/claude-plan.mjs +16 -16
  11. package/main/claude-usage.mjs +82 -25
  12. package/main/codex-bin.mjs +51 -249
  13. package/main/codex-session.mjs +38 -3
  14. package/main/codex-threads.mjs +41 -8
  15. package/main/codex-watch.mjs +67 -3
  16. package/main/config.mjs +26 -4
  17. package/main/corner-tag.mjs +489 -0
  18. package/main/crash-report.mjs +26 -3
  19. package/main/engine-bin.mjs +595 -0
  20. package/main/engine-setup.mjs +4 -15
  21. package/main/feedback.mjs +11 -6
  22. package/main/harnesses/claude-stream.mjs +208 -0
  23. package/main/harnesses/claude.mjs +84 -0
  24. package/main/harnesses/codex.mjs +65 -0
  25. package/main/harnesses/common.mjs +78 -0
  26. package/main/harnesses/instructions-file.mjs +116 -0
  27. package/main/harnesses/opencode.mjs +234 -0
  28. package/main/harnesses.mjs +65 -0
  29. package/main/importable-threads.mjs +38 -0
  30. package/main/ipc.mjs +165 -31
  31. package/main/local-app-frames.mjs +82 -0
  32. package/main/main.mjs +100 -4
  33. package/main/memory-gate-history.mjs +51 -12
  34. package/main/memory-gate-server.mjs +67 -11
  35. package/main/memory-gate.mjs +49 -8
  36. package/main/memory-log.mjs +124 -0
  37. package/main/notify.mjs +47 -4
  38. package/main/opencode-server.mjs +271 -0
  39. package/main/opencode.mjs +287 -0
  40. package/main/phone-link.mjs +551 -0
  41. package/main/relay-address.mjs +28 -0
  42. package/main/relay-client.mjs +246 -0
  43. package/main/row-label.mjs +6 -6
  44. package/main/runs-on.mjs +32 -35
  45. package/main/serve.mjs +113 -9
  46. package/main/settings.mjs +148 -10
  47. package/main/shell-path.mjs +86 -0
  48. package/main/ship-queue.mjs +24 -6
  49. package/main/sign-in-files.mjs +3 -7
  50. package/main/source-updater.mjs +27 -1
  51. package/main/start-hold.mjs +73 -0
  52. package/main/store.mjs +1 -1
  53. package/main/supervisor.mjs +832 -428
  54. package/main/task-commands.mjs +13 -17
  55. package/main/task-folders-offthread.mjs +6 -1
  56. package/main/task-folders.mjs +490 -98
  57. package/main/task-terminals.mjs +37 -7
  58. package/main/updater.mjs +71 -9
  59. package/main/worktree-include.mjs +494 -0
  60. package/package.json +10 -1
  61. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  62. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  63. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  64. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  65. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  66. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  67. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  68. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  69. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  70. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  71. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  72. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  73. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  74. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  75. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  76. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  77. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  78. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  79. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  80. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  81. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  82. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  83. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  84. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  85. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  86. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  87. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  88. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  89. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  90. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  91. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  92. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  93. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  94. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  95. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  96. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  97. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  98. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  99. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  100. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  101. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  102. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  103. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  104. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  105. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  106. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  107. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  108. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  109. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  110. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  111. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  112. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  113. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  114. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  115. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  116. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  117. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  118. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  119. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  120. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  121. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  122. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  123. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  124. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  125. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  126. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  127. package/renderer/dist/corner-tag.html +13 -0
  128. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  129. package/renderer/dist/emulator/data/compression/README.md +11 -0
  130. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  131. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  132. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  133. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  134. package/renderer/dist/emulator/data/cores/README.md +22 -0
  135. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  136. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  137. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  138. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  139. package/renderer/dist/emulator/data/cores/package.json +65 -0
  140. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  141. package/renderer/dist/emulator/data/emulator.css +1614 -0
  142. package/renderer/dist/emulator/data/loader.js +170 -0
  143. package/renderer/dist/emulator/data/localization/README.md +63 -0
  144. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  145. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  146. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  147. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  148. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  149. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  150. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  151. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  152. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  153. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  154. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  155. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  156. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  157. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  158. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  159. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  160. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  161. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  162. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  163. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  164. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  165. package/renderer/dist/emulator/data/src/compression.js +142 -0
  166. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  167. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  168. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  169. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  170. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  171. package/renderer/dist/emulator/data/src/storage.js +125 -0
  172. package/renderer/dist/emulator/data/version.json +5 -0
  173. package/renderer/dist/emulator/host.js +297 -0
  174. package/renderer/dist/emulator/index.html +28 -0
  175. package/renderer/dist/emulator/provenance.json +6 -0
  176. package/renderer/dist/index.html +16 -3
  177. package/scripts/approval-server.sh +24 -5
  178. package/scripts/before-publish.mjs +12 -10
  179. package/shared/agent-import.mjs +2 -0
  180. package/shared/agents.d.mts +1 -0
  181. package/shared/bridge-map.mjs +18 -0
  182. package/shared/claude-commands.generated.mjs +6 -4
  183. package/shared/claude-models.generated.mjs +6 -4
  184. package/shared/corner-tag.d.mts +14 -0
  185. package/shared/corner-tag.mjs +141 -0
  186. package/shared/engines.mjs +20 -5
  187. package/shared/harness-definitions.mjs +103 -0
  188. package/shared/notify-rules.d.mts +25 -0
  189. package/shared/notify-rules.mjs +19 -0
  190. package/shared/provider-commands.mjs +10 -2
  191. package/shared/rank.d.mts +2 -0
  192. package/shared/rank.mjs +50 -0
  193. package/shared/relay-address.mjs +6 -0
  194. package/shared/relay-crypto.mjs +165 -0
  195. package/shared/runs-on.d.mts +0 -10
  196. package/shared/runs-on.mjs +25 -49
  197. package/shared/shortcuts.mjs +10 -4
  198. package/shared/spawn-trouble.mjs +57 -6
  199. package/shared/terminal-input.d.mts +1 -0
  200. package/shared/terminal-input.mjs +30 -0
  201. package/shared/terminal-keys.d.mts +1 -0
  202. package/shared/terminal-keys.mjs +16 -0
  203. package/shared/terminal-size.d.mts +4 -0
  204. package/shared/terminal-size.mjs +29 -0
  205. package/shared/usage.d.mts +3 -0
  206. package/shared/usage.mjs +7 -0
  207. package/shared/where-it-runs-from.d.mts +43 -0
  208. package/shared/where-it-runs-from.mjs +155 -0
  209. package/shared/work-items.mjs +25 -1
  210. package/shared/work-lines.mjs +1 -1
  211. package/shared/xterm-invalid-range.d.mts +2 -0
  212. package/shared/xterm-invalid-range.mjs +58 -0
  213. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  214. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -0,0 +1,595 @@
1
+ // WHERE A CODING AGENT ACTUALLY IS, on a machine that is not ours.
2
+ //
3
+ // ONE registry and ONE search, for every harness the app can run. There used to
4
+ // be two files, `main/claude-bin.mjs` (338 lines) and `main/codex-bin.mjs` (279
5
+ // lines), which were two copies of one idea; both of them now declare their
6
+ // engine here and run this. A third harness is an ENTRY, not a file.
7
+ //
8
+ // THE RULE THIS EXISTS TO KEEP, inherited from the pair it replaces and not
9
+ // re-argued: never tell somebody their coding agent is missing unless we are
10
+ // certain it is. So there are two different questions and they must never be
11
+ // answered by the same value. `found` is whether a runnable file is really
12
+ // there, and it decides which path gets spawned. `certain` is whether we are
13
+ // entitled to SAY it is missing, and most of this file exists to make `certain`
14
+ // hard to earn.
15
+ //
16
+ // ---------------------------------------------------------------------------
17
+ // WHY THE ORDER CHANGED, 2026-10-08 (w-ce8c1a9275), and it is the whole point
18
+ // of this rewrite.
19
+ //
20
+ // Both old finders tried the hand-written list FIRST and asked the person's own
21
+ // login shell THIRD. The shell is the only source that actually KNOWS where
22
+ // their tools are -- it is their PATH, with their profile loaded, as them -- and
23
+ // it was the fallback. So every new installer, version manager and distro was a
24
+ // patch we ship and a release they wait for. Measured on this repository
25
+ // 2026-10-08: of the 7 commits those two files ever had, three are literally
26
+ // "one more install location".
27
+ //
28
+ // THE SHELL GOES FIRST NOW. The list stays, demoted to what it was really
29
+ // written for: a GUI launch with a thin PATH. A double-clicked Electron app
30
+ // inherits launchd's PATH, which is /usr/bin:/bin:/usr/sbin:/sbin and has never
31
+ // heard of nvm, volta or Homebrew, so a login shell that times out or will not
32
+ // start leaves the list as the only thing that can answer.
33
+ //
34
+ // WHAT THAT COSTS, measured on this Mac the same day: `zsh -lic 'command -v
35
+ // claude'` takes 0.38-0.43 s, so a machine whose list would have hit on the
36
+ // first stat call now pays about 0.4 s per engine at boot instead of nothing.
37
+ // Two engines, once, behind a thirty second memory. That is the price of not
38
+ // shipping a release every time somebody installs their tools a new way.
39
+ //
40
+ // AND A LOOKUP THAT THROWS IS NOT AN ABSENCE. The shell step used never to run
41
+ // at all on a machine whose list had the binary, so nothing downstream of it was
42
+ // ever reached by a throw. It runs first now, and the app refuses to open an
43
+ // inbox without Claude Code, so a probe that blows up falls through to the list
44
+ // rather than taking Claude Code away from somebody who has it. It still does
45
+ // not count as an answer, so it cannot earn the sentence either.
46
+ // ---------------------------------------------------------------------------
47
+ //
48
+ // Nothing here overrides a path somebody set. An explicit `claudeBin` or
49
+ // `codexBin` wins untouched and is never searched around, because that promise
50
+ // is what makes "point at it yourself" a real way out of a search that missed
51
+ // (main/ipc.mjs, `zero:engine-bin`).
52
+
53
+ import fs from 'node:fs';
54
+ import os from 'node:os';
55
+ import path from 'node:path';
56
+ import { execFileSync } from 'node:child_process';
57
+
58
+ /* ------------------------------ the strategy ------------------------------ */
59
+
60
+ /**
61
+ * EVERY PACKAGE MANAGER'S DEFAULT BIN DIRECTORY, in order, for any tool.
62
+ *
63
+ * This list was written TWICE, once per engine, fourteen entries each, and a
64
+ * path added to one and not the other is how one engine quietly stops being
65
+ * found where the other is. It is one list now and every engine's candidates
66
+ * are generated from it, so a new entry reaches every harness at once.
67
+ */
68
+ export function sharedBinDirs(home = os.homedir()) {
69
+ return [
70
+ '/opt/homebrew/bin', // Homebrew, Apple Silicon
71
+ '/usr/local/bin', // Homebrew on Intel, and npm -g
72
+ path.join(home, '.bun/bin'), // bun
73
+ path.join(home, '.volta/bin'), // volta
74
+ path.join(home, '.asdf/shims'), // asdf, through its shims
75
+ path.join(home, '.npm-global/bin'), // the two npm prefixes people
76
+ path.join(home, '.npm-packages/bin'), // actually write in a guide
77
+ path.join(home, '.yarn/bin'), // yarn global
78
+ path.join(home, 'Library/pnpm'), // pnpm's own bin on macOS
79
+ path.join(home, '.local/share/pnpm'), // pnpm's own bin on Linux
80
+ path.join(home, '.deno/bin'), // deno
81
+ path.join(home, 'bin'), // a hand-rolled ~/bin on PATH
82
+ '/opt/local/bin', // MacPorts
83
+ '/usr/bin', // packaged by something system-wide
84
+ ];
85
+ }
86
+
87
+ /**
88
+ * ONE BIN DIRECTORY PER INSTALLED NODE, under a name no list can hold. nvm,
89
+ * fnm, n and asdf each keep one per version, so they are READ rather than
90
+ * listed, and a directory that is not there contributes nothing. This is where
91
+ * a global npm install under any of them lands and nowhere else.
92
+ */
93
+ export function versionedBinPaths(bin, home = os.homedir(), readdir = defaultReaddir) {
94
+ const under = (dir, tail) => readdir(path.join(home, dir)).map((v) => path.join(home, dir, v, tail));
95
+ return [
96
+ ...under('.nvm/versions/node', `bin/${bin}`),
97
+ ...under('.local/share/fnm/node-versions', `installation/bin/${bin}`),
98
+ ...under('Library/Application Support/fnm/node-versions', `installation/bin/${bin}`),
99
+ ...under('n/versions/node', `bin/${bin}`),
100
+ ...under('.asdf/installs/nodejs', `bin/${bin}`),
101
+ ];
102
+ }
103
+
104
+ function defaultReaddir(dir) {
105
+ try { return fs.readdirSync(dir); } catch { return []; }
106
+ }
107
+
108
+ /**
109
+ * A path that is a symlink to nothing. `existsSync` follows the link and says
110
+ * false, which is indistinguishable from nothing being there at all, and the two
111
+ * mean opposite things: both engines' ordinary installs point a stable link at a
112
+ * versioned file and swap that file on every update, so this is the shape of a
113
+ * tool mid-upgrade rather than an odd one.
114
+ */
115
+ function defaultDangles(p) {
116
+ try { return !!fs.lstatSync(p) && !fs.existsSync(p); } catch { return false; }
117
+ }
118
+
119
+ /** Newest version folder first, by number and not by spelling. */
120
+ function newestFirst(a, b) {
121
+ const x = a.split('.').map(Number), y = b.split('.').map(Number);
122
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
123
+ if ((x[i] ?? 0) !== (y[i] ?? 0)) return (y[i] ?? 0) - (x[i] ?? 0);
124
+ }
125
+ return 0;
126
+ }
127
+
128
+ /* ------------------------------- the registry ----------------------------- */
129
+
130
+ /**
131
+ * WHAT AN ENGINE IS, and the whole of what a new harness has to say:
132
+ *
133
+ * bin the program name, which is also its key here
134
+ * name what it is called on a screen
135
+ * installUrl where to get it, when we are sure it is not here
136
+ * ownPaths where the tool's OWN installer puts it, tried before the
137
+ * package managers because it is the answer the tool produces
138
+ * appCopies a copy carried inside a desktop app, tried last of all
139
+ * evidence what it leaves behind, which is never spawned and only ever
140
+ * takes away our right to announce an absence
141
+ */
142
+ function engine({ bin, name, installUrl, ownPaths, appCopies = () => [], evidence }) {
143
+ return {
144
+ bin,
145
+ name,
146
+ installUrl,
147
+ ownPaths,
148
+ appCopies,
149
+ evidence,
150
+ // In order, and the first that exists wins. The tool's own paths, then every
151
+ // package manager's bin directory, then one directory per installed Node.
152
+ candidates(home = os.homedir(), readdir = defaultReaddir) {
153
+ return [
154
+ ...ownPaths(home),
155
+ ...sharedBinDirs(home).map((dir) => path.join(dir, bin)),
156
+ ...versionedBinPaths(bin, home, readdir),
157
+ ];
158
+ },
159
+ };
160
+ }
161
+
162
+ export const ENGINES = {
163
+ /**
164
+ * CLAUDE CODE. Required: the app will not open an inbox without it, which is
165
+ * why its absence is the one a screen is ever allowed to shout about.
166
+ *
167
+ * `~/.local/bin/claude` is the installer's own path and `~/.claude/local/claude`
168
+ * is what `claude migrate-installer` leaves behind. The installer points the
169
+ * first at a versioned file and swaps that file on every update, so a dangling
170
+ * link there is a real state and not a hypothetical.
171
+ *
172
+ * THE COPY THE CLAUDE DESKTOP APP KEEPS, one folder per version, newest first.
173
+ * Somebody who has only ever used Claude Code inside the Claude app has this
174
+ * and nothing else (w-9f6975906c, measured 2026-10-05: 2.1.209 there, sharing
175
+ * the CLI's sign-in). Last of all, because it updates when the app does and a
176
+ * real install is fresher -- that copy was three months old beside a 2.1.289
177
+ * install on the same Mac.
178
+ */
179
+ claude: engine({
180
+ bin: 'claude',
181
+ name: 'Claude Code',
182
+ installUrl: 'https://code.claude.com/docs/en/setup',
183
+ ownPaths: (home) => [
184
+ path.join(home, '.local/bin/claude'), // the installer
185
+ path.join(home, '.claude/local/claude'), // claude migrate-installer
186
+ ],
187
+ appCopies: (home, readdir = defaultReaddir) => {
188
+ const dir = path.join(home, 'Library/Application Support/Claude/claude-code');
189
+ return readdir(dir)
190
+ .filter((v) => /^\d+(\.\d+)*$/.test(v))
191
+ .sort(newestFirst)
192
+ .map((v) => path.join(dir, v, 'claude.app/Contents/MacOS/claude'));
193
+ },
194
+ // WHAT CLAUDE CODE LEAVES BEHIND. Any one of these means it has run on this
195
+ // machine. None is a binary and none is ever spawned.
196
+ evidence: (home) => [
197
+ path.join(home, '.claude.json'), // its own config file
198
+ path.join(home, '.claude/settings.json'),
199
+ path.join(home, '.claude/projects'), // one folder per project it has opened
200
+ path.join(home, '.claude/statsig'),
201
+ path.join(home, '.claude/.credentials.json'), // it has been logged in
202
+ path.join(home, '.claude/history.jsonl'),
203
+ path.join(home, '.local/share/claude'), // the installer's version store
204
+ ],
205
+ }),
206
+
207
+ /**
208
+ * CODEX, and it is OPTIONAL where Claude Code is not. An absence is the
209
+ * ordinary case rather than a fault, so nothing about it ever produces a
210
+ * warning: it just leaves the second engine out of the map
211
+ * `shared/engines.mjs` reads.
212
+ *
213
+ * MEASURED ON A MAC 2026-09-04, because the 2026-08-25 original was written
214
+ * against codex-cli 0.144.0 and six of its assumptions had moved. codex-cli
215
+ * 0.148.0 at /opt/homebrew/bin/codex, a Homebrew CASK rather than a formula,
216
+ * whose bin entry is a SYMLINK into /opt/homebrew/Caskroom/codex/0.148.0/bin.
217
+ * `brew info --cask codex` the same minute: 0.148.0 installed against 0.152.1
218
+ * available, so the moment mid-upgrade where that link points at nothing is a
219
+ * live state and not a story.
220
+ *
221
+ * THE INSTALL METHOD THE DOCS NOW LEAD WITH, `chatgpt.com/codex/install.sh`,
222
+ * read 2026-09-04: `BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}"`,
223
+ * `STANDALONE_ROOT="${CODEX_HOME:-$HOME/.codex}/packages/standalone"`. It links
224
+ * `~/.local/bin/codex` at a versioned file under that root. AND THE STORE KEEPS
225
+ * THE BINARY IN TWO PLACES -- the same script stages `<release>/bin/codex` and
226
+ * links `<release>/codex` beside it (lines 951 and 1021), and `current` is a
227
+ * symlink at the live release -- so both resolve on a machine set up the
228
+ * documented way, each costs one stat call, and listing only one is how the app
229
+ * tells somebody their Codex is missing while they are looking at it.
230
+ *
231
+ * WHAT ~/.codex REALLY HOLDS: 38 entries on that Mac. auth.json, config.toml,
232
+ * sessions/ and history.jsonl are all still there, but the live record moved
233
+ * into state_5.sqlite, thread_history_1.sqlite, logs_2.sqlite and friends,
234
+ * whose names carry a schema number that increments -- sessions/ held 1691
235
+ * rollout files written the day before while history.jsonl had not been
236
+ * touched since 2026-06-03. That is why the directory itself is on the list,
237
+ * last and broad, and why nothing tries to enumerate what is inside it.
238
+ */
239
+ codex: engine({
240
+ bin: 'codex',
241
+ name: 'Codex',
242
+ installUrl: 'https://learn.chatgpt.com/docs/codex/cli',
243
+ ownPaths: (home) => [
244
+ path.join(home, '.local/bin/codex'), // the shell installer's link
245
+ path.join(home, '.codex/packages/standalone/current/bin/codex'),
246
+ path.join(home, '.codex/packages/standalone/current/codex'),
247
+ ],
248
+ // THE COPY THE CHATGPT APP CARRIES INSIDE ITSELF (w-9f6975906c, measured
249
+ // 2026-10-05: codex-cli 0.158.0-alpha.2, sharing the CLI's sign-in), and the
250
+ // one the Codex desktop app carries (w-d5d632e503, its reported place rather
251
+ // than one measured here). A path that is not there costs one `exists`.
252
+ appCopies: (home) => [
253
+ 'ChatGPT.app/Contents/Resources/codex-cli/bin/codex',
254
+ 'Codex.app/Contents/Resources/codex',
255
+ ].flatMap((p) => [path.join('/Applications', p), path.join(home, 'Applications', p)]),
256
+ // BOTH HOMES ARE LOOKED IN WHEN CODEX_HOME IS SET, on purpose: a CODEX_HOME
257
+ // inherited from somewhere else is not proof the ordinary directory is gone,
258
+ // and the whole job of this list is to make an absence hard to claim.
259
+ evidence: (home, env = process.env) => {
260
+ const homes = [env?.CODEX_HOME, path.join(home, '.codex')].filter(Boolean);
261
+ const out = [];
262
+ for (const dir of new Set(homes)) {
263
+ out.push(
264
+ path.join(dir, 'auth.json'), // it has been logged in
265
+ path.join(dir, 'config.toml'),
266
+ path.join(dir, 'sessions'), // one folder tree of past runs
267
+ path.join(dir, 'history.jsonl'),
268
+ path.join(dir, 'packages/standalone/releases'), // the installer's version store
269
+ dir, // and the directory itself, last
270
+ );
271
+ }
272
+ return out;
273
+ },
274
+ }),
275
+ };
276
+
277
+ export function engineKeys() {
278
+ return Object.keys(ENGINES);
279
+ }
280
+
281
+ function engineFor(key) {
282
+ const found = ENGINES[key];
283
+ if (!found) throw new Error(`no coding agent is declared as ${JSON.stringify(key)}`);
284
+ return found;
285
+ }
286
+
287
+ /* -------------------------------- the shell ------------------------------- */
288
+
289
+ // The shells worth asking, in order, and never the same one twice. Theirs first
290
+ // because it is the one that has their PATH in it; the other two because a
291
+ // $SHELL that will not start is a fact about $SHELL and not about the tool.
292
+ export function shellProbes(env = process.env) {
293
+ const preferred = env.SHELL || '/bin/zsh';
294
+ const out = [[preferred, '-lic', 4000], [preferred, '-lc', 2500]];
295
+ for (const other of ['/bin/zsh', '/bin/bash']) {
296
+ if (other !== preferred) out.push([other, '-lic', 2500]);
297
+ }
298
+ return out;
299
+ }
300
+
301
+ /**
302
+ * WHAT MAY BE HANDED TO A LOGIN SHELL, AND WHY THIS IS CHECKED AT ALL.
303
+ *
304
+ * The probe below interpolates `bin` into a string a LOGIN, INTERACTIVE shell
305
+ * executes, with their profile loaded, as them. Every caller passes a registry
306
+ * entry's `bin`, which is a literal in this file, so nothing is exploitable --
307
+ * but a `codexBin` read out of zero.config.json reaching this argument is one
308
+ * plausible slice away, and at that point `command -v x; curl | sh` is a string
309
+ * in a file that runs itself.
310
+ *
311
+ * This function looks up program NAMES, so a name is all it takes. An allowlist
312
+ * rather than quoting, because quoting is a thing a later edit can undo without
313
+ * noticing and a shape check is not.
314
+ */
315
+ const PROGRAM_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
316
+
317
+ /**
318
+ * Ask the person's own login shell, and go on asking until one of them answers.
319
+ * Anything that is not an absolute path to a file that exists is treated as no
320
+ * path: `command -v` also prints shell functions and aliases, and a function
321
+ * name handed to spawn is a crash with a worse message than "not found".
322
+ *
323
+ * `answered` is the strict half. It is true only when every probe that ran came
324
+ * back with a real exit status, and at least one did. One shell that timed out
325
+ * alongside one that said no is not a machine we know anything about.
326
+ *
327
+ * It must be a LOGIN, INTERACTIVE shell, because the version managers put
328
+ * themselves on PATH from `.zprofile` and `.zshrc` and nowhere else. That is the
329
+ * whole reason an npm install is obvious in a terminal and invisible to the app.
330
+ * It is asked more than once because ONE probe that fails to start is not a fact
331
+ * about anybody's machine.
332
+ */
333
+ export function askShell({ env = process.env, exists = fs.existsSync, run = defaultRun, bin = 'claude' } = {}) {
334
+ if (typeof bin !== 'string' || !PROGRAM_NAME.test(bin)) {
335
+ throw new Error(`askShell looks up a program name, and ${JSON.stringify(bin)} is not one`);
336
+ }
337
+ let said = false;
338
+ let inconclusive = false;
339
+ for (const [shell, flags, timeout] of shellProbes(env)) {
340
+ let out = '';
341
+ try {
342
+ out = run(shell, [flags, `command -v ${bin}`], timeout);
343
+ } catch (e) {
344
+ // TWO DIFFERENT SILENCES, AND ONLY ONE OF THEM IS AN ANSWER. A shell that
345
+ // ran and exited non-zero has told us the tool is not on PATH, and that is
346
+ // an answer. A shell that could not be started, or that ran past the
347
+ // timeout, has told us nothing at all, and reporting nothing as "not
348
+ // installed" is what put a line about missing software on the screen of
349
+ // people who have it.
350
+ if (exitedSayingNo(e)) said = true; else inconclusive = true;
351
+ continue;
352
+ }
353
+ const line = String(out).split('\n').map((s) => s.trim()).filter(Boolean).pop();
354
+ if (line && line.startsWith('/') && exists(line)) return { path: line, answered: true };
355
+ said = true;
356
+ }
357
+ return { path: null, answered: said && !inconclusive };
358
+ }
359
+
360
+ // The shell ran to completion and returned a non-zero exit code, which for
361
+ // `command -v` is how "not on PATH" is spelt. A timeout (SIGTERM, ETIMEDOUT)
362
+ // and a shell that does not exist (ENOENT) both arrive here too, and neither
363
+ // of them is a fact about the tool.
364
+ function exitedSayingNo(e) {
365
+ if (!e || typeof e !== 'object') return false;
366
+ if (e.signal || e.code === 'ETIMEDOUT' || e.code === 'ENOENT') return false;
367
+ return typeof e.status === 'number';
368
+ }
369
+
370
+ function defaultRun(shell, args, timeout = 4000) {
371
+ return execFileSync(shell, args, {
372
+ encoding: 'utf8',
373
+ // stdin ignored on purpose: an interactive shell handed a live terminal can
374
+ // sit waiting for input, and this runs on the boot path.
375
+ stdio: ['ignore', 'pipe', 'ignore'],
376
+ timeout,
377
+ });
378
+ }
379
+
380
+ // askShell used to answer with a path or null, and both a real answer and a
381
+ // failed probe were null. Anything still handing back the old shape is read as
382
+ // an answer, because that is what it always meant.
383
+ function shellSaid(result) {
384
+ if (typeof result === 'string') return { path: result, answered: true };
385
+ if (!result || typeof result !== 'object') return { path: null, answered: true };
386
+ return { path: result.path ?? null, answered: result.answered !== false };
387
+ }
388
+
389
+ /**
390
+ * ONE LOGIN SHELL, ASKED ONCE, ANSWERING FOR EVERY ENGINE.
391
+ *
392
+ * MEASURED ON THIS MAC 2026-10-11, against the finished shell-first finder:
393
+ * `findClaudeBin()` and `findCodexBin()` together took 1640, 1432 and 1435 ms
394
+ * cold, and 0 ms with the shell stubbed out. main/main.mjs calls `loadConfig`
395
+ * inside `createWindow` and BEFORE `new BrowserWindow`, so that was not time in
396
+ * the background: it was a second and a half of nothing on screen, added to
397
+ * every launch, and it was the price of asking the shell first.
398
+ *
399
+ * THE COST IS THE SHELL STARTING, NOT THE QUESTION. A login, interactive shell
400
+ * reads the whole profile -- which is the entire reason it can answer at all --
401
+ * and that costs about the same whether it is asked about one program or six.
402
+ * Asking it once per engine paid it once per engine.
403
+ *
404
+ * So this asks about all of them in one command and `warmShellAnswers` seeds
405
+ * every engine's cache from it.
406
+ *
407
+ * IT ONLY EVER ANSWERS POSITIVELY, which is what makes it safe to put in front
408
+ * of the probe that was already here rather than in place of it. A path is
409
+ * taken only when it is absolute, really on the disk, and really named after
410
+ * the engine that asked -- the lines come back in whatever order the shell
411
+ * prints them, and a path handed to the wrong engine would spawn the wrong
412
+ * agent. Nothing here ever records an ABSENCE: `answered` is the subtle half of
413
+ * this file's rules, and the per-engine probe stays the only thing allowed to
414
+ * settle one. So the worst a bad combined answer can do is nothing at all.
415
+ */
416
+ export function askShellForAll(bins, { env = process.env, exists = fs.existsSync, run = defaultRun } = {}) {
417
+ for (const bin of bins) {
418
+ if (typeof bin !== 'string' || !PROGRAM_NAME.test(bin)) {
419
+ throw new Error(`askShellForAll looks up program names, and ${JSON.stringify(bin)} is not one`);
420
+ }
421
+ }
422
+ const command = bins.map((b) => `command -v ${b}`).join('; ');
423
+ for (const [shell, flags, timeout] of shellProbes(env)) {
424
+ let out = '';
425
+ try { out = run(shell, [flags, command], timeout); } catch { continue; }
426
+ const found = {};
427
+ for (const line of String(out).split('\n').map((s) => s.trim()).filter(Boolean)) {
428
+ // A function body, an alias line or a bare name is not a path, and a path
429
+ // belongs to the engine whose name it ends with or to nobody.
430
+ if (!line.startsWith('/') || !exists(line)) continue;
431
+ const bin = bins.find((b) => path.basename(line) === b);
432
+ if (bin && !found[bin]) found[bin] = line;
433
+ }
434
+ // A shell that ran has had its turn, whatever it did or did not print.
435
+ return found;
436
+ }
437
+ return {};
438
+ }
439
+
440
+ /**
441
+ * Seed every engine's held shell answer off one probe. Silent either way: a
442
+ * shell that will not start leaves each engine to its own probe, which is
443
+ * exactly what used to happen for all of them.
444
+ */
445
+ export function warmShellAnswers({ now = Date.now(), ...how } = {}) {
446
+ let found = {};
447
+ try { found = askShellForAll(engineKeys().map((k) => ENGINES[k].bin), how); } catch { return; }
448
+ for (const key of engineKeys()) {
449
+ const where = found[ENGINES[key].bin];
450
+ if (where) held.set(key, { answer: { path: where, answered: true }, at: now });
451
+ }
452
+ }
453
+
454
+ /**
455
+ * THE SHELL STEP, AND IT MAY NOT TAKE THE SEARCH DOWN WITH IT.
456
+ *
457
+ * It runs FIRST now, so it is the one step whose failure would reach everything
458
+ * after it. A lookup that throws is read as the shell having said nothing: the
459
+ * list still gets its turn, and the throw still does not count as an answer, so
460
+ * it cannot earn the sentence about missing software either.
461
+ */
462
+ function askedShell(lookup) {
463
+ try { return shellSaid(lookup()); } catch { return { path: null, answered: false }; }
464
+ }
465
+
466
+ /* -------------------------------- the search ------------------------------ */
467
+
468
+ /**
469
+ * WHATEVER THE TOOL LEFT BEHIND, or null. None of these is a binary and none is
470
+ * ever spawned; they exist only to stop the app announcing an absence it cannot
471
+ * support. A dangling candidate counts the same way, because an update that has
472
+ * swapped the versioned file out from under a stable link leaves exactly that.
473
+ */
474
+ export function installEvidence(key, {
475
+ home = os.homedir(), env = process.env, exists = fs.existsSync, dangles = defaultDangles,
476
+ } = {}) {
477
+ const spec = engineFor(key);
478
+ for (const p of spec.evidence(home, env)) if (exists(p)) return p;
479
+ for (const p of spec.candidates(home, () => [])) if (dangles(p)) return p;
480
+ return null;
481
+ }
482
+
483
+ /**
484
+ * THE WHOLE SEARCH, as a pure function of its inputs so a test can put a coding
485
+ * agent anywhere without touching the machine it runs on.
486
+ *
487
+ * Returns { path, found, certain, from, searched, evidence }. `found` is the only
488
+ * claim about the world: it means a file is really there. `certain` says whether
489
+ * we are allowed to tell somebody it is NOT there, so a screen can stay quiet
490
+ * rather than announce an absence it has not established. They come apart in
491
+ * exactly one case that is not a failure, and it is the case worth keeping
492
+ * honest: somebody has set a path that does not exist, so the path stays THEIRS
493
+ * (anything that spawns it should fail naming the path they chose, not one we
494
+ * picked) while the screen says it was not found and offers the install link,
495
+ * the same as any other machine.
496
+ */
497
+ export function findEngine(key, {
498
+ configured = null,
499
+ home = os.homedir(),
500
+ env = process.env,
501
+ exists = fs.existsSync,
502
+ shellLookup = null,
503
+ readdir = defaultReaddir,
504
+ dangles = defaultDangles,
505
+ } = {}) {
506
+ const spec = engineFor(key);
507
+ if (configured) {
508
+ return {
509
+ path: configured, found: exists(configured), certain: true, from: 'settings',
510
+ searched: [], evidence: null,
511
+ };
512
+ }
513
+
514
+ // THE SHELL FIRST. It is the only source that knows where their tools are.
515
+ // The default probe reads the real environment and the real disk, exactly as
516
+ // both old finders' defaults did: a test that wants to put a tool somewhere
517
+ // hands in `shellLookup`, and one that hands in only a fake `exists` is
518
+ // asking about the LIST, so feeding its fake into the shell step would
519
+ // silently answer a different question.
520
+ const lookup = shellLookup ?? (() => askShell({ bin: spec.bin }));
521
+ const fromShell = askedShell(lookup);
522
+ // Computed either way, because a caller that wants to say where we looked
523
+ // wants it most on the answer it is going to draw.
524
+ const searched = spec.candidates(home, readdir);
525
+ if (fromShell.path) {
526
+ return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
527
+ }
528
+
529
+ // THEN THE LIST, which is the GUI launch with launchd's thin PATH.
530
+ for (const candidate of searched) {
531
+ if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'disk', searched, evidence: null };
532
+ }
533
+
534
+ // THEN A DESKTOP APP'S OWN COPY, last because it moves with that app's
535
+ // releases and a real install anywhere is fresher.
536
+ for (const candidate of spec.appCopies(home, readdir)) {
537
+ if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'app', searched, evidence: null };
538
+ }
539
+
540
+ // NOT FOUND, AND WHETHER THAT IS KNOWN. Every shell we could reach missed and
541
+ // every cheap path missed, so the last question is whether this machine shows
542
+ // any sign of the tool anyway. If it does, we looked in the wrong places
543
+ // rather than at an empty machine, and nothing may be said out loud.
544
+ const evidence = installEvidence(key, { home, env, exists, dangles });
545
+ return {
546
+ path: null,
547
+ found: false,
548
+ certain: fromShell.answered && !evidence,
549
+ from: null,
550
+ searched,
551
+ evidence,
552
+ };
553
+ }
554
+
555
+ /* -------------------------------- the cache ------------------------------- */
556
+ // Only the SHELL step is remembered, and only briefly. Nothing else is: the path
557
+ // checks are stat calls, and caching them is how a settings screen ends up
558
+ // telling someone to install their coding agent and then still saying "not
559
+ // found" after they have. The sentence on that screen makes a promise and a
560
+ // cached answer breaks it.
561
+ //
562
+ // The shell is the one part worth holding, because it is up to three
563
+ // subprocesses and a screen somebody toggles a switch on reads settings again on
564
+ // every write. Thirty seconds is long enough that a burst of writes pays for it
565
+ // once, and short enough that it is never the reason someone is still stuck.
566
+ //
567
+ // PER ENGINE, because looking for Claude Code tells you nothing about Codex, and
568
+ // one shared window would have the second engine read the first one's answer.
569
+
570
+ const SHELL_TTL = 30_000;
571
+ const held = new Map();
572
+
573
+ export function resolveEngine(key, configured = null, { now = Date.now(), shellLookup = null, ...where } = {}) {
574
+ const spec = engineFor(key);
575
+ const lookup = shellLookup ?? (() => askShell({ bin: spec.bin }));
576
+ const remembered = () => {
577
+ const last = held.get(key);
578
+ // THE WINDOW HAS A FLOOR AS WELL AS A CEILING. `now - at < TTL` alone is
579
+ // true for any answer stamped in the FUTURE, and since `warmShellAnswers`
580
+ // stamps real clock time into this same map, anything asking with a
581
+ // synthetic `now` would read a warm answer as fresh forever. An answer from
582
+ // ahead of the question is not an answer; it is asked again.
583
+ const age = now - (last?.at ?? 0);
584
+ if (last && age >= 0 && age < SHELL_TTL) return last.answer;
585
+ const answer = lookup();
586
+ held.set(key, { answer, at: now });
587
+ return answer;
588
+ };
589
+ return findEngine(key, { ...where, configured, shellLookup: remembered });
590
+ }
591
+
592
+ export function forgetEngine(key) {
593
+ engineFor(key); // an engine nobody declared is a mistake here too, not a no-op
594
+ held.delete(key);
595
+ }
@@ -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
@@ -25,28 +26,16 @@ import { spawn as nodeSpawn } from 'node:child_process';
25
26
  import { terminalEnv } from './task-terminals.mjs';
26
27
  import { codexLaunchEnv } from './codex-launch-env.mjs';
27
28
 
28
- const INSTALLERS = {
29
- claude: { shell: '/bin/bash', url: 'https://claude.ai/install.sh', pipe: 'bash' },
30
- codex: { shell: '/bin/sh', url: 'https://chatgpt.com/codex/install.sh', pipe: 'sh' },
31
- };
32
-
33
- export function installCommand(engine) {
34
- const i = INSTALLERS[engine];
35
- return { file: i.shell, args: ['-c', `curl -fsSL ${i.url} | ${i.pipe}`] };
36
- }
29
+ export function installCommand(engine) { return harnessFor(engine).install(); }
37
30
 
38
31
  /** The plan, never the API console: `--claudeai` is Claude Code's own name for it. */
39
32
  export function signInCommand(engine, bin) {
40
- return engine === 'claude'
41
- ? { file: bin, args: ['auth', 'login', '--claudeai'] }
42
- : { file: bin, args: ['login'] };
33
+ return harnessFor(engine).signIn(bin);
43
34
  }
44
35
 
45
36
  /** Exit 0 is signed in and 1 is not, for both (measured 2026-10-05). */
46
37
  export function statusCommand(engine, bin) {
47
- return engine === 'claude'
48
- ? { file: bin, args: ['auth', 'status', '--json'] }
49
- : { file: bin, args: ['login', 'status'] };
38
+ return harnessFor(engine).signInStatus(bin);
50
39
  }
51
40
 
52
41
  const LOG_LIMIT = 64 * 1024;
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