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.
- package/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- 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; }
|