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,337 +1,72 @@
1
- // Where Claude Code actually is, on a machine that is not hers.
1
+ // Where Claude Code actually is, on a machine that is not ours.
2
2
  //
3
- // Agentbox assumed one path, `~/.local/bin/claude`, which is where Claude Code's
4
- // own installer puts it. That is one of several normal answers: install through
5
- // npm or Homebrew and the binary is somewhere else entirely. For those people
6
- // the app found nothing, said "missing" on a grey dot in Settings, and gave no
7
- // sentence and no next step. It read as broken rather than as unfinished.
3
+ // THIS FILE IS NOW THE NAMES AND NOTHING ELSE. The search, the registry entry
4
+ // that declares Claude Code, the measured notes about where it installs and the
5
+ // `found` / `certain` rule all live in main/engine-bin.mjs, which does the same
6
+ // job for every coding agent the app can run. Until 2026-10-08 this was 338
7
+ // lines and main/codex-bin.mjs was 279, and they were two copies of one idea:
8
+ // adding a harness meant a third file and three hundred lines of test.
8
9
  //
9
- // THE RULE this file exists to keep: never tell someone Claude Code is
10
- // missing unless we are certain it is.
11
- //
12
- // So there are two different questions here and they must never be answered by
13
- // the same value. `found` is whether a runnable file is really there, and it
14
- // decides which path gets spawned. `certain` is whether we are entitled to SAY
15
- // it is missing, and everything below exists to make `certain` hard to earn.
16
- //
17
- // Four steps before an absence is ever claimed, cheapest first.
18
- //
19
- // KNOWN PATHS. A filesystem check is instant, spawns nothing, and is the right
20
- // answer for most machines. The list is every normal home for this binary: the
21
- // installer's own directory, Claude Code's local install (what
22
- // `claude migrate-installer` leaves behind), Homebrew on Apple Silicon and on
23
- // Intel, MacPorts, and the default bin of every package manager that is not npm.
24
- //
25
- // VERSION MANAGER DIRECTORIES. nvm, fnm, n and asdf each keep one bin directory
26
- // PER INSTALLED NODE, under a name nobody can guess, so they are read rather
27
- // than listed. A global npm install under any of them lands there and nowhere
28
- // else, and this is the case the shell step used to be the only cover for.
29
- //
30
- // THE SHELL. The catch-all for installs no list can enumerate: volta, bun, a
31
- // custom npm prefix. It must be a LOGIN, INTERACTIVE shell, because those
32
- // managers put themselves on PATH from `.zprofile` and `.zshrc` and nowhere
33
- // else. And it is needed at all because a double-clicked Electron app inherits
34
- // launchd's PATH, which is /usr/bin:/bin:/usr/sbin:/sbin and has never heard of
35
- // any of them. That is the whole reason an npm install is obvious in a terminal
36
- // and invisible to the app. It is asked more than once now, because ONE probe
37
- // that fails to start is not a fact about anybody's Mac.
10
+ // WHAT IS KEPT HERE is the vocabulary the rest of the app already imports --
11
+ // main/config.mjs, main/settings.mjs, scripts/read-claude-commands.mjs,
12
+ // scripts/read-claude-models.mjs -- plus `askShell` and `shellProbes`, which
13
+ // several tests reach for through this file because Claude Code is the binary the
14
+ // app will not open an inbox without.
38
15
  //
39
- // EVIDENCE THAT IT LIVES HERE ANYWAY. Last, and it decides nothing about which
40
- // binary to run: it only takes away our right to announce an absence. Claude
41
- // Code leaves a home folder, a settings file and a projects directory behind
42
- // the moment it is used once. If those are on this Mac then Claude Code is on
43
- // this Mac, whatever our search of PATH did or did not manage, and the honest
44
- // thing on the screen is silence rather than a sentence telling somebody to
45
- // install software they are already running. A dangling `claude` symlink counts
46
- // the same way, and it is a real state: the installer points ~/.local/bin/claude
47
- // at a versioned file and swaps that file on every update.
16
+ // THE RULE THIS FILE ARGUED FOR AND engine-bin.mjs KEEPS: never tell somebody
17
+ // Claude Code is missing unless we are certain it is. `found` is whether a
18
+ // runnable file is really there, and it decides which path gets spawned.
19
+ // `certain` is whether we are entitled to SAY it is missing.
48
20
  //
49
21
  // Nothing here overrides the config file. An explicit `claudeBin` in
50
- // zero.config.json wins untouched and is never searched around, because that
51
- // file promises explicit paths with no discovery, and a search that
52
- // second-guesses a written setting would break that promise.
22
+ // zero.config.json wins untouched and is never searched around.
53
23
 
54
- import fs from 'node:fs';
55
24
  import os from 'node:os';
56
- import path from 'node:path';
57
- import { execFileSync } from 'node:child_process';
58
-
59
- // In order. The first that exists wins, so the installer's path stays first and
60
- // a machine that has always worked keeps resolving exactly as it did.
61
- export function candidatePaths(home = os.homedir()) {
62
- return [
63
- path.join(home, '.local/bin/claude'), // the installer
64
- path.join(home, '.claude/local/claude'), // claude migrate-installer
65
- '/opt/homebrew/bin/claude', // Homebrew, Apple Silicon
66
- '/usr/local/bin/claude', // Homebrew on Intel, and npm -g
67
- path.join(home, '.bun/bin/claude'), // bun
68
- path.join(home, '.volta/bin/claude'), // volta
69
- path.join(home, '.asdf/shims/claude'), // asdf, through its shims
70
- path.join(home, '.npm-global/bin/claude'), // the two npm prefixes people
71
- path.join(home, '.npm-packages/bin/claude'), // actually write in a guide
72
- path.join(home, '.yarn/bin/claude'), // yarn global
73
- path.join(home, 'Library/pnpm/claude'), // pnpm's own bin on macOS
74
- path.join(home, '.deno/bin/claude'), // deno
75
- path.join(home, 'bin/claude'), // a hand-rolled ~/bin on PATH
76
- '/opt/local/bin/claude', // MacPorts
77
- '/usr/bin/claude', // packaged by something system-wide
78
- ];
79
- }
80
-
81
- // One bin directory per installed Node, under a name no list can hold. Read
82
- // rather than guessed, and a directory that is not there just contributes
83
- // nothing. This is where a global npm install under nvm, fnm, n or asdf ends
84
- // up, and until now only the shell could reach it.
85
- export function versionedPaths(home = os.homedir(), readdir = defaultReaddir) {
86
- const under = (dir, tail) => readdir(path.join(home, dir)).map((v) => path.join(home, dir, v, tail));
87
- return [
88
- ...under('.nvm/versions/node', 'bin/claude'),
89
- ...under('.local/share/fnm/node-versions', 'installation/bin/claude'),
90
- ...under('Library/Application Support/fnm/node-versions', 'installation/bin/claude'),
91
- ...under('n/versions/node', 'bin/claude'),
92
- ...under('.asdf/installs/nodejs', 'bin/claude'),
93
- ];
94
- }
95
-
96
- function defaultReaddir(dir) {
97
- try { return fs.readdirSync(dir); } catch { return []; }
98
- }
99
-
100
- // THE COPY THE CLAUDE APP KEEPS FOR ITSELF, one folder per version, newest
101
- // first. Somebody who has only ever used Claude Code inside the Claude app has
102
- // this and nothing else (w-9f6975906c, measured 2026-10-05: 2.1.209 there, and
103
- // it shared the CLI's sign-in). It is searched LAST, after the shell, because it
104
- // updates when the app does and a real install is fresher.
105
- export function appCopyPaths(home = os.homedir(), readdir = defaultReaddir) {
106
- const dir = path.join(home, 'Library/Application Support/Claude/claude-code');
107
- return readdir(dir)
108
- .filter((v) => /^\d+(\.\d+)*$/.test(v))
109
- .sort(newestFirst)
110
- .map((v) => path.join(dir, v, 'claude.app/Contents/MacOS/claude'));
111
- }
112
-
113
- function newestFirst(a, b) {
114
- const x = a.split('.').map(Number), y = b.split('.').map(Number);
115
- for (let i = 0; i < Math.max(x.length, y.length); i++) {
116
- if ((x[i] ?? 0) !== (y[i] ?? 0)) return (y[i] ?? 0) - (x[i] ?? 0);
117
- }
118
- return 0;
119
- }
120
-
121
- // WHAT CLAUDE CODE LEAVES BEHIND. Any one of these means it has run on this
122
- // Mac. None of them is a binary and none of them is ever spawned; they exist
123
- // only to stop the app announcing an absence it cannot support.
124
- export function evidencePaths(home = os.homedir()) {
125
- return [
126
- path.join(home, '.claude.json'), // its own config file
127
- path.join(home, '.claude/settings.json'),
128
- path.join(home, '.claude/projects'), // one folder per project it has opened
129
- path.join(home, '.claude/statsig'),
130
- path.join(home, '.claude/.credentials.json'), // it has been logged in
131
- path.join(home, '.claude/history.jsonl'),
132
- path.join(home, '.local/share/claude'), // the installer's version store
133
- ];
134
- }
135
-
136
- // A path that is a symlink to nothing. `existsSync` follows the link and says
137
- // false, which is indistinguishable from nothing being there at all, and the
138
- // two mean opposite things: an update that has swapped the versioned file out
139
- // from under ~/.local/bin/claude leaves exactly this for a moment.
140
- function defaultDangles(p) {
141
- try { return !!fs.lstatSync(p) && !fs.existsSync(p); } catch { return false; }
142
- }
143
-
144
- export function installEvidence({ home = os.homedir(), exists = fs.existsSync, dangles = defaultDangles } = {}) {
145
- for (const p of evidencePaths(home)) if (exists(p)) return p;
146
- for (const p of candidatePaths(home)) if (dangles(p)) return p;
147
- return null;
148
- }
25
+ import {
26
+ ENGINES, findEngine, resolveEngine, forgetEngine, installEvidence as evidenceFor,
27
+ versionedBinPaths, askShell, shellProbes,
28
+ } from './engine-bin.mjs';
149
29
 
150
- export const INSTALL_URL = 'https://code.claude.com/docs/en/setup';
30
+ export { askShell, shellProbes };
151
31
 
152
- // The shells worth asking, in order, and never the same one twice. Hers first
153
- // because it is the one that has her PATH in it; the other two because a
154
- // $SHELL that will not start is a fact about $SHELL and not about Claude Code.
155
- export function shellProbes(env = process.env) {
156
- const preferred = env.SHELL || '/bin/zsh';
157
- const out = [[preferred, '-lic', 4000], [preferred, '-lc', 2500]];
158
- for (const other of ['/bin/zsh', '/bin/bash']) {
159
- if (other !== preferred) out.push([other, '-lic', 2500]);
160
- }
161
- return out;
162
- }
32
+ const CLAUDE = ENGINES.claude;
163
33
 
164
- // Ask the user's own login shell, and go on asking until one of them answers.
165
- // Anything that is not an absolute path to a file that exists is treated as no
166
- // path: `command -v` also prints shell functions and aliases, and a function
167
- // name handed to spawn is a crash with a worse message than "not found".
168
- //
169
- // `answered` is the strict half. It is true only when every probe that ran came
170
- // back with a real exit status, and at least one did. One shell that timed out
171
- // alongside one that said no is not a machine we know anything about.
172
- //
173
- // `bin` is a parameter rather than the word `claude` baked into the command
174
- // because Codex is a second engine (her ask, 2026-08-25) and it is found in the
175
- // same way: same shells, same "two different silences" rule, same refusal to
176
- // read a shell function name as a path. It is DEFAULTED, so every existing
177
- // caller and every existing test asks about Claude Code as it always did, and
178
- // that matters more here than it would anywhere else: this is the binary the
179
- // app will not open an inbox without.
180
- /**
181
- * WHAT MAY BE HANDED TO A LOGIN SHELL, AND WHY THIS IS CHECKED AT ALL.
182
- *
183
- * The probe below interpolates `bin` into a string a LOGIN, INTERACTIVE shell
184
- * executes, with her profile loaded, as her. Both callers today pass a literal
185
- * ('claude' here, 'codex' in main/codex-bin.mjs), so nothing is exploitable
186
- * now. But the entire direction of the second-engine work is a binary somebody
187
- * can configure, and a `codexBin` read out of zero.config.json reaching this
188
- * argument is one plausible slice away; at that point `command -v x; curl | sh`
189
- * is a string in a file that runs itself.
190
- *
191
- * This function looks up program NAMES, so a name is all it takes. An allowlist
192
- * rather than quoting, because quoting is a thing a later edit can undo without
193
- * noticing and a shape check is not.
194
- */
195
- const PROGRAM_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
34
+ export const INSTALL_URL = CLAUDE.installUrl;
196
35
 
197
- export function askShell({ env = process.env, exists = fs.existsSync, run = defaultRun, bin = 'claude' } = {}) {
198
- if (typeof bin !== 'string' || !PROGRAM_NAME.test(bin)) {
199
- throw new Error(`askShell looks up a program name, and ${JSON.stringify(bin)} is not one`);
200
- }
201
- let said = false;
202
- let inconclusive = false;
203
- for (const [shell, flags, timeout] of shellProbes(env)) {
204
- let out = '';
205
- try {
206
- out = run(shell, [flags, `command -v ${bin}`], timeout);
207
- } catch (e) {
208
- // TWO DIFFERENT SILENCES, AND ONLY ONE OF THEM IS AN ANSWER.A shell that
209
- // ran and exited non-zero has told us claude is not on PATH, and that is
210
- // an answer. A shell that could not be started, or that ran past the
211
- // timeout, has told us nothing at all, and reporting nothing as "not
212
- // installed" is what put a line about missing software on the screen of
213
- // people who have it.
214
- if (exitedSayingNo(e)) said = true; else inconclusive = true;
215
- continue;
216
- }
217
- const line = String(out).split('\n').map((s) => s.trim()).filter(Boolean).pop();
218
- if (line && line.startsWith('/') && exists(line)) return { path: line, answered: true };
219
- said = true;
220
- }
221
- return { path: null, answered: said && !inconclusive };
36
+ /** Every normal home for this binary, in order; the first that exists wins. */
37
+ export function candidatePaths(home = os.homedir()) {
38
+ return CLAUDE.candidates(home, () => []);
222
39
  }
223
40
 
224
- // The shell ran to completion and returned a non-zero exit code, which for
225
- // `command -v` is how "not on PATH" is spelt. A timeout (SIGTERM, ETIMEDOUT)
226
- // and a shell that does not exist (ENOENT) both arrive here too, and neither
227
- // of them is a fact about Claude Code.
228
- function exitedSayingNo(e) {
229
- if (!e || typeof e !== 'object') return false;
230
- if (e.signal || e.code === 'ETIMEDOUT' || e.code === 'ENOENT') return false;
231
- return typeof e.status === 'number';
41
+ /** One bin directory per installed Node, read rather than guessed. */
42
+ export function versionedPaths(home = os.homedir(), readdir = undefined) {
43
+ return versionedBinPaths('claude', home, readdir);
232
44
  }
233
45
 
234
- // askShell used to answer with a path or null, and both a real answer and a
235
- // failed probe were null. Anything still handing back the old shape is read as
236
- // an answer, because that is what it always meant.
237
- function shellSaid(result) {
238
- if (typeof result === 'string') return { path: result, answered: true };
239
- if (!result || typeof result !== 'object') return { path: null, answered: true };
240
- return { path: result.path ?? null, answered: result.answered !== false };
46
+ /** The copy the Claude desktop app keeps for itself, newest version first. */
47
+ export function appCopyPaths(home = os.homedir(), readdir = undefined) {
48
+ return CLAUDE.appCopies(home, readdir);
241
49
  }
242
50
 
243
- function defaultRun(shell, args, timeout = 4000) {
244
- return execFileSync(shell, args, {
245
- encoding: 'utf8',
246
- // stdin ignored on purpose: an interactive shell handed a live terminal can
247
- // sit waiting for input, and this runs on the boot path.
248
- stdio: ['ignore', 'pipe', 'ignore'],
249
- timeout,
250
- });
51
+ /** What Claude Code leaves behind. None of it is ever spawned. */
52
+ export function evidencePaths(home = os.homedir()) {
53
+ return CLAUDE.evidence(home, {});
251
54
  }
252
55
 
253
- // The whole search, as a pure function of its inputs so a test can put Claude
254
- // Code anywhere without touching the machine it runs on.
255
- //
256
- // Returns { path, found, certain, from, searched, evidence }. `found` is the
257
- // only claim about the world: it means a file is really there. `certain` says
258
- // whether we are allowed to tell somebody it is NOT there, so a screen can stay
259
- // quiet rather than announce an absence it has not established. They come apart
260
- // in exactly one case that is not a failure, and it is the case worth keeping
261
- // honest: she has set `claudeBin` to a path that does not exist, so the path
262
- // stays HERS (anything that spawns it should fail naming the path she chose,
263
- // not one we picked) while the screen says it was not found and offers the
264
- // install link, the same as any other machine.
265
- export function findClaudeBin({
266
- configured = null,
267
- home = os.homedir(),
268
- exists = fs.existsSync,
269
- shellLookup = askShell,
270
- readdir = defaultReaddir,
271
- dangles = defaultDangles,
272
- } = {}) {
273
- if (configured) {
274
- return {
275
- path: configured, found: exists(configured), certain: true, from: 'settings',
276
- searched: [], evidence: null,
277
- };
278
- }
279
-
280
- const searched = [...candidatePaths(home), ...versionedPaths(home, readdir)];
281
- for (const candidate of searched) {
282
- if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'disk', searched, evidence: null };
283
- }
284
-
285
- const fromShell = shellSaid(shellLookup());
286
- if (fromShell.path) return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
287
-
288
- for (const candidate of appCopyPaths(home, readdir)) {
289
- if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'app', searched, evidence: null };
290
- }
291
-
292
- // NOT FOUND, AND WHETHER THAT IS KNOWN. Every cheap path missed and every
293
- // shell we could reach missed, so the last question is whether this Mac shows
294
- // any sign of Claude Code anyway. If it does, we looked in the wrong places
295
- // rather than at an empty machine, and nothing may be said out loud.
296
- const evidence = installEvidence({ home, exists, dangles });
297
- return {
298
- path: null,
299
- found: false,
300
- certain: fromShell.answered && !evidence,
301
- from: null,
302
- searched,
303
- evidence,
304
- };
56
+ export function installEvidence({ home = os.homedir(), exists, dangles } = {}) {
57
+ return evidenceFor('claude', { home, env: {}, exists, dangles });
305
58
  }
306
59
 
307
- /* ------------------------------- the cache -------------------------------- */
308
- // Only the SHELL step is remembered, and only briefly. Nothing else is: the
309
- // path checks are stat calls, and caching them is how the settings screen ends
310
- // up telling someone to install Claude Code and then still saying "not found"
311
- // after they have. The sentence on that screen makes a promise and a cached
312
- // answer breaks it.
313
- //
314
- // The shell is the one part worth holding, because it is up to three
315
- // subprocesses and a screen she toggles a switch on reads settings again on
316
- // every write. Thirty seconds is long enough that a burst of writes pays for it
317
- // once, and short enough that it is never the reason someone is still stuck.
318
-
319
- const SHELL_TTL = 30_000;
320
- let shellAnswer = null;
321
- let shellAskedAt = 0;
322
-
323
- function rememberedShellLookup(now, lookup) {
324
- if (shellAskedAt && now - shellAskedAt < SHELL_TTL) return shellAnswer;
325
- shellAnswer = lookup();
326
- shellAskedAt = now;
327
- return shellAnswer;
60
+ /** The whole search. See main/engine-bin.mjs for what each answer means. */
61
+ export function findClaudeBin(where = {}) {
62
+ return findEngine('claude', where);
328
63
  }
329
64
 
330
- export function resolveClaudeBin(configured = null, { now = Date.now(), shellLookup = askShell, ...where } = {}) {
331
- return findClaudeBin({ ...where, configured, shellLookup: () => rememberedShellLookup(now, shellLookup) });
65
+ /** The same search with the shell step held for thirty seconds. */
66
+ export function resolveClaudeBin(configured = null, where = {}) {
67
+ return resolveEngine('claude', configured, where);
332
68
  }
333
69
 
334
70
  export function forgetClaudeBin() {
335
- shellAnswer = null;
336
- shellAskedAt = 0;
71
+ forgetEngine('claude');
337
72
  }
@@ -16,12 +16,18 @@ function turnClosed(message) {
16
16
  // before the session is closed anyway. Measured wakes arrive in about a second.
17
17
  const WAKE_GRACE_MS = 2 * 60 * 1000;
18
18
 
19
- export function claudeStreamArgs(args) {
19
+ // THE PROMPT LEAVES THE LINE WHEN REPLIES ARRIVE ON STDIN. Both the print flag
20
+ // and the input-format flags are read off the engine's `run` definition, so an
21
+ // engine of this family that spells either of them differently is a definition
22
+ // and not a change here. An engine with no `input` cannot take corrections on
23
+ // stdin, and gets its line back as it was.
24
+ export function streamInputArgs(run, args) {
25
+ if (!run?.input) return [...args];
20
26
  const copy = [...args];
21
- const index = copy.indexOf('-p');
27
+ const index = copy.indexOf(run.print);
22
28
  if (index < 0) throw Error('Missing print prompt');
23
29
  copy.splice(index + 1, 1);
24
- return [...copy, '--input-format', 'stream-json', '--replay-user-messages'];
30
+ return [...copy, ...run.input];
25
31
  }
26
32
 
27
33
  // Replay-user-messages acknowledges the exact UUID when Claude consumes it.
@@ -0,0 +1,54 @@
1
+ // An npm Claude Code from before the package shipped a native binary is a
2
+ // `#!/usr/bin/env node` script. Opened from the Dock, the app's PATH is
3
+ // launchd's four folders, and with nvm lazy-loaded no profile adds node, so the
4
+ // app found that Claude Code and every worker died with
5
+ // `env: node: No such file or directory`
6
+ // (tests/an-old-npm-claude-code-starts-as-a-worker-from-the-dock).
7
+ //
8
+ // Narrower than main/codex-launch-env.mjs on purpose. A worker's PATH is the
9
+ // PATH its agent's own commands run under, so it is changed only when this
10
+ // binary needs node and none is on it, and the node found is added at the end,
11
+ // where it cannot shadow anything the person's own PATH chose.
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { candidatePaths, versionedPaths } from './claude-bin.mjs';
16
+
17
+ function executable(file) {
18
+ try {
19
+ fs.accessSync(file, fs.constants.X_OK);
20
+ return fs.statSync(file).isFile();
21
+ } catch { return false; }
22
+ }
23
+
24
+ /** Whether `bin`, followed through its links, is a script run by node. */
25
+ function runsOnNode(bin) {
26
+ try {
27
+ const fd = fs.openSync(fs.realpathSync(bin), 'r');
28
+ const head = Buffer.alloc(128);
29
+ const read = fs.readSync(fd, head, 0, head.length, 0);
30
+ fs.closeSync(fd);
31
+ const first = head.subarray(0, read).toString('utf8').split('\n')[0];
32
+ return /^#!.*\bnode\b/.test(first);
33
+ } catch { return false; }
34
+ }
35
+
36
+ export function claudeLaunchEnv(bin, source = process.env, {
37
+ home = source.HOME || os.homedir(), exists = executable, readdir,
38
+ } = {}) {
39
+ if (!bin || !runsOnNode(bin)) return { ...source };
40
+ const inherited = (source.PATH || '').split(path.delimiter).filter(Boolean);
41
+ if (inherited.some(dir => exists(path.join(dir, 'node')))) return { ...source };
42
+ // The node npm installed it with sits beside the link, or beside the real
43
+ // file's package; then the places a Node install usually lives.
44
+ let real = bin;
45
+ try { real = fs.realpathSync(bin); } catch { /* keep the link */ }
46
+ const directories = [...new Set([
47
+ path.dirname(bin), path.dirname(real),
48
+ ...candidatePaths(home).map(path.dirname),
49
+ ...versionedPaths(home, readdir).map(path.dirname),
50
+ ])];
51
+ const nodeDir = directories.find(dir => exists(path.join(dir, 'node')));
52
+ if (!nodeDir) return { ...source };
53
+ return { ...source, PATH: [...inherited, nodeDir].join(path.delimiter) };
54
+ }
@@ -39,8 +39,35 @@
39
39
  // at all.
40
40
 
41
41
  import fs from 'node:fs';
42
+ import path from 'node:path';
43
+ import { fileURLToPath } from 'node:url';
42
44
  import { CLAUDE_MODELS, READ_FROM } from '../shared/claude-models.generated.mjs';
43
45
 
46
+ // WHAT THE LAST BUILD ON THIS MACHINE READ, which is a better floor than the
47
+ // committed table and is not a file anybody edits.
48
+ //
49
+ // The committed table is tracked, so a build that wrote over it made every
50
+ // folder that had run the app look like it held work (w-6bbb709b1f, 2026-10-07).
51
+ // The build writes this instead. It is gitignored, it is absent on a machine
52
+ // that has never built, and it is read with `fs` rather than imported, for the
53
+ // one reason that decides the whole shape: this module is node, but
54
+ // shared/claude-commands.mjs next door is shared with the renderer, where an
55
+ // import of a file that may not exist cannot be made to resolve. A read is a
56
+ // read in node and nothing at all in a bundle.
57
+ const LOCAL = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'shared', 'claude-models.local.json');
58
+
59
+ /** The table this machine read at build time, or null if there is none to trust. */
60
+ function readLocal(at) {
61
+ try {
62
+ const table = JSON.parse(fs.readFileSync(at, 'utf8'));
63
+ if (!Array.isArray(table.models) || !table.models.length) return null;
64
+ // Every row has to carry what the picker draws. A half-written file is a
65
+ // worse answer than the committed table, not a better one.
66
+ if (!table.models.every((m) => m && typeof m.alias === 'string' && typeof m.label === 'string')) return null;
67
+ return table;
68
+ } catch { return null; }
69
+ }
70
+
44
71
  /**
45
72
  * HER ORDER, AND ONLY HER ORDER. Not a filter: an alias missing from this list
46
73
  * is still offered, after these. Her three are from and Fable is the one she
@@ -203,8 +230,10 @@ function stampOf(binPath) {
203
230
  * WHAT MAY SHE BE OFFERED FOR CLAUDE CODE, and what version said so.
204
231
  *
205
232
  * `{ models: [{ alias, id, label }], readFrom, source }` where `source` is
206
- * `'cli'` when it came off this Mac's Claude Code and `'built'` when it is the
207
- * table committed at build time. Never empty: the committed table is the floor.
233
+ * `'cli'` when it came off this Mac's Claude Code at the moment it was asked,
234
+ * `'read'` when it is what the last build on this machine read out of it, and
235
+ * `'built'` when it is the table committed to the repository. Never empty: the
236
+ * committed table is the floor under both of them.
208
237
  *
209
238
  * THE PATH IS HANDED IN, NEVER SEARCHED FOR HERE. `config.claudeBin` is already
210
239
  * resolved once at startup (main/config.mjs), and `findClaudeBin` falls back to
@@ -216,15 +245,18 @@ function stampOf(binPath) {
216
245
  * `--version` so the Settings screen could say which one it read. That is one
217
246
  * `execFileSync` on the path a settings read takes, and no label on a screen is
218
247
  * worth a process started while she waits. `readFrom` is the committed table's
219
- * version, and it is meaningful only when `source` is `'built'`.
248
+ * version, and it is meaningful whenever `source` is not `'cli'`.
220
249
  */
221
- export function claudeModels({ bin = null } = {}) {
250
+ export function claudeModels({ bin = null, local = LOCAL } = {}) {
251
+ // THE FLOOR IS THE FRESHER OF THE TWO TABLES NOBODY HAS TO ASK FOR: what the
252
+ // last build on this machine read, and the one committed to the repository.
253
+ const read = readLocal(local);
222
254
  const built = {
223
- models: CLAUDE_MODELS.map((m) => ({
255
+ models: (read?.models ?? CLAUDE_MODELS).map((m) => ({
224
256
  alias: m.alias, id: m.id, label: m.label, defaultLevel: m.defaultLevel ?? null,
225
257
  })),
226
- readFrom: READ_FROM,
227
- source: 'built',
258
+ readFrom: read ? read.readFrom ?? null : READ_FROM,
259
+ source: read ? 'read' : 'built',
228
260
  };
229
261
  if (!bin) return built;
230
262
  const stamp = stampOf(bin);
@@ -71,6 +71,9 @@ export function planLabel(raw) {
71
71
  if (!s) return null;
72
72
  const bare = s.replace(/^default_/, '').replace(/^claude_/, '');
73
73
  if (!bare) return null;
74
+ // Claude's saved Enterprise tier includes an internal name. Keep the raw
75
+ // tier for capacity decisions, but show only the subscription name.
76
+ if (bare === 'raven_enterprise') return 'Enterprise';
74
77
  return bare
75
78
  .split('_')
76
79
  .filter(Boolean)
@@ -114,7 +117,15 @@ export function readPlan(profile, { home = os.homedir(), file = null } = {}) {
114
117
  const from = tier === account.organizationRateLimitTier
115
118
  ? 'org-tier'
116
119
  : tier === account.userRateLimitTier ? 'user-tier' : 'org-type';
117
- return { known: true, max: isMaxTier(tier), label: planLabel(tier), tier, from, file: at };
120
+ // A SUBSCRIPTION THAT HAS ENDED STILL NAMES ITS OLD TIER. Measured
121
+ // 2026-10-11 on a lapsed personal Max login: `billingType` had gone to
122
+ // "none" (a paying one says "stripe_subscription") while the tier still said
123
+ // `default_claude_max_20x`, so the page went on saying "Max 20x". Only the
124
+ // exact word counts; a file that names no billing at all is read as before.
125
+ if (account.billingType === 'none') {
126
+ return { known: true, ended: true, max: false, label: 'Subscription ended', tier, from, file: at };
127
+ }
128
+ return { known: true, ended: false, max: isMaxTier(tier), label: planLabel(tier), tier, from, file: at };
118
129
  }
119
130
 
120
131
  /**
@@ -142,7 +153,8 @@ export function readPlans(profiles, { home = os.homedir() } = {}) {
142
153
  * AND AN UNREADABLE ACCOUNT NEVER LOWERS IT. Somebody whose config file we
143
154
  * cannot parse gets exactly the app they had before this existed. */
144
155
  export function slotsForPlans(plans, fallback = 3) {
145
- const known = (plans ?? []).filter((p) => p.known);
156
+ // An ended subscription runs nothing, so it has no say in how many run.
157
+ const known = (plans ?? []).filter((p) => p.known && !p.ended);
146
158
  if (!known.length) return fallback;
147
159
  return known.every((p) => p.max) ? fallback : 1;
148
160
  }