agentbox-app 0.1.6

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 (302) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +176 -0
  3. package/bin/agentbox.mjs +103 -0
  4. package/briefs/adhd-mode.md +5 -0
  5. package/briefs/message-rules.md +95 -0
  6. package/briefs/worker.md +230 -0
  7. package/briefs/writing-rules.md +336 -0
  8. package/main/account-discovery.mjs +145 -0
  9. package/main/account-tooling.mjs +261 -0
  10. package/main/agent-activity.mjs +56 -0
  11. package/main/agent-files.mjs +422 -0
  12. package/main/agent-schedule.mjs +254 -0
  13. package/main/agent-sessions.mjs +435 -0
  14. package/main/agents.mjs +1095 -0
  15. package/main/analytics.mjs +206 -0
  16. package/main/answer-modes.mjs +206 -0
  17. package/main/approval-prompt-server.mjs +160 -0
  18. package/main/approvals.mjs +487 -0
  19. package/main/artifact-path.mjs +144 -0
  20. package/main/claude-bin.mjs +311 -0
  21. package/main/claude-input.mjs +117 -0
  22. package/main/claude-models.mjs +244 -0
  23. package/main/claude-plan.mjs +148 -0
  24. package/main/claude-usage.mjs +149 -0
  25. package/main/code-change.mjs +383 -0
  26. package/main/code-file.mjs +116 -0
  27. package/main/codex-account.mjs +146 -0
  28. package/main/codex-app-server.mjs +796 -0
  29. package/main/codex-approvals.mjs +712 -0
  30. package/main/codex-bin.mjs +261 -0
  31. package/main/codex-compaction.mjs +49 -0
  32. package/main/codex-models.mjs +194 -0
  33. package/main/codex-session.mjs +797 -0
  34. package/main/codex-threads.mjs +268 -0
  35. package/main/codex-usage-file.mjs +192 -0
  36. package/main/codex-usage.mjs +149 -0
  37. package/main/codex-watch.mjs +35 -0
  38. package/main/codex.mjs +757 -0
  39. package/main/config.mjs +345 -0
  40. package/main/copy-selection.mjs +55 -0
  41. package/main/crash-report.mjs +158 -0
  42. package/main/demo.mjs +254 -0
  43. package/main/dev-window.mjs +103 -0
  44. package/main/doc-file.mjs +99 -0
  45. package/main/doc-scheme.mjs +126 -0
  46. package/main/first-run.mjs +190 -0
  47. package/main/folders.mjs +91 -0
  48. package/main/fresh-user.mjs +120 -0
  49. package/main/git-change.mjs +323 -0
  50. package/main/img-scheme.mjs +90 -0
  51. package/main/instruction-settings.mjs +205 -0
  52. package/main/ipc.mjs +1558 -0
  53. package/main/live-replies.mjs +77 -0
  54. package/main/machine.mjs +113 -0
  55. package/main/main.mjs +972 -0
  56. package/main/menu.mjs +177 -0
  57. package/main/notify.mjs +155 -0
  58. package/main/open-source-file.mjs +8 -0
  59. package/main/project-identity.mjs +149 -0
  60. package/main/provider-commands.mjs +32 -0
  61. package/main/rail-note.mjs +74 -0
  62. package/main/repeats.mjs +261 -0
  63. package/main/row-label.mjs +185 -0
  64. package/main/screen-detail.mjs +44 -0
  65. package/main/serve.mjs +262 -0
  66. package/main/settings.mjs +912 -0
  67. package/main/staleness.mjs +76 -0
  68. package/main/store/chats.mjs +188 -0
  69. package/main/store/home.mjs +383 -0
  70. package/main/store/migrate.mjs +40 -0
  71. package/main/store/paths.mjs +82 -0
  72. package/main/store/pending-writes.mjs +122 -0
  73. package/main/store/project-lock.mjs +200 -0
  74. package/main/store/project.mjs +523 -0
  75. package/main/store/queues.mjs +31 -0
  76. package/main/store/work-items.mjs +299 -0
  77. package/main/store/write-queue.mjs +14 -0
  78. package/main/store-modules.mjs +23 -0
  79. package/main/store.mjs +1269 -0
  80. package/main/supervisor.mjs +6875 -0
  81. package/main/task-commands.mjs +161 -0
  82. package/main/task-folders-offthread.mjs +84 -0
  83. package/main/task-folders-thread.mjs +13 -0
  84. package/main/task-folders.mjs +409 -0
  85. package/main/task-remote-control.mjs +65 -0
  86. package/main/task-terminals.mjs +169 -0
  87. package/main/terminal-access.mjs +6 -0
  88. package/main/updater.mjs +240 -0
  89. package/main/zoom-keys.mjs +54 -0
  90. package/main/zoom-readout.mjs +20 -0
  91. package/mcp/agentbox-mcp.sh +34 -0
  92. package/mcp/core/account.mjs +77 -0
  93. package/mcp/core/chat.mjs +61 -0
  94. package/mcp/core/documents.mjs +238 -0
  95. package/mcp/core/page.mjs +152 -0
  96. package/mcp/core/products.mjs +110 -0
  97. package/mcp/core/skills.mjs +126 -0
  98. package/mcp/core/work.mjs +377 -0
  99. package/mcp/stdio.mjs +70 -0
  100. package/mcp/tools.mjs +257 -0
  101. package/package.json +186 -0
  102. package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
  103. package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
  104. package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
  105. package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
  106. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  107. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  108. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  109. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  110. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  111. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  112. package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
  113. package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
  114. package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
  115. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  116. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  117. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  118. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  119. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  120. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  121. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  122. package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
  123. package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
  124. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  125. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  126. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  127. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  128. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  129. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  130. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  131. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  132. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  133. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  134. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  135. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  136. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  137. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  138. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  139. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  140. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  141. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  142. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  143. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  144. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  145. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  146. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  147. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  148. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  149. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  150. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  151. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  152. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  153. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  154. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  155. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  156. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  157. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  158. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  159. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  160. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  161. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  162. package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
  163. package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  164. package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
  165. package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
  166. package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  167. package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
  168. package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
  169. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  170. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  171. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  172. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  173. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  174. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  175. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  176. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  177. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  178. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  179. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  180. package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
  181. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  182. package/renderer/dist/emulator/data/compression/README.md +11 -0
  183. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  184. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  185. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  186. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  187. package/renderer/dist/emulator/data/cores/README.md +22 -0
  188. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  189. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  190. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  191. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  192. package/renderer/dist/emulator/data/cores/package.json +65 -0
  193. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  194. package/renderer/dist/emulator/data/emulator.css +1614 -0
  195. package/renderer/dist/emulator/data/loader.js +170 -0
  196. package/renderer/dist/emulator/data/localization/README.md +63 -0
  197. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  198. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  199. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  200. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  201. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  202. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  203. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  204. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  205. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  206. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  207. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  208. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  209. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  210. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  211. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  212. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  213. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  214. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  215. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  216. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  217. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  218. package/renderer/dist/emulator/data/src/compression.js +142 -0
  219. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  220. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  221. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  222. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  223. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  224. package/renderer/dist/emulator/data/src/storage.js +125 -0
  225. package/renderer/dist/emulator/data/version.json +5 -0
  226. package/renderer/dist/emulator/host.js +299 -0
  227. package/renderer/dist/emulator/index.html +28 -0
  228. package/renderer/dist/emulator/provenance.json +6 -0
  229. package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
  230. package/renderer/dist/index.html +13 -0
  231. package/scripts/after-install.mjs +59 -0
  232. package/scripts/approval-server.sh +43 -0
  233. package/scripts/before-publish.mjs +133 -0
  234. package/shared/agent-import.d.mts +13 -0
  235. package/shared/agent-import.mjs +252 -0
  236. package/shared/agents.d.mts +190 -0
  237. package/shared/agents.mjs +937 -0
  238. package/shared/analytics-events.mjs +63 -0
  239. package/shared/answers.d.mts +12 -0
  240. package/shared/answers.mjs +80 -0
  241. package/shared/artifact-keys.d.mts +28 -0
  242. package/shared/artifact-keys.mjs +140 -0
  243. package/shared/bridge-map.mjs +128 -0
  244. package/shared/claude-commands.d.mts +20 -0
  245. package/shared/claude-commands.generated.mjs +149 -0
  246. package/shared/claude-commands.mjs +131 -0
  247. package/shared/claude-models.generated.d.mts +15 -0
  248. package/shared/claude-models.generated.mjs +26 -0
  249. package/shared/claude-usage.d.mts +16 -0
  250. package/shared/claude-usage.mjs +172 -0
  251. package/shared/codex-commands.d.mts +4 -0
  252. package/shared/codex-commands.mjs +17 -0
  253. package/shared/codex-import.d.mts +38 -0
  254. package/shared/codex-import.mjs +232 -0
  255. package/shared/codex-modes.mjs +159 -0
  256. package/shared/codex-usage.mjs +180 -0
  257. package/shared/contracts.mjs +412 -0
  258. package/shared/crash-scrub.mjs +182 -0
  259. package/shared/dashboard.mjs +481 -0
  260. package/shared/dead-run-trace.mjs +127 -0
  261. package/shared/demo-world.mjs +415 -0
  262. package/shared/effort-levels.d.mts +15 -0
  263. package/shared/effort-levels.mjs +76 -0
  264. package/shared/engines.d.mts +62 -0
  265. package/shared/engines.mjs +389 -0
  266. package/shared/first-run-examples.mjs +60 -0
  267. package/shared/first-run-line.mjs +149 -0
  268. package/shared/first-run-practice.d.mts +32 -0
  269. package/shared/first-run-practice.mjs +254 -0
  270. package/shared/first-run-shape.mjs +100 -0
  271. package/shared/fresh-user-home.mjs +189 -0
  272. package/shared/hook-failure.mjs +22 -0
  273. package/shared/install-id.mjs +39 -0
  274. package/shared/instruction-defaults.json +5 -0
  275. package/shared/library-folder.mjs +28 -0
  276. package/shared/notify-rules.mjs +120 -0
  277. package/shared/product-name.d.mts +6 -0
  278. package/shared/product-name.mjs +144 -0
  279. package/shared/project-folder-check.d.mts +22 -0
  280. package/shared/project-folder-check.mjs +152 -0
  281. package/shared/provider-commands.d.mts +8 -0
  282. package/shared/provider-commands.mjs +58 -0
  283. package/shared/rank.d.mts +19 -0
  284. package/shared/rank.mjs +146 -0
  285. package/shared/recovery.mjs +26 -0
  286. package/shared/referenced-files.d.mts +11 -0
  287. package/shared/referenced-files.mjs +122 -0
  288. package/shared/repeats.d.mts +45 -0
  289. package/shared/repeats.mjs +193 -0
  290. package/shared/schemes.mjs +33 -0
  291. package/shared/settings-terminal.d.mts +1 -0
  292. package/shared/settings-terminal.mjs +14 -0
  293. package/shared/side-build.mjs +57 -0
  294. package/shared/spawn-trouble.d.mts +8 -0
  295. package/shared/spawn-trouble.mjs +599 -0
  296. package/shared/terminal-state.d.mts +1 -0
  297. package/shared/terminal-state.mjs +58 -0
  298. package/shared/usage.d.mts +53 -0
  299. package/shared/usage.mjs +319 -0
  300. package/shared/work-items.mjs +561 -0
  301. package/shared/work-lines.mjs +360 -0
  302. package/worker-permissions.json +6 -0
@@ -0,0 +1,311 @@
1
+ // Where Claude Code actually is, on a machine that is not hers.
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.
8
+ //
9
+ // HER RULE, 2026-08-23, and it is the one this file exists to keep:
10
+ //
11
+ // So there are two different questions here and they must never be answered by
12
+ // the same value. `found` is whether a runnable file is really there, and it
13
+ // decides which path gets spawned. `certain` is whether we are entitled to SAY
14
+ // it is missing, and everything below exists to make `certain` hard to earn.
15
+ //
16
+ // Four steps before an absence is ever claimed, cheapest first.
17
+ //
18
+ // KNOWN PATHS. A filesystem check is instant, spawns nothing, and is the right
19
+ // answer for most machines. The list is every normal home for this binary: the
20
+ // installer's own directory, Claude Code's local install (what
21
+ // `claude migrate-installer` leaves behind), Homebrew on Apple Silicon and on
22
+ // Intel, MacPorts, and the default bin of every package manager that is not npm.
23
+ //
24
+ // VERSION MANAGER DIRECTORIES. nvm, fnm, n and asdf each keep one bin directory
25
+ // PER INSTALLED NODE, under a name nobody can guess, so they are read rather
26
+ // than listed. A global npm install under any of them lands there and nowhere
27
+ // else, and this is the case the shell step used to be the only cover for.
28
+ //
29
+ // THE SHELL. The catch-all for installs no list can enumerate: volta, bun, a
30
+ // custom npm prefix. It must be a LOGIN, INTERACTIVE shell, because those
31
+ // managers put themselves on PATH from `.zprofile` and `.zshrc` and nowhere
32
+ // else. And it is needed at all because a double-clicked Electron app inherits
33
+ // launchd's PATH, which is /usr/bin:/bin:/usr/sbin:/sbin and has never heard of
34
+ // any of them. That is the whole reason an npm install is obvious in a terminal
35
+ // and invisible to the app. It is asked more than once now, because ONE probe
36
+ // that fails to start is not a fact about anybody's Mac.
37
+ //
38
+ // EVIDENCE THAT IT LIVES HERE ANYWAY. Last, and it decides nothing about which
39
+ // binary to run: it only takes away our right to announce an absence. Claude
40
+ // Code leaves a home folder, a settings file and a projects directory behind
41
+ // the moment it is used once. If those are on this Mac then Claude Code is on
42
+ // this Mac, whatever our search of PATH did or did not manage, and the honest
43
+ // thing on the screen is silence rather than a sentence telling somebody to
44
+ // install software they are already running. A dangling `claude` symlink counts
45
+ // the same way, and it is a real state: the installer points ~/.local/bin/claude
46
+ // at a versioned file and swaps that file on every update.
47
+ //
48
+ // Nothing here overrides her file. An explicit `claudeBin` in zero.config.json
49
+ // wins untouched and is never searched around, because that file's first line
50
+ // is "all paths explicit, no discovery magic" and a search that second-guesses
51
+ // a setting she wrote is exactly that.
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
+ // In order. The first that exists wins, so the installer's path stays first and
59
+ // a machine that has always worked keeps resolving exactly as it did.
60
+ export function candidatePaths(home = os.homedir()) {
61
+ return [
62
+ path.join(home, '.local/bin/claude'), // the installer
63
+ path.join(home, '.claude/local/claude'), // claude migrate-installer
64
+ '/opt/homebrew/bin/claude', // Homebrew, Apple Silicon
65
+ '/usr/local/bin/claude', // Homebrew on Intel, and npm -g
66
+ path.join(home, '.bun/bin/claude'), // bun
67
+ path.join(home, '.volta/bin/claude'), // volta
68
+ path.join(home, '.asdf/shims/claude'), // asdf, through its shims
69
+ path.join(home, '.npm-global/bin/claude'), // the two npm prefixes people
70
+ path.join(home, '.npm-packages/bin/claude'), // actually write in a guide
71
+ path.join(home, '.yarn/bin/claude'), // yarn global
72
+ path.join(home, 'Library/pnpm/claude'), // pnpm's own bin on macOS
73
+ path.join(home, '.deno/bin/claude'), // deno
74
+ path.join(home, 'bin/claude'), // a hand-rolled ~/bin on PATH
75
+ '/opt/local/bin/claude', // MacPorts
76
+ '/usr/bin/claude', // packaged by something system-wide
77
+ ];
78
+ }
79
+
80
+ // One bin directory per installed Node, under a name no list can hold. Read
81
+ // rather than guessed, and a directory that is not there just contributes
82
+ // nothing. This is where a global npm install under nvm, fnm, n or asdf ends
83
+ // up, and until now only the shell could reach it.
84
+ export function versionedPaths(home = os.homedir(), readdir = defaultReaddir) {
85
+ const under = (dir, tail) => readdir(path.join(home, dir)).map((v) => path.join(home, dir, v, tail));
86
+ return [
87
+ ...under('.nvm/versions/node', 'bin/claude'),
88
+ ...under('.local/share/fnm/node-versions', 'installation/bin/claude'),
89
+ ...under('Library/Application Support/fnm/node-versions', 'installation/bin/claude'),
90
+ ...under('n/versions/node', 'bin/claude'),
91
+ ...under('.asdf/installs/nodejs', 'bin/claude'),
92
+ ];
93
+ }
94
+
95
+ function defaultReaddir(dir) {
96
+ try { return fs.readdirSync(dir); } catch { return []; }
97
+ }
98
+
99
+ // WHAT CLAUDE CODE LEAVES BEHIND. Any one of these means it has run on this
100
+ // Mac. None of them is a binary and none of them is ever spawned; they exist
101
+ // only to stop the app announcing an absence it cannot support.
102
+ export function evidencePaths(home = os.homedir()) {
103
+ return [
104
+ path.join(home, '.claude.json'), // its own config file
105
+ path.join(home, '.claude/settings.json'),
106
+ path.join(home, '.claude/projects'), // one folder per project it has opened
107
+ path.join(home, '.claude/statsig'),
108
+ path.join(home, '.claude/.credentials.json'), // it has been logged in
109
+ path.join(home, '.claude/history.jsonl'),
110
+ path.join(home, '.local/share/claude'), // the installer's version store
111
+ ];
112
+ }
113
+
114
+ // A path that is a symlink to nothing. `existsSync` follows the link and says
115
+ // false, which is indistinguishable from nothing being there at all, and the
116
+ // two mean opposite things: an update that has swapped the versioned file out
117
+ // from under ~/.local/bin/claude leaves exactly this for a moment.
118
+ function defaultDangles(p) {
119
+ try { return !!fs.lstatSync(p) && !fs.existsSync(p); } catch { return false; }
120
+ }
121
+
122
+ export function installEvidence({ home = os.homedir(), exists = fs.existsSync, dangles = defaultDangles } = {}) {
123
+ for (const p of evidencePaths(home)) if (exists(p)) return p;
124
+ for (const p of candidatePaths(home)) if (dangles(p)) return p;
125
+ return null;
126
+ }
127
+
128
+ export const INSTALL_URL = 'https://code.claude.com/docs/en/setup';
129
+
130
+ // The shells worth asking, in order, and never the same one twice. Hers first
131
+ // because it is the one that has her PATH in it; the other two because a
132
+ // $SHELL that will not start is a fact about $SHELL and not about Claude Code.
133
+ export function shellProbes(env = process.env) {
134
+ const preferred = env.SHELL || '/bin/zsh';
135
+ const out = [[preferred, '-lic', 4000], [preferred, '-lc', 2500]];
136
+ for (const other of ['/bin/zsh', '/bin/bash']) {
137
+ if (other !== preferred) out.push([other, '-lic', 2500]);
138
+ }
139
+ return out;
140
+ }
141
+
142
+ // Ask the user's own login shell, and go on asking until one of them answers.
143
+ // Anything that is not an absolute path to a file that exists is treated as no
144
+ // path: `command -v` also prints shell functions and aliases, and a function
145
+ // name handed to spawn is a crash with a worse message than "not found".
146
+ //
147
+ // `answered` is the strict half. It is true only when every probe that ran came
148
+ // back with a real exit status, and at least one did. One shell that timed out
149
+ // alongside one that said no is not a machine we know anything about.
150
+ //
151
+ // `bin` is a parameter rather than the word `claude` baked into the command
152
+ // because Codex is a second engine (her ask, 2026-08-25) and it is found in the
153
+ // same way: same shells, same "two different silences" rule, same refusal to
154
+ // read a shell function name as a path. It is DEFAULTED, so every existing
155
+ // caller and every existing test asks about Claude Code as it always did, and
156
+ // that matters more here than it would anywhere else: this is the binary the
157
+ // app will not open an inbox without.
158
+ /**
159
+ * WHAT MAY BE HANDED TO A LOGIN SHELL, AND WHY THIS IS CHECKED AT ALL.
160
+ *
161
+ * The probe below interpolates `bin` into a string a LOGIN, INTERACTIVE shell
162
+ * executes, with her profile loaded, as her. Both callers today pass a literal
163
+ * ('claude' here, 'codex' in main/codex-bin.mjs), so nothing is exploitable
164
+ * now. But the entire direction of the second-engine work is a binary somebody
165
+ * can configure, and a `codexBin` read out of zero.config.json reaching this
166
+ * argument is one plausible slice away; at that point `command -v x; curl | sh`
167
+ * is a string in a file that runs itself.
168
+ *
169
+ * This function looks up program NAMES, so a name is all it takes. An allowlist
170
+ * rather than quoting, because quoting is a thing a later edit can undo without
171
+ * noticing and a shape check is not.
172
+ */
173
+ const PROGRAM_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
174
+
175
+ export function askShell({ env = process.env, exists = fs.existsSync, run = defaultRun, bin = 'claude' } = {}) {
176
+ if (typeof bin !== 'string' || !PROGRAM_NAME.test(bin)) {
177
+ throw new Error(`askShell looks up a program name, and ${JSON.stringify(bin)} is not one`);
178
+ }
179
+ let said = false;
180
+ let inconclusive = false;
181
+ for (const [shell, flags, timeout] of shellProbes(env)) {
182
+ let out = '';
183
+ try {
184
+ out = run(shell, [flags, `command -v ${bin}`], timeout);
185
+ } catch (e) {
186
+ // TWO DIFFERENT SILENCES, AND ONLY ONE OF THEM IS AN ANSWER.A shell that
187
+ // ran and exited non-zero has told us claude is not on PATH, and that is
188
+ // an answer. A shell that could not be started, or that ran past the
189
+ // timeout, has told us nothing at all, and reporting nothing as "not
190
+ // installed" is what put a line about missing software on the screen of
191
+ // people who have it.
192
+ if (exitedSayingNo(e)) said = true; else inconclusive = true;
193
+ continue;
194
+ }
195
+ const line = String(out).split('\n').map((s) => s.trim()).filter(Boolean).pop();
196
+ if (line && line.startsWith('/') && exists(line)) return { path: line, answered: true };
197
+ said = true;
198
+ }
199
+ return { path: null, answered: said && !inconclusive };
200
+ }
201
+
202
+ // The shell ran to completion and returned a non-zero exit code, which for
203
+ // `command -v` is how "not on PATH" is spelt. A timeout (SIGTERM, ETIMEDOUT)
204
+ // and a shell that does not exist (ENOENT) both arrive here too, and neither
205
+ // of them is a fact about Claude Code.
206
+ function exitedSayingNo(e) {
207
+ if (!e || typeof e !== 'object') return false;
208
+ if (e.signal || e.code === 'ETIMEDOUT' || e.code === 'ENOENT') return false;
209
+ return typeof e.status === 'number';
210
+ }
211
+
212
+ // askShell used to answer with a path or null, and both a real answer and a
213
+ // failed probe were null. Anything still handing back the old shape is read as
214
+ // an answer, because that is what it always meant.
215
+ function shellSaid(result) {
216
+ if (typeof result === 'string') return { path: result, answered: true };
217
+ if (!result || typeof result !== 'object') return { path: null, answered: true };
218
+ return { path: result.path ?? null, answered: result.answered !== false };
219
+ }
220
+
221
+ function defaultRun(shell, args, timeout = 4000) {
222
+ return execFileSync(shell, args, {
223
+ encoding: 'utf8',
224
+ // stdin ignored on purpose: an interactive shell handed a live terminal can
225
+ // sit waiting for input, and this runs on the boot path.
226
+ stdio: ['ignore', 'pipe', 'ignore'],
227
+ timeout,
228
+ });
229
+ }
230
+
231
+ // The whole search, as a pure function of its inputs so a test can put Claude
232
+ // Code anywhere without touching the machine it runs on.
233
+ //
234
+ // Returns { path, found, certain, from, searched, evidence }. `found` is the
235
+ // only claim about the world: it means a file is really there. `certain` says
236
+ // whether we are allowed to tell somebody it is NOT there, so a screen can stay
237
+ // quiet rather than announce an absence it has not established. They come apart
238
+ // in exactly one case that is not a failure, and it is the case worth keeping
239
+ // honest: she has set `claudeBin` to a path that does not exist, so the path
240
+ // stays HERS (anything that spawns it should fail naming the path she chose,
241
+ // not one we picked) while the screen says it was not found and offers the
242
+ // install link, the same as any other machine.
243
+ export function findClaudeBin({
244
+ configured = null,
245
+ home = os.homedir(),
246
+ exists = fs.existsSync,
247
+ shellLookup = askShell,
248
+ readdir = defaultReaddir,
249
+ dangles = defaultDangles,
250
+ } = {}) {
251
+ if (configured) {
252
+ return {
253
+ path: configured, found: exists(configured), certain: true, from: 'settings',
254
+ searched: [], evidence: null,
255
+ };
256
+ }
257
+
258
+ const searched = [...candidatePaths(home), ...versionedPaths(home, readdir)];
259
+ for (const candidate of searched) {
260
+ if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'disk', searched, evidence: null };
261
+ }
262
+
263
+ const fromShell = shellSaid(shellLookup());
264
+ if (fromShell.path) return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
265
+
266
+ // NOT FOUND, AND WHETHER THAT IS KNOWN. Every cheap path missed and every
267
+ // shell we could reach missed, so the last question is whether this Mac shows
268
+ // any sign of Claude Code anyway. If it does, we looked in the wrong places
269
+ // rather than at an empty machine, and nothing may be said out loud.
270
+ const evidence = installEvidence({ home, exists, dangles });
271
+ return {
272
+ path: null,
273
+ found: false,
274
+ certain: fromShell.answered && !evidence,
275
+ from: null,
276
+ searched,
277
+ evidence,
278
+ };
279
+ }
280
+
281
+ /* ------------------------------- the cache -------------------------------- */
282
+ // Only the SHELL step is remembered, and only briefly. Nothing else is: the
283
+ // path checks are stat calls, and caching them is how the settings screen ends
284
+ // up telling someone to install Claude Code and then still saying "not found"
285
+ // after they have. The sentence on that screen makes a promise and a cached
286
+ // answer breaks it.
287
+ //
288
+ // The shell is the one part worth holding, because it is up to three
289
+ // subprocesses and a screen she toggles a switch on reads settings again on
290
+ // every write. Thirty seconds is long enough that a burst of writes pays for it
291
+ // once, and short enough that it is never the reason someone is still stuck.
292
+
293
+ const SHELL_TTL = 30_000;
294
+ let shellAnswer = null;
295
+ let shellAskedAt = 0;
296
+
297
+ function rememberedShellLookup(now, lookup) {
298
+ if (shellAskedAt && now - shellAskedAt < SHELL_TTL) return shellAnswer;
299
+ shellAnswer = lookup();
300
+ shellAskedAt = now;
301
+ return shellAnswer;
302
+ }
303
+
304
+ export function resolveClaudeBin(configured = null, { now = Date.now(), shellLookup = askShell, ...where } = {}) {
305
+ return findClaudeBin({ ...where, configured, shellLookup: () => rememberedShellLookup(now, shellLookup) });
306
+ }
307
+
308
+ export function forgetClaudeBin() {
309
+ shellAnswer = null;
310
+ shellAskedAt = 0;
311
+ }
@@ -0,0 +1,117 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ // THE SESSION STOPPED LISTENING BEFORE IT TOOK THIS MESSAGE. Not a refusal:
4
+ // the turn closed, or the process went, in the seconds between her pressing
5
+ // send and the line reaching it. `submitReply` saves such a message as an
6
+ // ordinary reply, which the next run picks up, instead of throwing it back
7
+ // at her with "The turn has ended" (w-12730c6506).
8
+ export const TURN_CLOSED = 'TURN_CLOSED';
9
+ function turnClosed(message) {
10
+ const error = Error(message);
11
+ error.code = TURN_CLOSED;
12
+ return error;
13
+ }
14
+
15
+ export function claudeStreamArgs(args) {
16
+ const copy = [...args];
17
+ const index = copy.indexOf('-p');
18
+ if (index < 0) throw Error('Missing print prompt');
19
+ copy.splice(index + 1, 1);
20
+ return [...copy, '--input-format', 'stream-json', '--replay-user-messages'];
21
+ }
22
+
23
+ // Replay-user-messages acknowledges the exact UUID when Claude consumes it.
24
+ // A result for earlier work must not close stdin over queued corrections.
25
+ export function attachClaudeInput(child) {
26
+ let closed = false, buffer = '', atResult = false, held = false;
27
+ const controls = new Map();
28
+ const seenUsers = new Set();
29
+ const pending = new Map();
30
+ const closeInput = () => { closed = true; child.stdin.end(); };
31
+ const fail = reason => {
32
+ closed = true;
33
+ for (const request of pending.values()) {
34
+ clearTimeout(request.timer);
35
+ request.reject(turnClosed(reason));
36
+ }
37
+ pending.clear();
38
+ for (const request of controls.values()) { clearTimeout(request.timer); request.reject(Error(reason)); }
39
+ controls.clear();
40
+ };
41
+ child.on('exit', () => fail('The agent exited before confirming this correction.'));
42
+ child.on('error', () => fail('The agent could not receive this correction.'));
43
+ child.stdin.on('error', () => fail('The agent input connection closed.'));
44
+ child.stdout.on('data', data => {
45
+ buffer += String(data);
46
+ let index;
47
+ while ((index = buffer.indexOf('\n')) >= 0) {
48
+ const line = buffer.slice(0, index);
49
+ buffer = buffer.slice(index + 1);
50
+ let event;
51
+ try { event = JSON.parse(line); } catch { continue; }
52
+ if (event.type === 'control_response') {
53
+ const response = event.response, request = controls.get(response?.request_id);
54
+ if (request) { controls.delete(response.request_id); clearTimeout(request.timer);
55
+ if (response.subtype === 'success') request.resolve(response.response ?? {});
56
+ else request.reject(Error(response.error || 'The provider rejected this operation.'));
57
+ }
58
+ }
59
+ if (event.type === 'system' && event.subtype === 'bridge_state') child.emit('bridge-state', event);
60
+ if (event.type === 'assistant') atResult = false;
61
+ if (event.type === 'user' && !event.parent_tool_use_id && typeof event.uuid === 'string' && !seenUsers.has(event.uuid)) {
62
+ seenUsers.add(event.uuid);
63
+ const content = event.message?.content;
64
+ const text = typeof content === 'string' ? content : Array.isArray(content) ? content.filter(b => b.type === 'text').map(b => b.text).join('\n') : '';
65
+ if (held && !pending.has(event.uuid) && text && !event.isMeta) child.emit('remote-user', event);
66
+ if (text) { atResult = false; child.emit('input-turn', event); }
67
+ }
68
+ if (event.type === 'user' && pending.has(event.uuid)) {
69
+ const request = pending.get(event.uuid);
70
+ pending.delete(event.uuid);
71
+ clearTimeout(request.timer);
72
+ atResult = false;
73
+ request.resolve({ accepted: true });
74
+ }
75
+ if (event.type === 'result') {
76
+ atResult = true;
77
+ if (!held && !pending.size) closeInput();
78
+ }
79
+ }
80
+ });
81
+ child.holdInput = value => {
82
+ if (closed && value) throw Error('This session has ended. Open remote control again to resume it.');
83
+ held = !!value;
84
+ if (!held && atResult && !pending.size) closeInput();
85
+ };
86
+ child.control = (request, timeoutMs = 30000) => new Promise((resolve, reject) => {
87
+ if (closed || !child.stdin.writable) return reject(Error('The agent connection is closed.'));
88
+ const request_id = randomUUID();
89
+ const timer = setTimeout(() => { controls.delete(request_id); reject(Error('The provider did not confirm the remote-control operation.')); }, timeoutMs);
90
+ timer.unref?.();
91
+ controls.set(request_id, { resolve, reject, timer });
92
+ try { child.stdin.write(JSON.stringify({ type:'control_request', request_id, request }) + '\n'); }
93
+ catch(error) { clearTimeout(timer); controls.delete(request_id); reject(error); }
94
+ });
95
+ // NO CLOCK ON THE ACKNOWLEDGEMENT. Claude replays a queued message at its next
96
+ // break, and a break can be minutes away: a worker inside one long command
97
+ // (a render poll, `sleep 12` fifty times) holds it until the command returns.
98
+ // This used to reject at 120s with "delivery is uncertain", which the
99
+ // renderer read as a failed send: it pulled her back to the row, put her
100
+ // words back in the box, and the agent then took the ORIGINAL anyway, so the
101
+ // one she re-sent arrived twice (w-d92559b84f: queued 22:21:00, taken
102
+ // 22:24:10, re-sent 22:24:34). Once the line is on stdin the CLI owns it, so
103
+ // the only real failures are the ones `fail` already hears: exit, error,
104
+ // stdin closing.
105
+ child.steer = text => new Promise((resolve, reject) => {
106
+ if (closed || !child.stdin.writable) return reject(turnClosed('The turn has ended. Send the message again to continue it.'));
107
+ const uuid = randomUUID();
108
+ pending.set(uuid, { resolve, reject });
109
+ try {
110
+ child.stdin.write(JSON.stringify({ type: 'user', uuid, message: { role: 'user', content: text }, parent_tool_use_id: null, session_id: '' }) + '\n');
111
+ } catch (error) {
112
+ pending.delete(uuid);
113
+ reject(turnClosed(error?.message || 'The agent input connection closed.'));
114
+ }
115
+ });
116
+ return child;
117
+ }
@@ -0,0 +1,244 @@
1
+ // WHICH MODELS CLAUDE CODE CAN BE ASKED FOR, READ OFF THIS MAC AT THE MOMENT
2
+ // IT IS ASKED, RATHER THAN AT THE MOMENT AGENTBOX WAS BUILT.
3
+ //
4
+ // WHAT WAS ALREADY RIGHT, AND IS NOT CHANGED HERE. Agentbox sends the CLI an
5
+ // ALIAS (`--model opus`), never a version, so the RUN is already on whatever
6
+ // Opus is on the day it spawns. Nothing about that needed fixing, and this file
7
+ // does not touch it.
8
+ //
9
+ // WHAT WAS WRONG IS THE WORD ON THE SCREEN. The picker's labels came out of
10
+ // `shared/claude-models.generated.mjs`, written by
11
+ // `scripts/read-claude-models.mjs` at BUILD time. That was built to stop a
12
+ // typed label drifting from the model that runs, and it does. But a table
13
+ // frozen at build time drifts from the model that runs in the other direction:
14
+ // she updates Claude Code, the alias starts resolving somewhere newer, and her
15
+ // picker keeps printing the old name until somebody rebuilds Agentbox. MEASURED
16
+ // 2026-09-22 and this is exactly what happened: her Claude Code 2.1.270
17
+ // resolves `opus` to `claude-opus-5`; 2.1.280, published the same week,
18
+ // resolves it to `claude-opus-5-5` ("Opus 5.5"). The build table could not have
19
+ // known.
20
+ //
21
+ // SO IT IS READ AT THE MOMENT IT IS ASKED FOR, which is the stance
22
+ // main/codex-models.mjs already takes beside this one for the same reason, and
23
+ // hers is the same question both times. One `statSync` decides whether the
24
+ // answer is still good; a `claude update` changes the binary's size and mtime,
25
+ // so the next read is a fresh one and her picker says the new name with no
26
+ // rebuild and no release.
27
+ //
28
+ // AND THE FAMILIES ARE READ TOO, NOT LISTED. The build script carries a typed
29
+ // `WANTED = ['opus','sonnet','haiku','fable']`, which is her order and is also
30
+ // the reason a brand new family could never appear on its own. Here, her four
31
+ // come first in her order and EVERY OTHER ALIAS THE CLI RESOLVES follows them.
32
+ // If Anthropic ships a fifth family tomorrow, it is in her picker the moment she
33
+ // updates Claude Code, labelled with Claude Code's own name for it.
34
+ //
35
+ // THE COMMITTED TABLE IS STILL THE FLOOR. A Mac with no Claude Code on it, or a
36
+ // Claude Code whose catalog this can no longer read, answers with the generated
37
+ // list rather than with nothing. An empty picker is a worse answer than a
38
+ // slightly old one, and it is the one failure that would stop her sending a task
39
+ // at all.
40
+
41
+ import fs from 'node:fs';
42
+ import { CLAUDE_MODELS, READ_FROM } from '../shared/claude-models.generated.mjs';
43
+
44
+ /**
45
+ * HER ORDER, AND ONLY HER ORDER. Not a filter: an alias missing from this list
46
+ * is still offered, after these. Her three are from and Fable is the one she
47
+ * added on. */
48
+ export const HER_ORDER = ['opus', 'sonnet', 'haiku', 'fable'];
49
+
50
+ // The binary is ~200 MB, so it is read in chunks with an overlap longer than the
51
+ // longest record we care about, exactly as scripts/read-claude-commands.mjs does.
52
+ const CHUNK = 8 * 1024 * 1024;
53
+ const OVERLAP = 64 * 1024;
54
+
55
+ /** The alias table: which model id each family word resolves to. */
56
+ export function readAliases(text) {
57
+ const m = text.match(/latest_per_family:\{([^}]*)\}/);
58
+ if (!m) return null;
59
+ const out = {};
60
+ for (const [, alias, id] of m[1].matchAll(/([a-z_]+):"([^"]+)"/g)) out[alias] = id;
61
+ return Object.keys(out).length ? out : null;
62
+ }
63
+
64
+ /**
65
+ * Every model in the catalog that carries a display name of its own, IN THE
66
+ * ORDER THE CATALOG LISTS THEM.
67
+ *
68
+ * The order is load-bearing now and it was not before. Her pick on this item is
69
+ * that the drawer names the model before the newest one as well, and "the one
70
+ * before" has to come from somewhere. Parsing a version out of the id cannot do
71
+ * it: the ids are not one shape, since `claude-3-5-haiku` and `claude-haiku-4-5`
72
+ * are both real and sit on either side of a naming change, so a comparator over
73
+ * them is a guess that will one day sort a newer model as older and quietly
74
+ * offer the wrong one.
75
+ *
76
+ * The catalog is already ascending within each family, and it is the same order
77
+ * Claude Code's own picker uses. MEASURED against 2.1.270 and 2.1.280 on
78
+ * 2026-09-22: haiku 3.5 then 4.5; sonnet 3.5, 3.7, 4, 4.5, 4.6, 5; opus 4, 4.1,
79
+ * 4.5, 4.6, 4.7, 4.8, 5, 5.5; fable 5 then 5.1. So "the one before" is the entry
80
+ * immediately before the alias's own, inside its family, and no version number
81
+ * is ever read by us.
82
+ */
83
+ export function readNames(text) {
84
+ const out = [];
85
+ for (const m of text.matchAll(/id:"(claude-[a-z0-9-]+)",family:"([a-z]+)",display_name:"([^"]+)"(.{0,1200}?)(?=id:"claude-|$)/gs)) {
86
+ const [, id, family, label, rest] = m;
87
+ out.push({ id, family, label, defaultLevel: rest.match(/default_effort:"([a-z]+)"/)?.[1] ?? null });
88
+ }
89
+ return out;
90
+ }
91
+
92
+ /** The catalog's own name for one id, or undefined if it names none. */
93
+ export const nameOf = (names, id) => names.find((n) => n.id === id)?.label;
94
+
95
+ // AND HOW HARD THAT MODEL THINKS WHEN NOBODY SAYS.
96
+ //
97
+ // The strip drew nothing lit because this app genuinely did not know the answer,
98
+ // and shared/effort-levels.mjs says so in as many words: "a level nobody picked
99
+ // is the engine's own choice for the model... and this app does not know what
100
+ // that is per model, so it does not claim to." That was true and it is not any
101
+ // more. Claude Code's catalog carries `default_effort` on each model, beside the
102
+ // display name this file already reads, exactly as Codex's cache carries
103
+ // `default_reasoning_level` (main/codex-models.mjs reads that one already).
104
+ //
105
+ // AND IT REALLY IS PER MODEL, which is why a single typed word would have been
106
+ // wrong. MEASURED out of 2.1.280 on 2026-09-22: Opus 5.5 medium, Opus 5 high,
107
+ // Opus 4.8 high, Opus 4.7 xhigh, Sonnet 5 high, Fable 5.1 high. So the word the
108
+ // strip lights changes when she changes model, and it is read rather than
109
+ // guessed, for the same reason the label beside it is.
110
+ //
111
+ // NULL IS A REAL ANSWER. A model the catalog gives no default_effort for, such
112
+ // as Haiku 4.5 in 2.1.280, lights nothing, which is exactly what the strip did
113
+ // before. Nothing here invents a level.
114
+
115
+ /**
116
+ * Walk the binary once for both halves of the catalog.
117
+ *
118
+ * IT STOPS EARLY ON PURPOSE. Claude Code keeps its catalog as one object
119
+ * literal, so the alias table and every display name sit inside a few hundred
120
+ * kilobytes of each other, and reading the remaining ~190 MB to find nothing is
121
+ * time she spends waiting for a picker to open. Measured on her Mac against
122
+ * 2.1.270: 82 ms for the whole file, 8 ms stopping at the alias table.
123
+ */
124
+ export function scan(binPath, { chunk = CHUNK } = {}) {
125
+ let aliases = null;
126
+ const names = [];
127
+ const seen = new Set();
128
+ const fd = fs.openSync(binPath, 'r');
129
+ try {
130
+ const size = fs.fstatSync(fd).size;
131
+ const buf = Buffer.alloc(chunk + OVERLAP);
132
+ for (let at = 0; at < size; at += chunk) {
133
+ const read = fs.readSync(fd, buf, 0, chunk + OVERLAP, at);
134
+ if (read <= 0) break;
135
+ const text = buf.toString('latin1', 0, read);
136
+ aliases = aliases ?? readAliases(text);
137
+ // The chunks overlap, so the same record is read twice at every seam and
138
+ // the second copy must not become a second row in a list whose ORDER is
139
+ // now what decides which model is offered as the previous one.
140
+ for (const m of readNames(text)) {
141
+ if (seen.has(m.id)) continue;
142
+ seen.add(m.id);
143
+ names.push(m);
144
+ }
145
+ // Both halves are in hand and they agree: there is nothing left to find.
146
+ if (aliases && Object.values(aliases).every((id) => seen.has(id))) break;
147
+ }
148
+ } finally {
149
+ fs.closeSync(fd);
150
+ }
151
+ return { aliases, names };
152
+ }
153
+
154
+ /* A family with only one model in the * catalog contributes one row, not a duplicate.
155
+ */
156
+ export function rowsFrom({ aliases, names }) {
157
+ if (!aliases) return null;
158
+ const order = [...HER_ORDER, ...Object.keys(aliases).filter((a) => !HER_ORDER.includes(a)).sort()];
159
+ const rows = [];
160
+ for (const alias of order) {
161
+ const id = aliases[alias];
162
+ const self = names.find((x) => x.id === id);
163
+ const label = self?.label;
164
+ if (!id || !label) continue;
165
+ // `defaultLevel` travels beside the name rather than as a list of its own,
166
+ // the way main/codex-models.mjs carries its models' levels: it is per model,
167
+ // so a table of its own could disagree with the row it belongs to.
168
+ rows.push({ alias, id, label, defaultLevel: self.defaultLevel ?? null });
169
+ // The entry immediately before this one inside its own family. The catalog
170
+ // is ascending, so that is the model she was on before the newest shipped.
171
+ const family = names.filter((n) => n.family === self.family);
172
+ const before = family[family.findIndex((n) => n.id === id) - 1];
173
+ if (before) {
174
+ rows.push({ alias: before.id, id: before.id, label: before.label, defaultLevel: before.defaultLevel ?? null });
175
+ }
176
+ }
177
+ return rows.length ? rows : null;
178
+ }
179
+
180
+ /* --------------------------------- caching -------------------------------- */
181
+ // KEYED ON THE BINARY ITSELF, NOT ON A CLOCK. main/codex-models.mjs refuses to
182
+ // cache at all, and says why: caching a cheap read is how a screen ends up still
183
+ // saying the old answer after the thing changed underneath it. Its read is one
184
+ // 199 KB parse. This one is a walk through a 200 MB file, which is not cheap
185
+ // enough to repeat on every settings read while she is typing, so it is cached
186
+ // against the one fact that decides whether the answer can still be right: the
187
+ // path, size and mtime of the binary that produced it. `claude update` replaces
188
+ // that file, so the very next read is a fresh one. There is no staleness window
189
+ // and no interval to tune.
190
+ let memo = null;
191
+
192
+ /** Exported for the tests, and for anything that has a reason to force a re-read. */
193
+ export function forget() { memo = null; }
194
+
195
+ function stampOf(binPath) {
196
+ try {
197
+ const s = fs.statSync(binPath);
198
+ return `${binPath}:${s.size}:${s.mtimeMs}`;
199
+ } catch { return null; }
200
+ }
201
+
202
+ /**
203
+ * WHAT MAY SHE BE OFFERED FOR CLAUDE CODE, and what version said so.
204
+ *
205
+ * `{ 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.
208
+ *
209
+ * THE PATH IS HANDED IN, NEVER SEARCHED FOR HERE. `config.claudeBin` is already
210
+ * resolved once at startup (main/config.mjs), and `findClaudeBin` falls back to
211
+ * asking a login shell when the known folders miss. main/codex-models.mjs states
212
+ * the rule this follows: a picker must never be the reason a shell is spawned
213
+ * while she is typing.
214
+ *
215
+ * AND NOTHING HERE SPAWNS ANYTHING EITHER. An earlier draft asked the CLI its
216
+ * `--version` so the Settings screen could say which one it read. That is one
217
+ * `execFileSync` on the path a settings read takes, and no label on a screen is
218
+ * worth a process started while she waits. `readFrom` is the committed table's
219
+ * version, and it is meaningful only when `source` is `'built'`.
220
+ */
221
+ export function claudeModels({ bin = null } = {}) {
222
+ const built = {
223
+ models: CLAUDE_MODELS.map((m) => ({
224
+ alias: m.alias, id: m.id, label: m.label, defaultLevel: m.defaultLevel ?? null,
225
+ })),
226
+ readFrom: READ_FROM,
227
+ source: 'built',
228
+ };
229
+ if (!bin) return built;
230
+ const stamp = stampOf(bin);
231
+ if (!stamp) return built;
232
+ if (memo?.stamp === stamp) return memo.value;
233
+ let rows = null;
234
+ try { rows = rowsFrom(scan(bin)); } catch { rows = null; }
235
+ // A CLI WE CANNOT READ IS NOT A REASON TO DRAW AN EMPTY PICKER, and it is not
236
+ // a reason to stop either: this is the labelling, not the spawn, and the alias
237
+ // Agentbox sends works whether or not this file could find the table it is in.
238
+ const value = rows ? { models: rows, readFrom: null, source: 'cli' } : built;
239
+ memo = { stamp, value };
240
+ return value;
241
+ }
242
+
243
+ /** Just the rows, for the callers that do not care where they came from. */
244
+ export function claudeModelRows(opts = {}) { return claudeModels(opts).models; }