agentbox-app 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +57 -20
- package/main/agent-updates.mjs +9 -22
- package/main/analytics.mjs +30 -0
- package/main/claude-bin.mjs +45 -311
- package/main/claude-input.mjs +9 -3
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +16 -16
- package/main/claude-usage.mjs +82 -25
- package/main/codex-bin.mjs +51 -249
- package/main/codex-session.mjs +38 -3
- package/main/codex-threads.mjs +41 -8
- package/main/codex-watch.mjs +67 -3
- package/main/config.mjs +26 -4
- package/main/corner-tag.mjs +489 -0
- package/main/crash-report.mjs +26 -3
- package/main/engine-bin.mjs +595 -0
- package/main/engine-setup.mjs +4 -15
- package/main/feedback.mjs +11 -6
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +84 -0
- package/main/harnesses/codex.mjs +65 -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 +38 -0
- package/main/ipc.mjs +165 -31
- package/main/local-app-frames.mjs +82 -0
- package/main/main.mjs +100 -4
- package/main/memory-gate-history.mjs +51 -12
- package/main/memory-gate-server.mjs +67 -11
- package/main/memory-gate.mjs +49 -8
- package/main/memory-log.mjs +124 -0
- package/main/notify.mjs +47 -4
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-link.mjs +551 -0
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +246 -0
- package/main/row-label.mjs +6 -6
- package/main/runs-on.mjs +32 -35
- package/main/serve.mjs +113 -9
- package/main/settings.mjs +148 -10
- package/main/shell-path.mjs +86 -0
- package/main/ship-queue.mjs +24 -6
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +27 -1
- package/main/start-hold.mjs +73 -0
- package/main/store.mjs +1 -1
- package/main/supervisor.mjs +832 -428
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +490 -98
- package/main/task-terminals.mjs +37 -7
- package/main/updater.mjs +71 -9
- package/main/worktree-include.mjs +494 -0
- package/package.json +10 -1
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
- package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
- package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
- package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
- package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
- package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
- package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.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/approval-server.sh +24 -5
- package/scripts/before-publish.mjs +12 -10
- package/shared/agent-import.mjs +2 -0
- package/shared/agents.d.mts +1 -0
- package/shared/bridge-map.mjs +18 -0
- package/shared/claude-commands.generated.mjs +6 -4
- package/shared/claude-models.generated.mjs +6 -4
- package/shared/corner-tag.d.mts +14 -0
- package/shared/corner-tag.mjs +141 -0
- package/shared/engines.mjs +20 -5
- package/shared/harness-definitions.mjs +103 -0
- package/shared/notify-rules.d.mts +25 -0
- package/shared/notify-rules.mjs +19 -0
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +2 -0
- package/shared/rank.mjs +50 -0
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/runs-on.d.mts +0 -10
- package/shared/runs-on.mjs +25 -49
- package/shared/shortcuts.mjs +10 -4
- package/shared/spawn-trouble.mjs +57 -6
- 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/usage.d.mts +3 -0
- package/shared/usage.mjs +7 -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 +25 -1
- package/shared/work-lines.mjs +1 -1
- 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
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)
|
|
@@ -95,25 +98,13 @@ export function isMaxTier(raw) {
|
|
|
95
98
|
* as it was". */
|
|
96
99
|
export function readPlan(profile, { home = os.homedir(), file = null } = {}) {
|
|
97
100
|
const at = file ?? planFile(profile, home);
|
|
101
|
+
const none = { known: false, max: false, label: null, tier: null, from: null, file: at };
|
|
98
102
|
let account = null;
|
|
99
103
|
try {
|
|
100
104
|
account = JSON.parse(fs.readFileSync(at, 'utf8'))?.oauthAccount ?? null;
|
|
101
105
|
} catch {
|
|
102
|
-
return
|
|
106
|
+
return none;
|
|
103
107
|
}
|
|
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
108
|
if (!account || typeof account !== 'object') return none;
|
|
118
109
|
// THE ORDER IS MOST SPECIFIC FIRST. A rate limit tier names the plan AND its
|
|
119
110
|
// size (`max_20x` against `max_5x`), which is the thing worth showing; the
|
|
@@ -126,7 +117,15 @@ export function planOfAccount(account, at = null) {
|
|
|
126
117
|
const from = tier === account.organizationRateLimitTier
|
|
127
118
|
? 'org-tier'
|
|
128
119
|
: tier === account.userRateLimitTier ? 'user-tier' : 'org-type';
|
|
129
|
-
|
|
120
|
+
// A SUBSCRIPTION THAT HAS ENDED STILL NAMES ITS OLD TIER. Measured
|
|
121
|
+
// 2026-10-11 on a lapsed personal Max login: `billingType` had gone to
|
|
122
|
+
// "none" (a paying one says "stripe_subscription") while the tier still said
|
|
123
|
+
// `default_claude_max_20x`, so the page went on saying "Max 20x". Only the
|
|
124
|
+
// exact word counts; a file that names no billing at all is read as before.
|
|
125
|
+
if (account.billingType === 'none') {
|
|
126
|
+
return { known: true, ended: true, max: false, label: 'Subscription ended', tier, from, file: at };
|
|
127
|
+
}
|
|
128
|
+
return { known: true, ended: false, max: isMaxTier(tier), label: planLabel(tier), tier, from, file: at };
|
|
130
129
|
}
|
|
131
130
|
|
|
132
131
|
/**
|
|
@@ -154,7 +153,8 @@ export function readPlans(profiles, { home = os.homedir() } = {}) {
|
|
|
154
153
|
* AND AN UNREADABLE ACCOUNT NEVER LOWERS IT. Somebody whose config file we
|
|
155
154
|
* cannot parse gets exactly the app they had before this existed. */
|
|
156
155
|
export function slotsForPlans(plans, fallback = 3) {
|
|
157
|
-
|
|
156
|
+
// An ended subscription runs nothing, so it has no say in how many run.
|
|
157
|
+
const known = (plans ?? []).filter((p) => p.known && !p.ended);
|
|
158
158
|
if (!known.length) return fallback;
|
|
159
159
|
return known.every((p) => p.max) ? fallback : 1;
|
|
160
160
|
}
|
package/main/claude-usage.mjs
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// by reading it every few minutes and everything is lost by reading it on a
|
|
11
11
|
// timer the window waits for.
|
|
12
12
|
//
|
|
13
|
-
// SO IT IS A CACHE WITH A BACKGROUND REFRESH
|
|
13
|
+
// SO IT IS A CACHE WITH A BACKGROUND REFRESH. Only the explicit Settings
|
|
14
|
+
// refresh waits for a result; snapshots always return immediately:
|
|
14
15
|
//
|
|
15
16
|
// - `read` answers instantly, always, from the last measurement or with null.
|
|
16
17
|
// It never blocks and never throws.
|
|
@@ -33,7 +34,7 @@ import fs from 'node:fs';
|
|
|
33
34
|
import os from 'node:os';
|
|
34
35
|
import path from 'node:path';
|
|
35
36
|
import { readUsage } from '../shared/claude-usage.mjs';
|
|
36
|
-
import { localZone } from '../shared/usage.mjs';
|
|
37
|
+
import { localZone, USAGE_UNREADABLE } from '../shared/usage.mjs';
|
|
37
38
|
|
|
38
39
|
// WHERE THE READING RUNS, AND WHY IT IS NOT "WHEREVER THE APP IS". An app
|
|
39
40
|
// opened from Finder has `/` as its working directory, and this used to spawn
|
|
@@ -76,20 +77,36 @@ export class ClaudeUsage {
|
|
|
76
77
|
* is, asked each time rather than captured: it can be installed while the
|
|
77
78
|
* app is running, and the settings screen already promises that works.
|
|
78
79
|
* @param { => void} [how.onChange] Told when a new reading lands, so the
|
|
79
|
-
* window redraws
|
|
80
|
+
* window redraws, including a new checked time on unchanged percentages.
|
|
81
|
+
* @param { => { profile: string, env?: object }} [how.account] WHOSE LIMIT
|
|
82
|
+
* IS READ, asked each time. It used to be nobody's in particular: the
|
|
83
|
+
* command ran with the app's own environment, which is the default login,
|
|
84
|
+
* so a Mac with a second account picked as In use showed "No usage reading
|
|
85
|
+
* yet" for good, because the default one was at its limit and Claude Code
|
|
86
|
+
* printed no limits for it (2026-10-08, Claude Code 2.1.293). `profile` is
|
|
87
|
+
* the account's name ('default' or its CLAUDE_CONFIG_DIR) and `env` is the
|
|
88
|
+
* environment to run it in. Supervisor.usageAccount is the real one.
|
|
80
89
|
*/
|
|
81
|
-
constructor({ bin, onChange = () => {}, run = execFile, now = () => Date.now(), where = usageCwd } = {}) {
|
|
90
|
+
constructor({ bin, onChange = () => {}, run = execFile, now = () => Date.now(), where = usageCwd, account = () => ({ profile: 'default' }) } = {}) {
|
|
82
91
|
this._bin = bin;
|
|
92
|
+
this._selectedProfile = null;
|
|
93
|
+
this._generation = 0;
|
|
94
|
+
this._pending = null;
|
|
83
95
|
this._onChange = onChange;
|
|
84
96
|
this._run = run;
|
|
85
97
|
this._now = now;
|
|
86
98
|
this._where = where;
|
|
87
|
-
|
|
99
|
+
this._account = account;
|
|
100
|
+
/** @type {{ limits: any[], at: number, zone: string|null, profile: string }|null} */
|
|
88
101
|
this._last = null;
|
|
89
102
|
this._busy = false;
|
|
90
103
|
this._missedUntil = 0;
|
|
91
104
|
}
|
|
92
105
|
|
|
106
|
+
_who() {
|
|
107
|
+
try { return this._account() ?? { profile: 'default' }; } catch { return { profile: 'default' }; }
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
/**
|
|
94
111
|
* THE LAST READING, INSTANTLY, and a refresh started if it has gone stale.
|
|
95
112
|
*
|
|
@@ -98,13 +115,28 @@ export class ClaudeUsage {
|
|
|
98
115
|
* than a pill full of dashes.
|
|
99
116
|
*/
|
|
100
117
|
read() {
|
|
118
|
+
this._syncProfile();
|
|
101
119
|
this._maybeRefresh();
|
|
102
|
-
return this.
|
|
120
|
+
return this.peek();
|
|
103
121
|
}
|
|
104
122
|
|
|
105
123
|
// The secondary subscription can show its last reading without starting
|
|
106
124
|
// another coding process merely to populate the sidebar.
|
|
107
|
-
peek() {
|
|
125
|
+
peek() {
|
|
126
|
+
this._syncProfile();
|
|
127
|
+
return this._last ? { limits: this._last.limits, at: this._last.at, profile: this._last.profile } : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_syncProfile() {
|
|
131
|
+
const account = this._who().profile || 'default';
|
|
132
|
+
if (account !== this._selectedProfile) {
|
|
133
|
+
this._selectedProfile = account;
|
|
134
|
+
this._generation++;
|
|
135
|
+
this._last = null;
|
|
136
|
+
this._missedUntil = 0;
|
|
137
|
+
}
|
|
138
|
+
return account;
|
|
139
|
+
}
|
|
108
140
|
|
|
109
141
|
_maybeRefresh() {
|
|
110
142
|
const now = this._now();
|
|
@@ -114,33 +146,58 @@ export class ClaudeUsage {
|
|
|
114
146
|
}
|
|
115
147
|
|
|
116
148
|
/** Ask again now, whatever the cache says. The settings screen's own button. */
|
|
117
|
-
refreshNow() {
|
|
149
|
+
async refreshNow() {
|
|
150
|
+
this._syncProfile();
|
|
118
151
|
this._missedUntil = 0;
|
|
119
|
-
|
|
152
|
+
// Join an existing reading, including one begun by the background poll.
|
|
153
|
+
// If it belongs to the previous login, wait for it and then ask this one.
|
|
154
|
+
const generation = this._generation;
|
|
155
|
+
const pendingGeneration = this._pendingGeneration;
|
|
156
|
+
const result = await (this._pending ?? this._refresh());
|
|
157
|
+
this._syncProfile();
|
|
158
|
+
if (generation !== this._generation) return { ok: false, error: 'The account changed. Refresh usage again.' };
|
|
159
|
+
if (pendingGeneration != null && pendingGeneration !== generation) return this.refreshNow();
|
|
160
|
+
return result;
|
|
120
161
|
}
|
|
121
162
|
|
|
122
163
|
_refresh() {
|
|
164
|
+
const profile = this._syncProfile();
|
|
165
|
+
const generation = this._generation;
|
|
123
166
|
const bin = (() => { try { return this._bin(); } catch { return null; } })();
|
|
124
|
-
|
|
167
|
+
const failure = () => ({ ok: false, error: USAGE_UNREADABLE });
|
|
168
|
+
if (!bin) { this._missed(); return Promise.resolve(failure()); }
|
|
125
169
|
this._busy = true;
|
|
126
|
-
this.
|
|
170
|
+
this._pendingGeneration = generation;
|
|
171
|
+
let resolve;
|
|
172
|
+
const pending = new Promise(done => { resolve = done; });
|
|
173
|
+
this._pending = pending;
|
|
174
|
+
const finish = (err, stdout) => {
|
|
175
|
+
this._busy = false;
|
|
176
|
+
this._pending = null;
|
|
177
|
+
this._pendingGeneration = null;
|
|
178
|
+
this._syncProfile();
|
|
179
|
+
if (generation !== this._generation) { resolve(failure()); this._onChange(); return; }
|
|
180
|
+
// Failed or unreadable output cannot acquire a fresh checked time.
|
|
181
|
+
const limits = err ? [] : readUsage(stdout, this._now(), localZone());
|
|
182
|
+
if (!limits.length) { this._missed(); resolve(failure()); return; }
|
|
183
|
+
this._last = { limits, at: this._now(), zone: localZone(), profile };
|
|
184
|
+
resolve({ ok: true, reading: this.peek() });
|
|
185
|
+
// Even unchanged percentages now have a newly checked time to draw.
|
|
186
|
+
this._onChange();
|
|
187
|
+
};
|
|
188
|
+
const env = { ...(this._who().env ?? process.env) };
|
|
189
|
+
for (const key of Object.keys(env)) {
|
|
190
|
+
if (/^ANTHROPIC_|^CLAUDE_CODE_/.test(key) || ['CLAUDECODE', 'CLAUDE_PID', 'CLAUDE_EFFORT', 'CLAUDE_CONFIG_DIR'].includes(key)) delete env[key];
|
|
191
|
+
}
|
|
192
|
+
if (profile !== 'default') env.CLAUDE_CONFIG_DIR = profile;
|
|
193
|
+
try { this._run(
|
|
127
194
|
bin,
|
|
128
195
|
['-p', '/usage', '--output-format', 'text'],
|
|
129
196
|
// `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
|
-
);
|
|
197
|
+
{ cwd: this._where(), env, timeout: TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, encoding: 'utf8' },
|
|
198
|
+
finish,
|
|
199
|
+
); } catch (err) { finish(err, ''); }
|
|
200
|
+
return pending;
|
|
144
201
|
}
|
|
145
202
|
|
|
146
203
|
_missed() {
|
package/main/codex-bin.mjs
CHANGED
|
@@ -1,279 +1,81 @@
|
|
|
1
1
|
// Where Codex actually is, and whether we are entitled to say it is not here.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// THIS FILE IS NOW THE NAMES AND NOTHING ELSE. The search, the registry entry
|
|
4
|
+
// that declares Codex, and every measurement behind its paths live in
|
|
5
|
+
// main/engine-bin.mjs, which does the same job for every coding agent the app
|
|
6
|
+
// can run. Until 2026-10-08 this was 279 lines beside main/claude-bin.mjs's 338,
|
|
7
|
+
// and the two were copies of one idea: three of the seven commits they ever had
|
|
8
|
+
// are literally "one more install location".
|
|
5
9
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
10
|
+
// WHAT IS KEPT HERE is the vocabulary the rest of the app already imports --
|
|
11
|
+
// main/config.mjs, main/settings.mjs and main/codex-launch-env.mjs, which builds
|
|
12
|
+
// the PATH a Codex opened from the desktop needs out of `candidatePaths` and
|
|
13
|
+
// `versionedPaths`.
|
|
14
|
+
//
|
|
15
|
+
// `found` decides which binary would be spawned and `certain` decides whether a
|
|
16
|
+
// screen may SAY there is nothing here, and they are never the same value.
|
|
9
17
|
//
|
|
10
18
|
// ONE DIFFERENCE FROM CLAUDE CODE, AND IT CHANGES THE SCREEN. Claude Code is
|
|
11
|
-
// required: the app will not open an inbox without it. Codex is optional, and
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
19
|
+
// required: the app will not open an inbox without it. Codex is optional, and an
|
|
20
|
+
// absence is the ordinary case rather than a fault. So nothing here ever produces
|
|
21
|
+
// a warning. All a missing Codex does is leave the second engine out of the map
|
|
22
|
+
// that shared/engines.mjs reads, which is silence rather than a sentence about
|
|
23
|
+
// software nobody asked for.
|
|
16
24
|
//
|
|
17
25
|
// NOTHING HERE SPAWNS ANYTHING, AND main/config.mjs IS WHAT CALLS IT.
|
|
26
|
+
// `resolveCodexBin` is what answers "is there a Codex on this machine", and its
|
|
27
|
+
// answer is the fact `engineFor` refuses on. Before it was wired in, a `codexBin`
|
|
28
|
+
// in zero.config.json arrived on `config` through the unknown-key spread and went
|
|
29
|
+
// to `spawn` with no finder and no existence check behind it.
|
|
18
30
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// arrived on `config` through the unknown-key spread in main/config.mjs and
|
|
23
|
-
// went to `spawn` with no finder and no existence check behind it, which is the
|
|
24
|
-
// one path in the second engine that becomes a process and was the only one
|
|
25
|
-
// with no guard on it. `resolveCodexBin` is now what answers "is there a Codex
|
|
26
|
-
// on this Mac", and its answer is the fact `engineFor` refuses on.
|
|
27
|
-
//
|
|
28
|
-
// There is still no Settings row, which is a real gap and not a claim of one:
|
|
29
|
-
// she can set `codexBin` in zero.config.json and nothing on screen says whether
|
|
30
|
-
// it was found.
|
|
31
|
-
//
|
|
32
|
-
// ------------------------------------------------------------------------
|
|
33
|
-
// MEASURED ON THIS MAC 2026-09-04, BECAUSE THE 2026-08-25 ORIGINAL WAS WRITTEN
|
|
34
|
-
// AGAINST codex-cli 0.144.0 AND SIX OF ITS ASSUMPTIONS HAVE MOVED SINCE.
|
|
35
|
-
//
|
|
36
|
-
// Installed here: codex-cli 0.148.0 at /opt/homebrew/bin/codex. That is a
|
|
37
|
-
// Homebrew CASK, not a formula as the original said, and the bin entry is a
|
|
38
|
-
// SYMLINK into /opt/homebrew/Caskroom/codex/0.148.0/bin/codex. `brew info
|
|
39
|
-
// --cask codex` the same minute reports 0.148.0 installed against 0.152.1
|
|
40
|
-
// available, so the moment mid-upgrade where that link points at nothing is a
|
|
41
|
-
// live state on this machine and not a hypothetical. It is the same shape the
|
|
42
|
-
// dangling-symlink rule was written for in the sibling file.
|
|
43
|
-
//
|
|
44
|
-
// THE INSTALL METHOD THE DOCS NOW LEAD WITH DID NOT EXIST IN THE ORIGINAL.
|
|
45
|
-
// `https://chatgpt.com/codex/install.sh`, read 2026-09-04:
|
|
46
|
-
//
|
|
47
|
-
// BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}"
|
|
48
|
-
// CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}"
|
|
49
|
-
// STANDALONE_ROOT="$CODEX_HOME_DIR/packages/standalone"
|
|
50
|
-
//
|
|
51
|
-
// It links ~/.local/bin/codex at a versioned file under that root and swaps the
|
|
52
|
-
// file on every update. Three things follow, and all three are changes from the
|
|
53
|
-
// original: that path goes FIRST (the tool's own answer settles a machine set
|
|
54
|
-
// up the documented way in one stat call, exactly as ~/.local/bin/claude does
|
|
55
|
-
// next door), the standalone store's own bin is worth listing for the case
|
|
56
|
-
// where CODEX_INSTALL_DIR put the link somewhere no list covers, and CODEX_HOME
|
|
57
|
-
// can move every trace below out from under us.
|
|
58
|
-
//
|
|
59
|
-
// AND THE STORE KEEPS THE BINARY IN TWO PLACES, WHICH THE FIRST VERSION OF THIS
|
|
60
|
-
// FILE MISSED. Same script, read the same day:
|
|
61
|
-
//
|
|
62
|
-
// line 951: ln -sf "bin/codex" "$stage_release/codex"
|
|
63
|
-
// line 1021: version_from_binary "$release_dir/bin/codex" \
|
|
64
|
-
// || version_from_binary "$release_dir/codex"
|
|
65
|
-
//
|
|
66
|
-
// Every staged release carries the binary at `<release>/bin/codex` AND at
|
|
67
|
-
// `<release>/codex`, and the installer accepts either when it goes looking for
|
|
68
|
-
// a version. `current` is a symlink at the live release, so BOTH
|
|
69
|
-
// `current/bin/codex` and `current/codex` resolve on a machine set up the
|
|
70
|
-
// documented way. Each is one stat call, so listing both costs nothing, and
|
|
71
|
-
// listing only one is how the app tells somebody their Codex is missing while
|
|
72
|
-
// they are looking at it.
|
|
73
|
-
//
|
|
74
|
-
// WHAT ~/.codex REALLY HOLDS NOW: 38 entries. auth.json, config.toml, sessions/
|
|
75
|
-
// and history.jsonl are all still there, so the original's evidence list is not
|
|
76
|
-
// wrong. It is INCOMPLETE, and it is aging in a specific way. The directory
|
|
77
|
-
// also holds state_5.sqlite, thread_history_1.sqlite, logs_2.sqlite,
|
|
78
|
-
// memories_1.sqlite, queue_1.sqlite, goals_1.sqlite, models_cache.json,
|
|
79
|
-
// installation_id and shell_snapshots/, and the split matters: sessions/ holds
|
|
80
|
-
// 1691 rollout files with the newest written 2026-09-03, while history.jsonl
|
|
81
|
-
// has not been touched since 2026-06-03. The live record moved into sqlite
|
|
82
|
-
// files whose names carry a schema number that increments, which is a name no
|
|
83
|
-
// list can hold. That is why the directory itself stays on the list, last and
|
|
84
|
-
// broad, and why nothing below tries to enumerate what is inside it.
|
|
85
|
-
//
|
|
86
|
-
// AND THE LINK MOVED. The original's install URL, developers.openai.com/codex/
|
|
87
|
-
// cli, answers 308 to learn.chatgpt.com/docs/codex/cli.
|
|
88
|
-
//
|
|
89
|
-
// The npm package is unchanged and still real (@openai/codex, 0.153.0 on
|
|
90
|
-
// 2026-09-04), so the package-manager paths and the per-Node directories below
|
|
91
|
-
// are carried over exactly as they were.
|
|
31
|
+
// AND THERE IS A SETTINGS ROW NOW (w-ce8c1a9275): somebody whose Codex the
|
|
32
|
+
// search misses can point the app at the binary themselves, which is written
|
|
33
|
+
// back as `codexBin` and wins over everything below.
|
|
92
34
|
|
|
93
|
-
import fs from 'node:fs';
|
|
94
35
|
import os from 'node:os';
|
|
95
|
-
import
|
|
96
|
-
|
|
36
|
+
import {
|
|
37
|
+
ENGINES, findEngine, resolveEngine, forgetEngine, installEvidence as evidenceFor,
|
|
38
|
+
versionedBinPaths,
|
|
39
|
+
} from './engine-bin.mjs';
|
|
97
40
|
|
|
98
|
-
|
|
99
|
-
// first for the same reason its sibling's is: it is the answer the tool itself
|
|
100
|
-
// produces. Everything after it is a package manager's default bin directory,
|
|
101
|
-
// because Codex ships as a Homebrew cask and as an npm package rather than with
|
|
102
|
-
// an installer that owns a folder.
|
|
103
|
-
export function candidatePaths(home = os.homedir()) {
|
|
104
|
-
return [
|
|
105
|
-
path.join(home, '.local/bin/codex'), // the shell installer's link
|
|
106
|
-
path.join(home, '.codex/packages/standalone/current/bin/codex'),
|
|
107
|
-
path.join(home, '.codex/packages/standalone/current/codex'),
|
|
108
|
-
'/opt/homebrew/bin/codex', // the Homebrew cask, Apple Silicon
|
|
109
|
-
'/usr/local/bin/codex', // Homebrew on Intel, and npm -g
|
|
110
|
-
path.join(home, '.bun/bin/codex'),
|
|
111
|
-
path.join(home, '.volta/bin/codex'),
|
|
112
|
-
path.join(home, '.asdf/shims/codex'),
|
|
113
|
-
path.join(home, '.npm-global/bin/codex'), // the two npm prefixes people
|
|
114
|
-
path.join(home, '.npm-packages/bin/codex'), // actually write in a guide
|
|
115
|
-
path.join(home, '.yarn/bin/codex'),
|
|
116
|
-
path.join(home, 'Library/pnpm/codex'), // pnpm's own bin on macOS
|
|
117
|
-
path.join(home, '.local/share/pnpm/codex'), // pnpm's own bin on Linux
|
|
118
|
-
path.join(home, '.deno/bin/codex'),
|
|
119
|
-
path.join(home, 'bin/codex'), // a hand-rolled ~/bin on PATH
|
|
120
|
-
'/opt/local/bin/codex', // MacPorts
|
|
121
|
-
'/usr/bin/codex',
|
|
122
|
-
];
|
|
123
|
-
}
|
|
41
|
+
const CODEX = ENGINES.codex;
|
|
124
42
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const under = (dir, tail) => readdir(path.join(home, dir)).map((v) => path.join(home, dir, v, tail));
|
|
131
|
-
return [
|
|
132
|
-
...under('.nvm/versions/node', 'bin/codex'),
|
|
133
|
-
...under('.local/share/fnm/node-versions', 'installation/bin/codex'),
|
|
134
|
-
...under('Library/Application Support/fnm/node-versions', 'installation/bin/codex'),
|
|
135
|
-
...under('n/versions/node', 'bin/codex'),
|
|
136
|
-
...under('.asdf/installs/nodejs', 'bin/codex'),
|
|
137
|
-
];
|
|
43
|
+
export const INSTALL_URL = CODEX.installUrl;
|
|
44
|
+
|
|
45
|
+
/** Every normal home for this binary, in order; the first that exists wins. */
|
|
46
|
+
export function candidatePaths(home = os.homedir()) {
|
|
47
|
+
return CODEX.candidates(home, () => []);
|
|
138
48
|
}
|
|
139
49
|
|
|
140
|
-
|
|
141
|
-
|
|
50
|
+
/** One bin directory per installed Node, read rather than guessed. */
|
|
51
|
+
export function versionedPaths(home = os.homedir(), readdir = undefined) {
|
|
52
|
+
return versionedBinPaths('codex', home, readdir);
|
|
142
53
|
}
|
|
143
54
|
|
|
144
|
-
|
|
145
|
-
// used Codex in the ChatGPT app has this and nothing else (w-9f6975906c,
|
|
146
|
-
// measured 2026-10-05: codex-cli 0.158.0-alpha.2, sharing the CLI's sign-in).
|
|
147
|
-
// Searched LAST, after the shell, because it moves with the app's releases.
|
|
148
|
-
//
|
|
149
|
-
// AND THE ONE THE CODEX DESKTOP APP CARRIES (w-d5d632e503). Its reported place,
|
|
150
|
-
// not measured on a Mac here: none of the Macs this was written on had the app.
|
|
151
|
-
// A path that is not there costs one missed `exists`.
|
|
55
|
+
/** The copies the ChatGPT and Codex desktop apps carry inside themselves. */
|
|
152
56
|
export function appCopyPaths(home = os.homedir()) {
|
|
153
|
-
|
|
154
|
-
return inside.flatMap((p) => [path.join('/Applications', p), path.join(home, 'Applications', p)]);
|
|
57
|
+
return CODEX.appCopies(home);
|
|
155
58
|
}
|
|
156
59
|
|
|
157
|
-
|
|
158
|
-
// They exist only to stop the app claiming an absence it has not established.
|
|
159
|
-
//
|
|
160
|
-
// Both homes are looked in when CODEX_HOME is set, and that is on purpose: a
|
|
161
|
-
// CODEX_HOME inherited from somewhere else is not proof the ordinary directory
|
|
162
|
-
// is gone, and the whole job of this list is to make an absence hard to claim
|
|
163
|
-
// rather than easy.
|
|
60
|
+
/** What Codex leaves behind. None of it is ever spawned. */
|
|
164
61
|
export function evidencePaths(home = os.homedir(), env = process.env) {
|
|
165
|
-
|
|
166
|
-
const out = [];
|
|
167
|
-
for (const dir of new Set(homes)) {
|
|
168
|
-
out.push(
|
|
169
|
-
path.join(dir, 'auth.json'), // it has been logged in
|
|
170
|
-
path.join(dir, 'config.toml'),
|
|
171
|
-
path.join(dir, 'sessions'), // one folder tree of past runs
|
|
172
|
-
path.join(dir, 'history.jsonl'),
|
|
173
|
-
path.join(dir, 'packages/standalone/releases'), // the installer's version store
|
|
174
|
-
dir, // and the directory itself, last
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
return out;
|
|
62
|
+
return CODEX.evidence(home, env);
|
|
178
63
|
}
|
|
179
64
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// two mean opposite things. Both of Codex's normal installs are a symlink into
|
|
183
|
-
// a versioned directory that gets swapped on every update, so this is the
|
|
184
|
-
// ordinary shape of a Codex that is being upgraded rather than an odd one.
|
|
185
|
-
function defaultDangles(p) {
|
|
186
|
-
try { return !!fs.lstatSync(p) && !fs.existsSync(p); } catch { return false; }
|
|
65
|
+
export function installEvidence({ home = os.homedir(), env = process.env, exists, dangles } = {}) {
|
|
66
|
+
return evidenceFor('codex', { home, env, exists, dangles });
|
|
187
67
|
}
|
|
188
68
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
for (const p of evidencePaths(home, env)) if (exists(p)) return p;
|
|
193
|
-
for (const p of candidatePaths(home)) if (dangles(p)) return p;
|
|
194
|
-
return null;
|
|
69
|
+
/** The whole search. See main/engine-bin.mjs for what each answer means. */
|
|
70
|
+
export function findCodexBin(where = {}) {
|
|
71
|
+
return findEngine('codex', where);
|
|
195
72
|
}
|
|
196
73
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
* The whole search, a pure function of its inputs so a test can put Codex
|
|
201
|
-
* anywhere without touching the machine it runs on.
|
|
202
|
-
*
|
|
203
|
-
* Returns the same shape its sibling does: { path, found, certain, from,
|
|
204
|
-
* searched, evidence }, so a caller can hold the two side by side without
|
|
205
|
-
* learning two vocabularies.
|
|
206
|
-
*/
|
|
207
|
-
export function findCodexBin({
|
|
208
|
-
configured = null,
|
|
209
|
-
home = os.homedir(),
|
|
210
|
-
env = process.env,
|
|
211
|
-
exists = fs.existsSync,
|
|
212
|
-
shellLookup = () => askShell({ bin: 'codex' }),
|
|
213
|
-
readdir = defaultReaddir,
|
|
214
|
-
dangles = defaultDangles,
|
|
215
|
-
} = {}) {
|
|
216
|
-
if (configured) {
|
|
217
|
-
return {
|
|
218
|
-
path: configured, found: exists(configured), certain: true, from: 'settings',
|
|
219
|
-
searched: [], evidence: null,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const searched = [...candidatePaths(home), ...versionedPaths(home, readdir)];
|
|
224
|
-
for (const candidate of searched) {
|
|
225
|
-
if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'disk', searched, evidence: null };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const fromShell = shellSaid(shellLookup());
|
|
229
|
-
if (fromShell.path) return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
|
|
230
|
-
|
|
231
|
-
for (const candidate of appCopyPaths(home)) {
|
|
232
|
-
if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'app', searched, evidence: null };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// NOT FOUND, AND WHETHER THAT IS KNOWN. Every cheap path missed and every
|
|
236
|
-
// shell we could reach missed, so the last question is whether this Mac shows
|
|
237
|
-
// any sign of Codex anyway.
|
|
238
|
-
const evidence = installEvidence({ home, env, exists, dangles });
|
|
239
|
-
return {
|
|
240
|
-
path: null,
|
|
241
|
-
found: false,
|
|
242
|
-
certain: fromShell.answered && !evidence,
|
|
243
|
-
from: null,
|
|
244
|
-
searched,
|
|
245
|
-
evidence,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// askShell used to answer with a path or null. Anything handing back the old
|
|
250
|
-
// shape is read as an answer, because that is what it always meant.
|
|
251
|
-
function shellSaid(result) {
|
|
252
|
-
if (typeof result === 'string') return { path: result, answered: true };
|
|
253
|
-
if (!result || typeof result !== 'object') return { path: null, answered: true };
|
|
254
|
-
return { path: result.path ?? null, answered: result.answered !== false };
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/* ------------------------------- the cache -------------------------------- */
|
|
258
|
-
// Only the SHELL step is remembered, for the same thirty seconds and the same
|
|
259
|
-
// reason: the path checks are stat calls, and caching those is how a settings
|
|
260
|
-
// screen ends up still saying "not found" after somebody has installed it.
|
|
261
|
-
|
|
262
|
-
const SHELL_TTL = 30_000;
|
|
263
|
-
let shellAnswer = null;
|
|
264
|
-
let shellAskedAt = 0;
|
|
265
|
-
|
|
266
|
-
export function resolveCodexBin(configured = null, { now = Date.now(), shellLookup = () => askShell({ bin: 'codex' }), ...where } = {}) {
|
|
267
|
-
const remembered = () => {
|
|
268
|
-
if (shellAskedAt && now - shellAskedAt < SHELL_TTL) return shellAnswer;
|
|
269
|
-
shellAnswer = shellLookup();
|
|
270
|
-
shellAskedAt = now;
|
|
271
|
-
return shellAnswer;
|
|
272
|
-
};
|
|
273
|
-
return findCodexBin({ ...where, configured, shellLookup: remembered });
|
|
74
|
+
/** The same search with the shell step held for thirty seconds. */
|
|
75
|
+
export function resolveCodexBin(configured = null, where = {}) {
|
|
76
|
+
return resolveEngine('codex', configured, where);
|
|
274
77
|
}
|
|
275
78
|
|
|
276
79
|
export function forgetCodexBin() {
|
|
277
|
-
|
|
278
|
-
shellAskedAt = 0;
|
|
80
|
+
forgetEngine('codex');
|
|
279
81
|
}
|