agentbox-app 0.1.12 → 0.1.16
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/README.md +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +311 -260
- package/main/agent-updates.mjs +9 -22
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +42 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-bin.mjs +45 -311
- package/main/claude-input.mjs +57 -7
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +64 -16
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +94 -59
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-bin.mjs +51 -249
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +246 -506
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +112 -11
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +83 -5
- package/main/codex.mjs +95 -363
- package/main/config.mjs +37 -5
- package/main/corner-tag.mjs +810 -0
- package/main/crash-report.mjs +26 -3
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +545 -0
- package/main/engine-setup.mjs +25 -24
- package/main/feedback.mjs +14 -31
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +105 -0
- package/main/harnesses/codex.mjs +72 -0
- package/main/harnesses/common.mjs +78 -0
- package/main/harnesses/instructions-file.mjs +116 -0
- package/main/harnesses/opencode.mjs +234 -0
- package/main/harnesses.mjs +65 -0
- package/main/importable-threads.mjs +96 -0
- package/main/ipc.mjs +520 -135
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/local-app-frames.mjs +82 -0
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +248 -37
- package/main/memory-gate-history.mjs +85 -20
- package/main/memory-gate-server.mjs +129 -11
- package/main/memory-gate.mjs +62 -8
- package/main/memory-log.mjs +124 -0
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +64 -5
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +571 -0
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +314 -0
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +18 -47
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/runs-on.mjs +32 -35
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +155 -9
- package/main/settings.mjs +364 -50
- package/main/shell-path.mjs +234 -0
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +134 -20
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +45 -11
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/start-hold.mjs +73 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +227 -53
- package/main/supervisor.mjs +3865 -699
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +897 -105
- package/main/task-terminals.mjs +147 -41
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/updater.mjs +71 -9
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +365 -0
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +30 -5
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
- package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-CbkyZOUH.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
- package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.js +7 -0
- package/renderer/dist/corner-tag.html +13 -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 +297 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/index.html +16 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/approval-server.sh +24 -5
- package/scripts/before-publish.mjs +12 -10
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +62 -34
- package/shared/agents.d.mts +1 -0
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +45 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-commands.generated.mjs +6 -4
- package/shared/claude-models.generated.mjs +6 -4
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +36 -0
- package/shared/corner-tag.mjs +396 -0
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +76 -300
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +111 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +31 -0
- package/shared/notify-rules.mjs +55 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +4 -1
- package/shared/rank.mjs +82 -19
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -13
- package/shared/runs-on.mjs +31 -52
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +24 -10
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +484 -30
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/terminal-input.d.mts +1 -0
- package/shared/terminal-input.mjs +30 -0
- package/shared/terminal-keys.d.mts +1 -0
- package/shared/terminal-keys.mjs +16 -0
- package/shared/terminal-size.d.mts +4 -0
- package/shared/terminal-size.mjs +29 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +12 -0
- package/shared/usage.mjs +56 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/where-it-runs-from.d.mts +43 -0
- package/shared/where-it-runs-from.mjs +155 -0
- package/shared/work-items.mjs +150 -29
- package/shared/work-lines.mjs +93 -245
- package/shared/xterm-invalid-range.d.mts +2 -0
- package/shared/xterm-invalid-range.mjs +58 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
- package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// An npm Claude Code from before the package shipped a native binary is a
|
|
2
|
+
// `#!/usr/bin/env node` script. Opened from the Dock, the app's PATH is
|
|
3
|
+
// launchd's four folders, and with nvm lazy-loaded no profile adds node, so the
|
|
4
|
+
// app found that Claude Code and every worker died with
|
|
5
|
+
// `env: node: No such file or directory`
|
|
6
|
+
// (tests/an-old-npm-claude-code-starts-as-a-worker-from-the-dock).
|
|
7
|
+
//
|
|
8
|
+
// Narrower than main/codex-launch-env.mjs on purpose. A worker's PATH is the
|
|
9
|
+
// PATH its agent's own commands run under, so it is changed only when this
|
|
10
|
+
// binary needs node and none is on it, and the node found is added at the end,
|
|
11
|
+
// where it cannot shadow anything the person's own PATH chose.
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { candidatePaths, versionedPaths } from './claude-bin.mjs';
|
|
16
|
+
|
|
17
|
+
function executable(file) {
|
|
18
|
+
try {
|
|
19
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
20
|
+
return fs.statSync(file).isFile();
|
|
21
|
+
} catch { return false; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Whether `bin`, followed through its links, is a script run by node. */
|
|
25
|
+
function runsOnNode(bin) {
|
|
26
|
+
try {
|
|
27
|
+
const fd = fs.openSync(fs.realpathSync(bin), 'r');
|
|
28
|
+
const head = Buffer.alloc(128);
|
|
29
|
+
const read = fs.readSync(fd, head, 0, head.length, 0);
|
|
30
|
+
fs.closeSync(fd);
|
|
31
|
+
const first = head.subarray(0, read).toString('utf8').split('\n')[0];
|
|
32
|
+
return /^#!.*\bnode\b/.test(first);
|
|
33
|
+
} catch { return false; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function claudeLaunchEnv(bin, source = process.env, {
|
|
37
|
+
home = source.HOME || os.homedir(), exists = executable, readdir,
|
|
38
|
+
} = {}) {
|
|
39
|
+
if (!bin || !runsOnNode(bin)) return { ...source };
|
|
40
|
+
const inherited = (source.PATH || '').split(path.delimiter).filter(Boolean);
|
|
41
|
+
if (inherited.some(dir => exists(path.join(dir, 'node')))) return { ...source };
|
|
42
|
+
// The node npm installed it with sits beside the link, or beside the real
|
|
43
|
+
// file's package; then the places a Node install usually lives.
|
|
44
|
+
let real = bin;
|
|
45
|
+
try { real = fs.realpathSync(bin); } catch { /* keep the link */ }
|
|
46
|
+
const directories = [...new Set([
|
|
47
|
+
path.dirname(bin), path.dirname(real),
|
|
48
|
+
...candidatePaths(home).map(path.dirname),
|
|
49
|
+
...versionedPaths(home, readdir).map(path.dirname),
|
|
50
|
+
])];
|
|
51
|
+
const nodeDir = directories.find(dir => exists(path.join(dir, 'node')));
|
|
52
|
+
if (!nodeDir) return { ...source };
|
|
53
|
+
return { ...source, PATH: [...inherited, nodeDir].join(path.delimiter) };
|
|
54
|
+
}
|
package/main/claude-models.mjs
CHANGED
|
@@ -39,8 +39,35 @@
|
|
|
39
39
|
// at all.
|
|
40
40
|
|
|
41
41
|
import fs from 'node:fs';
|
|
42
|
+
import path from 'node:path';
|
|
43
|
+
import { fileURLToPath } from 'node:url';
|
|
42
44
|
import { CLAUDE_MODELS, READ_FROM } from '../shared/claude-models.generated.mjs';
|
|
43
45
|
|
|
46
|
+
// WHAT THE LAST BUILD ON THIS MACHINE READ, which is a better floor than the
|
|
47
|
+
// committed table and is not a file anybody edits.
|
|
48
|
+
//
|
|
49
|
+
// The committed table is tracked, so a build that wrote over it made every
|
|
50
|
+
// folder that had run the app look like it held work (w-6bbb709b1f, 2026-10-07).
|
|
51
|
+
// The build writes this instead. It is gitignored, it is absent on a machine
|
|
52
|
+
// that has never built, and it is read with `fs` rather than imported, for the
|
|
53
|
+
// one reason that decides the whole shape: this module is node, but
|
|
54
|
+
// shared/claude-commands.mjs next door is shared with the renderer, where an
|
|
55
|
+
// import of a file that may not exist cannot be made to resolve. A read is a
|
|
56
|
+
// read in node and nothing at all in a bundle.
|
|
57
|
+
const LOCAL = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'shared', 'claude-models.local.json');
|
|
58
|
+
|
|
59
|
+
/** The table this machine read at build time, or null if there is none to trust. */
|
|
60
|
+
function readLocal(at) {
|
|
61
|
+
try {
|
|
62
|
+
const table = JSON.parse(fs.readFileSync(at, 'utf8'));
|
|
63
|
+
if (!Array.isArray(table.models) || !table.models.length) return null;
|
|
64
|
+
// Every row has to carry what the picker draws. A half-written file is a
|
|
65
|
+
// worse answer than the committed table, not a better one.
|
|
66
|
+
if (!table.models.every((m) => m && typeof m.alias === 'string' && typeof m.label === 'string')) return null;
|
|
67
|
+
return table;
|
|
68
|
+
} catch { return null; }
|
|
69
|
+
}
|
|
70
|
+
|
|
44
71
|
/**
|
|
45
72
|
* HER ORDER, AND ONLY HER ORDER. Not a filter: an alias missing from this list
|
|
46
73
|
* is still offered, after these. Her three are from and Fable is the one she
|
|
@@ -203,8 +230,10 @@ function stampOf(binPath) {
|
|
|
203
230
|
* WHAT MAY SHE BE OFFERED FOR CLAUDE CODE, and what version said so.
|
|
204
231
|
*
|
|
205
232
|
* `{ models: [{ alias, id, label }], readFrom, source }` where `source` is
|
|
206
|
-
* `'cli'` when it came off this Mac's Claude Code
|
|
207
|
-
*
|
|
233
|
+
* `'cli'` when it came off this Mac's Claude Code at the moment it was asked,
|
|
234
|
+
* `'read'` when it is what the last build on this machine read out of it, and
|
|
235
|
+
* `'built'` when it is the table committed to the repository. Never empty: the
|
|
236
|
+
* committed table is the floor under both of them.
|
|
208
237
|
*
|
|
209
238
|
* THE PATH IS HANDED IN, NEVER SEARCHED FOR HERE. `config.claudeBin` is already
|
|
210
239
|
* resolved once at startup (main/config.mjs), and `findClaudeBin` falls back to
|
|
@@ -216,15 +245,18 @@ function stampOf(binPath) {
|
|
|
216
245
|
* `--version` so the Settings screen could say which one it read. That is one
|
|
217
246
|
* `execFileSync` on the path a settings read takes, and no label on a screen is
|
|
218
247
|
* worth a process started while she waits. `readFrom` is the committed table's
|
|
219
|
-
* version, and it is meaningful
|
|
248
|
+
* version, and it is meaningful whenever `source` is not `'cli'`.
|
|
220
249
|
*/
|
|
221
|
-
export function claudeModels({ bin = null } = {}) {
|
|
250
|
+
export function claudeModels({ bin = null, local = LOCAL } = {}) {
|
|
251
|
+
// THE FLOOR IS THE FRESHER OF THE TWO TABLES NOBODY HAS TO ASK FOR: what the
|
|
252
|
+
// last build on this machine read, and the one committed to the repository.
|
|
253
|
+
const read = readLocal(local);
|
|
222
254
|
const built = {
|
|
223
|
-
models: CLAUDE_MODELS.map((m) => ({
|
|
255
|
+
models: (read?.models ?? CLAUDE_MODELS).map((m) => ({
|
|
224
256
|
alias: m.alias, id: m.id, label: m.label, defaultLevel: m.defaultLevel ?? null,
|
|
225
257
|
})),
|
|
226
|
-
readFrom: READ_FROM,
|
|
227
|
-
source: 'built',
|
|
258
|
+
readFrom: read ? read.readFrom ?? null : READ_FROM,
|
|
259
|
+
source: read ? 'read' : 'built',
|
|
228
260
|
};
|
|
229
261
|
if (!bin) return built;
|
|
230
262
|
const stamp = stampOf(bin);
|
package/main/claude-plan.mjs
CHANGED
|
@@ -71,6 +71,9 @@ export function planLabel(raw) {
|
|
|
71
71
|
if (!s) return null;
|
|
72
72
|
const bare = s.replace(/^default_/, '').replace(/^claude_/, '');
|
|
73
73
|
if (!bare) return null;
|
|
74
|
+
// Claude's saved Enterprise tier includes an internal name. Keep the raw
|
|
75
|
+
// tier for capacity decisions, but show only the subscription name.
|
|
76
|
+
if (bare === 'raven_enterprise') return 'Enterprise';
|
|
74
77
|
return bare
|
|
75
78
|
.split('_')
|
|
76
79
|
.filter(Boolean)
|
|
@@ -93,27 +96,35 @@ export function isMaxTier(raw) {
|
|
|
93
96
|
* Claude Code has not fetched a profile for yet: all of them come back
|
|
94
97
|
* `known: false`, which every caller here treats as "leave everything exactly
|
|
95
98
|
* as it was". */
|
|
99
|
+
const planCache = new Map();
|
|
100
|
+
|
|
96
101
|
export function readPlan(profile, { home = os.homedir(), file = null } = {}) {
|
|
97
102
|
const at = file ?? planFile(profile, home);
|
|
103
|
+
// Keyed on the file's mtime AND its size, as accountIdentity is keyed on the
|
|
104
|
+
// mtime alone: the cloud tiles ask on every snapshot.
|
|
105
|
+
//
|
|
106
|
+
// THE SIZE IS THERE BECAUSE AN MTIME CAN STAND STILL (w-dac993a554). Two
|
|
107
|
+
// writes inside one tick of the filesystem's clock carry the same mtime, and
|
|
108
|
+
// the cache then served the first one's plan for ever. A subscription
|
|
109
|
+
// renewed in that window read as ended until the file was written again, so
|
|
110
|
+
// `Supervisor#_forgetRenewedSubscriptions` never let the account back in.
|
|
111
|
+
let stat = null;
|
|
112
|
+
try { stat = fs.statSync(at); } catch { /* read below says none */ }
|
|
113
|
+
const hit = planCache.get(at);
|
|
114
|
+
if (stat && hit?.mtime === stat.mtimeMs && hit?.size === stat.size) return hit.plan;
|
|
115
|
+
const plan = readPlanNow(at);
|
|
116
|
+
if (stat) planCache.set(at, { mtime: stat.mtimeMs, size: stat.size, plan });
|
|
117
|
+
return plan;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readPlanNow(at) {
|
|
121
|
+
const none = { known: false, max: false, label: null, tier: null, from: null, file: at };
|
|
98
122
|
let account = null;
|
|
99
123
|
try {
|
|
100
124
|
account = JSON.parse(fs.readFileSync(at, 'utf8'))?.oauthAccount ?? null;
|
|
101
125
|
} catch {
|
|
102
|
-
return
|
|
126
|
+
return none;
|
|
103
127
|
}
|
|
104
|
-
return planOfAccount(account, at);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* THE SAME ANSWER OFF AN `oauthAccount` SOMEBODY ALREADY HAS IN HAND.
|
|
109
|
-
*
|
|
110
|
-
* Lifted out of `readPlan` so a caller that has read the file for its own
|
|
111
|
-
* reasons (main/runs-on.mjs, which also wants to know whether there is a login
|
|
112
|
-
* in there at all) does not carry a second copy of the tier ordering. One copy
|
|
113
|
-
* of that rule, and the file read stays in one place per caller.
|
|
114
|
-
*/
|
|
115
|
-
export function planOfAccount(account, at = null) {
|
|
116
|
-
const none = { known: false, max: false, label: null, tier: null, from: null, file: at };
|
|
117
128
|
if (!account || typeof account !== 'object') return none;
|
|
118
129
|
// THE ORDER IS MOST SPECIFIC FIRST. A rate limit tier names the plan AND its
|
|
119
130
|
// size (`max_20x` against `max_5x`), which is the thing worth showing; the
|
|
@@ -126,7 +137,43 @@ export function planOfAccount(account, at = null) {
|
|
|
126
137
|
const from = tier === account.organizationRateLimitTier
|
|
127
138
|
? 'org-tier'
|
|
128
139
|
: tier === account.userRateLimitTier ? 'user-tier' : 'org-type';
|
|
129
|
-
|
|
140
|
+
// A SUBSCRIPTION THAT HAS ENDED STILL NAMES ITS OLD TIER. Measured
|
|
141
|
+
// 2026-10-11 on a lapsed personal Max login: `billingType` had gone to
|
|
142
|
+
// "none" (a paying one says "stripe_subscription") while the tier still said
|
|
143
|
+
// `default_claude_max_20x`, so the page went on saying "Max 20x". Only the
|
|
144
|
+
// exact word counts; a file that names no billing at all is read as before.
|
|
145
|
+
if (account.billingType === 'none') {
|
|
146
|
+
return { known: true, ended: true, max: false, label: 'Subscription ended', tier, from, file: at };
|
|
147
|
+
}
|
|
148
|
+
return { known: true, ended: false, max: isMaxTier(tier), label: planLabel(tier), tier, from, file: at };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* WHICH ACCOUNT AND PLAN ONE LOGIN IS ON, as a string that changes only when
|
|
153
|
+
* one of them does: an upgrade, extra usage switched on, a renewal, or a
|
|
154
|
+
* /login as somebody else. Claude Code rewrites this file all day for other
|
|
155
|
+
* reasons, so only these fields count (named as Claude Code 2.1.305 writes
|
|
156
|
+
* them). Null when the file names no account or cannot be read.
|
|
157
|
+
*/
|
|
158
|
+
const PLAN_FIELDS = ['accountUuid', 'organizationUuid', 'organizationType', 'organizationRateLimitTier',
|
|
159
|
+
'userRateLimitTier', 'seatTier', 'billingType', 'hasExtraUsageEnabled', 'planDisplayName', 'subscriptionCreatedAt'];
|
|
160
|
+
const stampCache = new Map();
|
|
161
|
+
|
|
162
|
+
export function planStamp(profile, { home = os.homedir(), file = null } = {}) {
|
|
163
|
+
const at = file ?? planFile(profile, home);
|
|
164
|
+
let mtime;
|
|
165
|
+
try { mtime = fs.statSync(at).mtimeMs; } catch { return null; }
|
|
166
|
+
const hit = stampCache.get(at);
|
|
167
|
+
if (hit?.mtime === mtime) return hit.stamp;
|
|
168
|
+
let stamp = null;
|
|
169
|
+
try {
|
|
170
|
+
const account = JSON.parse(fs.readFileSync(at, 'utf8'))?.oauthAccount;
|
|
171
|
+
if (account?.accountUuid) {
|
|
172
|
+
stamp = JSON.stringify(PLAN_FIELDS.map((k) => (k === 'hasExtraUsageEnabled' ? account[k] === true : account[k] ?? null)));
|
|
173
|
+
}
|
|
174
|
+
} catch { /* half written: the next write has a new mtime and is read again */ }
|
|
175
|
+
stampCache.set(at, { mtime, stamp });
|
|
176
|
+
return stamp;
|
|
130
177
|
}
|
|
131
178
|
|
|
132
179
|
/**
|
|
@@ -154,7 +201,8 @@ export function readPlans(profiles, { home = os.homedir() } = {}) {
|
|
|
154
201
|
* AND AN UNREADABLE ACCOUNT NEVER LOWERS IT. Somebody whose config file we
|
|
155
202
|
* cannot parse gets exactly the app they had before this existed. */
|
|
156
203
|
export function slotsForPlans(plans, fallback = 3) {
|
|
157
|
-
|
|
204
|
+
// An ended subscription runs nothing, so it has no say in how many run.
|
|
205
|
+
const known = (plans ?? []).filter((p) => p.known && !p.ended);
|
|
158
206
|
if (!known.length) return fallback;
|
|
159
207
|
return known.every((p) => p.max) ? fallback : 1;
|
|
160
208
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Whether Claude Code still has a session open somewhere, read off the state
|
|
2
|
+
// file it keeps for each open session (`<home>/sessions/<pid>.json`, with the
|
|
3
|
+
// session id and the pid holding it). A state file whose process has gone is a
|
|
4
|
+
// session somebody closed.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
function alive(pid) {
|
|
9
|
+
try { process.kill(pid, 0); return true; } catch (err) { return err?.code === 'EPERM'; }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function claudeSessionOpen(sessionId, homes = []) {
|
|
13
|
+
const id = String(sessionId ?? '').trim();
|
|
14
|
+
if (!id) return false;
|
|
15
|
+
for (const home of new Set(homes.filter(Boolean))) {
|
|
16
|
+
const dir = path.join(home, 'sessions');
|
|
17
|
+
let names = [];
|
|
18
|
+
try { names = fs.readdirSync(dir); } catch { continue; }
|
|
19
|
+
for (const name of names) {
|
|
20
|
+
if (!name.endsWith('.json')) continue;
|
|
21
|
+
let rec = null;
|
|
22
|
+
try { rec = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')); } catch { continue; }
|
|
23
|
+
const pid = Number(rec?.pid);
|
|
24
|
+
if (rec?.sessionId === id && pid > 0 && alive(pid)) return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { attachClaudeInput } from './claude-input.mjs';
|
|
3
|
+
|
|
4
|
+
// No user message is submitted. The CLI owns authentication and the provider request.
|
|
5
|
+
export function runClaudeUsage(bin, args, opts, done) {
|
|
6
|
+
let child, timer, killTimer, finished = false, bytes = 0;
|
|
7
|
+
const finish = (error, report = '') => {
|
|
8
|
+
if (finished) return;
|
|
9
|
+
finished = true;
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
if (child?.exitCode == null && child?.pid) {
|
|
12
|
+
child.kill('SIGTERM');
|
|
13
|
+
killTimer = setTimeout(() => child.kill('SIGKILL'), 1000);
|
|
14
|
+
killTimer.unref();
|
|
15
|
+
}
|
|
16
|
+
done(error, report);
|
|
17
|
+
};
|
|
18
|
+
try {
|
|
19
|
+
child = attachClaudeInput(spawn(bin, args, { cwd: opts.cwd, env: opts.env, stdio: ['pipe', 'pipe', 'ignore'] }));
|
|
20
|
+
child.holdInput(true);
|
|
21
|
+
child.once('error', error => finish(error));
|
|
22
|
+
child.once('exit', () => {
|
|
23
|
+
clearTimeout(killTimer);
|
|
24
|
+
finish(Error('Claude Code closed before reporting usage.'));
|
|
25
|
+
});
|
|
26
|
+
child.stdout.on('data', data => {
|
|
27
|
+
bytes += data.length;
|
|
28
|
+
if (bytes > opts.maxBuffer) finish(Error('Claude Code sent too much usage output.'));
|
|
29
|
+
});
|
|
30
|
+
timer = setTimeout(() => finish(Error('Claude Code did not report usage in time.')), opts.timeout);
|
|
31
|
+
void (async () => {
|
|
32
|
+
try {
|
|
33
|
+
await child.control({ subtype: 'initialize' }, opts.timeout);
|
|
34
|
+
const report = await child.control({ subtype: 'get_usage', skip_behaviors: true }, opts.timeout);
|
|
35
|
+
finish(null, JSON.stringify(report));
|
|
36
|
+
} catch (error) { finish(error); }
|
|
37
|
+
})();
|
|
38
|
+
} catch (error) { finish(error); }
|
|
39
|
+
return child;
|
|
40
|
+
}
|
package/main/claude-usage.mjs
CHANGED
|
@@ -1,39 +1,13 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// The pure half is shared/claude-usage.mjs: the text in, the limits out, the
|
|
4
|
-
// words the pill prints. This is the half that shells out, and everything about
|
|
5
|
-
// its shape comes from one measurement.
|
|
6
|
-
//
|
|
7
|
-
// TIMED TWICE ON HER MAC, 2026-08-31: 23.2s and 28.0s for `claude -p '/usage'`.
|
|
8
|
-
// The percentage did not move between the two, which is the other half of the
|
|
9
|
-
// answer: this is not a number that changes second to second, so nothing is lost
|
|
10
|
-
// by reading it every few minutes and everything is lost by reading it on a
|
|
11
|
-
// timer the window waits for.
|
|
12
|
-
//
|
|
13
|
-
// SO IT IS A CACHE WITH A BACKGROUND REFRESH, and never a call anything awaits:
|
|
14
|
-
//
|
|
15
|
-
// - `read` answers instantly, always, from the last measurement or with null.
|
|
16
|
-
// It never blocks and never throws.
|
|
17
|
-
// - A refresh is kicked off when the cached figure is older than STALE_MS,
|
|
18
|
-
// and only ever one at a time.
|
|
19
|
-
// - The window is told when a new one lands, the same way everything else in
|
|
20
|
-
// this app tells it: a push.
|
|
21
|
-
//
|
|
22
|
-
// WHY NOT A SESSION. Agentbox spawns real workers through the supervisor and could
|
|
23
|
-
// have asked one of them. It must not: a worker is a unit of her work, it lands
|
|
24
|
-
// in her inbox and burns a slot in the fleet, and this is a status reading. It
|
|
25
|
-
// gets its own short-lived child process with no store, no brief and no row.
|
|
26
|
-
//
|
|
27
|
-
// WHAT IS NOT SENT ANYWHERE. The output carries her usage figures and nothing
|
|
28
|
-
// else leaves this machine because of it; `main/analytics.mjs` is the only file
|
|
29
|
-
// that sends, and none of its eight counts is fed from here.
|
|
1
|
+
// Cached provider usage, read through Claude Code without starting a model turn.
|
|
2
|
+
// Snapshots never wait; only an explicit refresh waits for the CLI's answer.
|
|
30
3
|
|
|
31
|
-
import { execFile } from 'node:child_process';
|
|
32
4
|
import fs from 'node:fs';
|
|
33
5
|
import os from 'node:os';
|
|
34
6
|
import path from 'node:path';
|
|
35
|
-
import {
|
|
36
|
-
import { localZone } from '../shared/usage.mjs';
|
|
7
|
+
import { readUsageReport } from '../shared/claude-usage.mjs';
|
|
8
|
+
import { localZone, USAGE_UNREADABLE } from '../shared/usage.mjs';
|
|
9
|
+
import { runClaudeUsage } from './claude-usage-process.mjs';
|
|
10
|
+
import { NO_METERED_KEY } from './harnesses/claude.mjs';
|
|
37
11
|
|
|
38
12
|
// WHERE THE READING RUNS, AND WHY IT IS NOT "WHEREVER THE APP IS". An app
|
|
39
13
|
// opened from Finder has `/` as its working directory, and this used to spawn
|
|
@@ -61,9 +35,7 @@ export function usageCwd() {
|
|
|
61
35
|
// than the number itself is reported to.
|
|
62
36
|
export const STALE_MS = 5 * 60_000;
|
|
63
37
|
|
|
64
|
-
|
|
65
|
-
// this the reading is not late, it is not coming.
|
|
66
|
-
const TIMEOUT_MS = 90_000;
|
|
38
|
+
const TIMEOUT_MS = 30_000;
|
|
67
39
|
|
|
68
40
|
// After a failure, how long before it is worth trying again. A machine that is
|
|
69
41
|
// signed out, rate limited, or has no Claude Code at all would otherwise be
|
|
@@ -76,20 +48,36 @@ export class ClaudeUsage {
|
|
|
76
48
|
* is, asked each time rather than captured: it can be installed while the
|
|
77
49
|
* app is running, and the settings screen already promises that works.
|
|
78
50
|
* @param { => void} [how.onChange] Told when a new reading lands, so the
|
|
79
|
-
* window redraws
|
|
51
|
+
* window redraws, including a new checked time on unchanged percentages.
|
|
52
|
+
* @param { => { profile: string, env?: object }} [how.account] WHOSE LIMIT
|
|
53
|
+
* IS READ, asked each time. It used to be nobody's in particular: the
|
|
54
|
+
* command ran with the app's own environment, which is the default login,
|
|
55
|
+
* so a Mac with a second account picked as In use showed "No usage reading
|
|
56
|
+
* yet" for good, because the default one was at its limit and Claude Code
|
|
57
|
+
* printed no limits for it (2026-10-08, Claude Code 2.1.293). `profile` is
|
|
58
|
+
* the account's name ('default' or its CLAUDE_CONFIG_DIR) and `env` is the
|
|
59
|
+
* environment to run it in. Supervisor.usageAccount is the real one.
|
|
80
60
|
*/
|
|
81
|
-
constructor({ bin, onChange = () => {}, run =
|
|
61
|
+
constructor({ bin, onChange = () => {}, run = runClaudeUsage, now = () => Date.now(), where = usageCwd, account = () => ({ profile: 'default' }) } = {}) {
|
|
82
62
|
this._bin = bin;
|
|
63
|
+
this._selectedProfile = null;
|
|
64
|
+
this._generation = 0;
|
|
65
|
+
this._pending = null;
|
|
83
66
|
this._onChange = onChange;
|
|
84
67
|
this._run = run;
|
|
85
68
|
this._now = now;
|
|
86
69
|
this._where = where;
|
|
87
|
-
|
|
70
|
+
this._account = account;
|
|
71
|
+
/** @type {{ limits: any[], at: number, zone: string|null, profile: string }|null} */
|
|
88
72
|
this._last = null;
|
|
89
73
|
this._busy = false;
|
|
90
74
|
this._missedUntil = 0;
|
|
91
75
|
}
|
|
92
76
|
|
|
77
|
+
_who() {
|
|
78
|
+
try { return this._account() ?? { profile: 'default' }; } catch { return { profile: 'default' }; }
|
|
79
|
+
}
|
|
80
|
+
|
|
93
81
|
/**
|
|
94
82
|
* THE LAST READING, INSTANTLY, and a refresh started if it has gone stale.
|
|
95
83
|
*
|
|
@@ -98,13 +86,28 @@ export class ClaudeUsage {
|
|
|
98
86
|
* than a pill full of dashes.
|
|
99
87
|
*/
|
|
100
88
|
read() {
|
|
89
|
+
this._syncProfile();
|
|
101
90
|
this._maybeRefresh();
|
|
102
|
-
return this.
|
|
91
|
+
return this.peek();
|
|
103
92
|
}
|
|
104
93
|
|
|
105
94
|
// The secondary subscription can show its last reading without starting
|
|
106
95
|
// another coding process merely to populate the sidebar.
|
|
107
|
-
peek() {
|
|
96
|
+
peek() {
|
|
97
|
+
this._syncProfile();
|
|
98
|
+
return this._last ? { limits: this._last.limits, at: this._last.at, profile: this._last.profile } : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_syncProfile() {
|
|
102
|
+
const account = this._who().profile || 'default';
|
|
103
|
+
if (account !== this._selectedProfile) {
|
|
104
|
+
this._selectedProfile = account;
|
|
105
|
+
this._generation++;
|
|
106
|
+
this._last = null;
|
|
107
|
+
this._missedUntil = 0;
|
|
108
|
+
}
|
|
109
|
+
return account;
|
|
110
|
+
}
|
|
108
111
|
|
|
109
112
|
_maybeRefresh() {
|
|
110
113
|
const now = this._now();
|
|
@@ -114,33 +117,65 @@ export class ClaudeUsage {
|
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
/** Ask again now, whatever the cache says. The settings screen's own button. */
|
|
117
|
-
refreshNow() {
|
|
120
|
+
async refreshNow() {
|
|
121
|
+
this._syncProfile();
|
|
118
122
|
this._missedUntil = 0;
|
|
119
|
-
|
|
123
|
+
// Join an existing reading, including one begun by the background poll.
|
|
124
|
+
// If it belongs to the previous login, wait for it and then ask this one.
|
|
125
|
+
const generation = this._generation;
|
|
126
|
+
const pendingGeneration = this._pendingGeneration;
|
|
127
|
+
const result = await (this._pending ?? this._refresh());
|
|
128
|
+
this._syncProfile();
|
|
129
|
+
if (generation !== this._generation) return { ok: false, error: 'The account changed. Refresh usage again.' };
|
|
130
|
+
if (pendingGeneration != null && pendingGeneration !== generation) return this.refreshNow();
|
|
131
|
+
return result;
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
_refresh() {
|
|
135
|
+
const profile = this._syncProfile();
|
|
136
|
+
const generation = this._generation;
|
|
123
137
|
const bin = (() => { try { return this._bin(); } catch { return null; } })();
|
|
124
|
-
|
|
138
|
+
const failure = (error = USAGE_UNREADABLE) => ({ ok: false, error });
|
|
139
|
+
if (!bin) { this._missed(); return Promise.resolve(failure()); }
|
|
125
140
|
this._busy = true;
|
|
126
|
-
this.
|
|
141
|
+
this._pendingGeneration = generation;
|
|
142
|
+
let resolve;
|
|
143
|
+
const pending = new Promise(done => { resolve = done; });
|
|
144
|
+
this._pending = pending;
|
|
145
|
+
const finish = (err, stdout) => {
|
|
146
|
+
this._busy = false;
|
|
147
|
+
this._pending = null;
|
|
148
|
+
this._pendingGeneration = null;
|
|
149
|
+
this._syncProfile();
|
|
150
|
+
if (generation !== this._generation) { resolve(failure()); this._onChange(); return; }
|
|
151
|
+
// Failed or unreadable output cannot acquire a fresh checked time.
|
|
152
|
+
const limits = err ? [] : readUsageReport(stdout, this._now());
|
|
153
|
+
if (!limits.length) {
|
|
154
|
+
this._missed();
|
|
155
|
+
const unsupported = /get_usage.*(?:not supported|unknown)|unknown.*get_usage/i.test(err?.message ?? '');
|
|
156
|
+
resolve(failure(unsupported ? 'Update Claude Code to read usage for this account.' : USAGE_UNREADABLE));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this._last = { limits, at: this._now(), zone: localZone(), profile };
|
|
160
|
+
resolve({ ok: true, reading: this.peek() });
|
|
161
|
+
// Even unchanged percentages now have a newly checked time to draw.
|
|
162
|
+
this._onChange();
|
|
163
|
+
};
|
|
164
|
+
const env = { ...(this._who().env ?? process.env) };
|
|
165
|
+
for (const key of Object.keys(env)) {
|
|
166
|
+
if (/^ANTHROPIC_|^CLAUDE_CODE_/.test(key) || ['CLAUDECODE', 'CLAUDE_PID', 'CLAUDE_EFFORT', 'CLAUDE_CONFIG_DIR'].includes(key)) delete env[key];
|
|
167
|
+
}
|
|
168
|
+
if (profile !== 'default') env.CLAUDE_CONFIG_DIR = profile;
|
|
169
|
+
try { this._run(
|
|
127
170
|
bin,
|
|
128
|
-
['-p', '
|
|
171
|
+
['-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose',
|
|
172
|
+
'--no-session-persistence', '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}', '--tools', '',
|
|
173
|
+
'--settings', JSON.stringify({ ...NO_METERED_KEY, disableAllHooks: true })],
|
|
129
174
|
// `cwd` is the whole fix for the permission panels; see usageCwd above.
|
|
130
|
-
{ cwd: this._where(), timeout: TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, encoding: 'utf8' },
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// than wrong, and a corner that empties itself every time the network
|
|
135
|
-
// hiccups is worse than one that is five minutes behind.
|
|
136
|
-
if (err && !stdout) { this._missed(); return; }
|
|
137
|
-
const limits = readUsage(stdout, this._now(), localZone());
|
|
138
|
-
if (!limits.length) { this._missed(); return; }
|
|
139
|
-
const before = JSON.stringify(this._last?.limits ?? null);
|
|
140
|
-
this._last = { limits, at: this._now(), zone: localZone() };
|
|
141
|
-
if (JSON.stringify(limits) !== before) this._onChange();
|
|
142
|
-
},
|
|
143
|
-
);
|
|
175
|
+
{ cwd: this._where(), env, timeout: TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, encoding: 'utf8' },
|
|
176
|
+
finish,
|
|
177
|
+
); } catch (err) { finish(err, ''); }
|
|
178
|
+
return pending;
|
|
144
179
|
}
|
|
145
180
|
|
|
146
181
|
_missed() {
|