agentbox-app 0.1.6 → 0.1.11
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 +209 -55
- package/bin/agentbox.mjs +6 -0
- package/briefs/message-rules.md +13 -3
- package/briefs/worker.md +63 -49
- package/briefs/writing-rules.md +90 -131
- package/main/account-discovery.mjs +21 -20
- package/main/account-tooling.mjs +9 -10
- package/main/agent-activity.mjs +45 -5
- package/main/agent-schedule.mjs +14 -14
- package/main/agent-sessions.mjs +94 -10
- package/main/agent-updates.mjs +114 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +31 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +11 -11
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +17 -0
- package/main/codex-memory-gate.mjs +260 -0
- package/main/codex-models.mjs +12 -7
- package/main/codex-threads.mjs +46 -23
- package/main/codex-usage-file.mjs +7 -7
- package/main/codex-usage.mjs +4 -4
- package/main/codex-watch.mjs +12 -12
- package/main/config.mjs +68 -8
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +193 -0
- package/main/feedback.mjs +55 -0
- package/main/folder-pictures.mjs +48 -0
- package/main/frame-scrollbars.mjs +90 -0
- package/main/fresh-user.mjs +6 -3
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +1 -1
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +92 -54
- package/main/ipc.mjs +316 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +47 -0
- package/main/main.mjs +119 -15
- package/main/memory-gate-history.mjs +254 -0
- package/main/memory-gate-server.mjs +461 -0
- package/main/memory-gate.mjs +206 -0
- package/main/message-priority.mjs +87 -0
- package/main/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +20 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +114 -51
- package/main/serve.mjs +67 -5
- package/main/settings.mjs +223 -26
- package/main/ship-queue.mjs +279 -0
- package/main/sign-in-files.mjs +39 -0
- package/main/source-updater.mjs +298 -0
- package/main/store/home.mjs +47 -2
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +235 -9
- package/main/supervisor.mjs +1444 -406
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +74 -13
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +2 -1
- package/main/team/index.mjs +440 -0
- package/main/team/memory-cloud.mjs +247 -0
- package/main/team/projects.mjs +162 -0
- package/main/team/session.mjs +225 -0
- package/main/team/sign-in.mjs +106 -0
- package/main/team/supabase-backend.mjs +231 -0
- package/main/team/sync.mjs +163 -0
- package/main/updater.mjs +15 -5
- package/main/write-before-reload.mjs +50 -0
- package/mcp/core/page.mjs +9 -11
- package/mcp/core/work.mjs +91 -25
- package/mcp/tools.mjs +12 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-B-2Az952.css +32 -0
- package/renderer/dist/assets/index-D6qncgai.js +1168 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +38 -15
- package/shared/agents.d.mts +26 -8
- package/shared/agents.mjs +100 -108
- package/shared/analytics-events.mjs +19 -2
- package/shared/answers.mjs +12 -0
- package/shared/bridge-map.d.mts +12 -0
- package/shared/bridge-map.mjs +37 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +4 -4
- package/shared/claude-usage.mjs +6 -6
- package/shared/codex-import.mjs +7 -7
- package/shared/codex-modes.d.mts +50 -0
- package/shared/codex-usage.mjs +7 -8
- package/shared/demo-world.mjs +5 -5
- package/shared/engines.mjs +27 -10
- package/shared/feedback.d.mts +14 -0
- package/shared/feedback.mjs +44 -0
- package/shared/first-run-examples.mjs +4 -4
- package/shared/first-run-practice.d.mts +16 -0
- package/shared/first-run-practice.mjs +176 -70
- package/shared/fresh-user-home.mjs +22 -3
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +6 -0
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- package/shared/schemes.d.mts +11 -0
- package/shared/schemes.mjs +1 -1
- package/shared/shortcuts.d.mts +41 -0
- package/shared/shortcuts.mjs +236 -0
- package/shared/spawn-trouble.d.mts +2 -0
- package/shared/spawn-trouble.mjs +39 -18
- package/shared/team-rules.d.mts +17 -0
- package/shared/team-rules.mjs +230 -0
- package/shared/team-status.d.mts +11 -0
- package/shared/team-status.mjs +90 -0
- package/shared/terminal-links.d.mts +2 -0
- package/shared/terminal-links.mjs +55 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +16 -0
- package/shared/thread-cards.d.mts +19 -0
- package/shared/thread-cards.mjs +254 -0
- package/shared/usage.d.mts +1 -1
- package/shared/usage.mjs +6 -7
- package/shared/work-items.mjs +255 -27
- package/shared/work-lines.mjs +393 -16
- package/worker-permissions.json +1 -1
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
- package/renderer/dist/assets/index-D9hCAVPl.js +0 -684
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/emulator/audio-rules.mjs +0 -74
- package/renderer/dist/emulator/data/compression/README.md +0 -11
- package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
- package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
- package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +0 -22
- 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 +0 -65
- package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
- package/renderer/dist/emulator/data/emulator.css +0 -1614
- package/renderer/dist/emulator/data/loader.js +0 -170
- package/renderer/dist/emulator/data/localization/README.md +0 -63
- package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
- package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
- package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
- package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
- package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
- package/renderer/dist/emulator/data/localization/en-US.json +0 -339
- package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
- package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
- package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
- package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
- package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
- package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
- package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
- package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
- package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
- package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
- package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
- package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
- package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
- package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
- package/renderer/dist/emulator/data/src/GameManager.js +0 -464
- package/renderer/dist/emulator/data/src/compression.js +0 -142
- package/renderer/dist/emulator/data/src/emulator.js +0 -6158
- package/renderer/dist/emulator/data/src/gamepad.js +0 -167
- package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
- package/renderer/dist/emulator/data/src/shaders.js +0 -236
- package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
- package/renderer/dist/emulator/data/src/storage.js +0 -125
- package/renderer/dist/emulator/data/version.json +0 -5
- package/renderer/dist/emulator/host.js +0 -299
- package/renderer/dist/emulator/index.html +0 -28
- package/renderer/dist/emulator/provenance.json +0 -6
package/main/codex-threads.mjs
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
// events, of which `user_message` and
|
|
15
15
|
// `agent_message` are the two this reads.
|
|
16
16
|
//
|
|
17
|
-
// WHICH THREADS ARE
|
|
18
|
-
// `guardian_review` for Codex's own reviewer of a turn, and `subagent` for
|
|
19
|
-
// helper a thread spawned.
|
|
20
|
-
//
|
|
21
|
-
//
|
|
17
|
+
// WHICH THREADS ARE THE USER'S. `thread_source` is `user` for one a person typed
|
|
18
|
+
// into, `guardian_review` for Codex's own reviewer of a turn, and `subagent` for
|
|
19
|
+
// a helper a thread spawned. Guardians are the majority of transcripts, so
|
|
20
|
+
// without this filter the inbox would offer several rows for every real
|
|
21
|
+
// conversation.
|
|
22
22
|
//
|
|
23
23
|
// There is also `~/.codex/state_5.sqlite`, which has everything above in one
|
|
24
24
|
// table. It is not read: Electron ships no sqlite, the file is written live
|
|
@@ -26,9 +26,12 @@
|
|
|
26
26
|
import fs from 'node:fs';
|
|
27
27
|
import os from 'node:os';
|
|
28
28
|
import path from 'node:path';
|
|
29
|
-
import { isScratchFolder, shortFolder, threadTitle } from './agent-sessions.mjs';
|
|
29
|
+
import { RECENT_DAYS, isScratchFolder, shortFolder, threadTitle } from './agent-sessions.mjs';
|
|
30
|
+
import { WAS_SLUGS, nameSlug } from '../shared/product-name.mjs';
|
|
30
31
|
|
|
31
|
-
export const CODEX_RECENT_DAYS =
|
|
32
|
+
export const CODEX_RECENT_DAYS = RECENT_DAYS;
|
|
33
|
+
/** The originator Codex records for a run this app started (CLIENT_INFO.name), now and before. */
|
|
34
|
+
const OUR_ORIGINATORS = new Set([nameSlug, ...WAS_SLUGS]);
|
|
32
35
|
/** How many day folders the walk opens at most, whatever `days` says. */
|
|
33
36
|
const MAX_DAY_FOLDERS = 40;
|
|
34
37
|
/**
|
|
@@ -62,22 +65,38 @@ const numericDesc = (dir) => {
|
|
|
62
65
|
return names.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(b) - Number(a));
|
|
63
66
|
};
|
|
64
67
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Every transcript in the newest `days` day folders, newest folder first.
|
|
70
|
+
*
|
|
71
|
+
* AND, WITH `since`, EVERY OLDER ONE WRITTEN TO SINCE THEN. Codex files a
|
|
72
|
+
* conversation under the day it STARTED, so a thread begun two weeks ago and
|
|
73
|
+
* worked in yesterday sits in a folder the newest-`days` walk never opens
|
|
74
|
+
* (w-db6f5e331e). Past those folders only a stat is paid per file, and the
|
|
75
|
+
* walk still stops at MAX_DAY_FOLDERS. */
|
|
76
|
+
export function listRolloutFiles(codexDir = codexHome(), { days = CODEX_RECENT_DAYS, since = null } = {}) {
|
|
67
77
|
const root = path.join(codexDir, 'sessions');
|
|
68
78
|
const out = [];
|
|
69
79
|
let looked = 0;
|
|
70
80
|
const limit = Math.min(Math.max(1, Number(days) || 1), MAX_DAY_FOLDERS);
|
|
81
|
+
const reach = since == null ? limit : MAX_DAY_FOLDERS;
|
|
71
82
|
for (const year of numericDesc(root)) {
|
|
72
83
|
for (const month of numericDesc(path.join(root, year))) {
|
|
73
84
|
for (const day of numericDesc(path.join(root, year, month))) {
|
|
74
|
-
if (looked >=
|
|
85
|
+
if (looked >= reach) return out;
|
|
86
|
+
const older = looked >= limit;
|
|
75
87
|
looked += 1;
|
|
76
88
|
const dir = path.join(root, year, month, day);
|
|
77
89
|
let names = [];
|
|
78
90
|
try { names = fs.readdirSync(dir); } catch { names = []; }
|
|
79
91
|
for (const name of names) {
|
|
80
|
-
if (
|
|
92
|
+
if (!/^rollout-.*\.jsonl$/.test(name)) continue;
|
|
93
|
+
const file = path.join(dir, name);
|
|
94
|
+
if (older) {
|
|
95
|
+
let mtime = 0;
|
|
96
|
+
try { mtime = fs.statSync(file).mtimeMs; } catch { continue; }
|
|
97
|
+
if (mtime < since) continue;
|
|
98
|
+
}
|
|
99
|
+
out.push(file);
|
|
81
100
|
}
|
|
82
101
|
}
|
|
83
102
|
}
|
|
@@ -103,17 +122,17 @@ export function cleanPrompt(text) {
|
|
|
103
122
|
* WHAT CODEX PUTS IN HER MOUTH. In paginated history mode the user turn is a
|
|
104
123
|
* `response_item` with several `<recommended_plugins>…`,
|
|
105
124
|
* `<environment_context>…`, `<user_instructions>…`. Every one of those opens
|
|
106
|
-
* with an angle-bracket tag on its first line, and nothing
|
|
107
|
-
* Measured on
|
|
125
|
+
* with an angle-bracket tag on its first line, and nothing a person types does.
|
|
126
|
+
* Measured on a real thread: three parts, the
|
|
108
127
|
* first two injected. */
|
|
109
128
|
export function isInjected(text) {
|
|
110
129
|
const t = String(text ?? '').trimStart();
|
|
111
130
|
const first = t.split('\n')[0] ?? '';
|
|
112
131
|
if (/^<[a-z_]+[^>]*>/i.test(first)) return true;
|
|
113
132
|
// The folder's AGENTS.md, pasted in as "# AGENTS.md instructions for
|
|
114
|
-
// /path" followed by an <INSTRUCTIONS> block. Measured on
|
|
115
|
-
//
|
|
116
|
-
//
|
|
133
|
+
// /path" followed by an <INSTRUCTIONS> block. Measured on a real thread: it
|
|
134
|
+
// was the second part of the first turn, and the first read of it printed it
|
|
135
|
+
// as what the user asked.
|
|
117
136
|
if (/^#\s*AGENTS\.md instructions/i.test(first)) return true;
|
|
118
137
|
return /^\s*<INSTRUCTIONS>/i.test(t.slice(first.length, first.length + 200));
|
|
119
138
|
}
|
|
@@ -142,9 +161,8 @@ const partsOf = (payload) => (Array.isArray(payload?.content) ? payload.content
|
|
|
142
161
|
* lines of type `user_message` / `agent_message` with a `message` string.
|
|
143
162
|
* Paginated writes none of those: the turns are `response_item` lines of type
|
|
144
163
|
* `message` with a `role` and a `content` array of `input_text`, `input_image`
|
|
145
|
-
* and `output_text` parts.
|
|
146
|
-
*
|
|
147
|
-
* legacy, which is why her row said "Codex has not answered yet." */
|
|
164
|
+
* and `output_text` parts. Current Codex threads are paginated, and a build
|
|
165
|
+
* that read only legacy showed "Codex has not answered yet." on every row. */
|
|
148
166
|
export function readRollout(file) {
|
|
149
167
|
let text = '';
|
|
150
168
|
try {
|
|
@@ -177,7 +195,12 @@ export function readRollout(file) {
|
|
|
177
195
|
meta = {
|
|
178
196
|
id: String(p.session_id ?? p.id ?? '').trim(),
|
|
179
197
|
cwd: String(p.cwd ?? '').trim(),
|
|
180
|
-
|
|
198
|
+
// A RUN THIS APP STARTED IS NOT HERS TO IMPORT (w-db6f5e331e). Codex
|
|
199
|
+
// 0.160.0 writes no thread_source, so the line after this would call
|
|
200
|
+
// every run a person's; its originator is the clientInfo name we hand
|
|
201
|
+
// Codex, under this name or one the app had before.
|
|
202
|
+
threadSource: OUR_ORIGINATORS.has(String(p.originator ?? '').toLowerCase()) ? 'agentbox'
|
|
203
|
+
: typeof p.thread_source === 'string' ? p.thread_source : (typeof p.source === 'string' ? 'user' : 'other'),
|
|
181
204
|
startedAt: Number.isFinite(startedAt) ? startedAt : 0,
|
|
182
205
|
};
|
|
183
206
|
continue;
|
|
@@ -227,15 +250,15 @@ export const LIVE_MS = 2 * 60_000;
|
|
|
227
250
|
export const isLive = (mtime, now = Date.now()) => Number(mtime) > 0 && now - Number(mtime) < LIVE_MS;
|
|
228
251
|
|
|
229
252
|
/**
|
|
230
|
-
*
|
|
231
|
-
* import row both read. One entry per thread
|
|
253
|
+
* THE USER'S CONVERSATIONS, newest first, in the shape the import card and the
|
|
254
|
+
* import row both read. One entry per thread a person typed into, in a real folder,
|
|
232
255
|
* touched in the last `days` days. */
|
|
233
256
|
export function readCodexThreads({ home = os.homedir(), codexDir = codexHome(home), days = CODEX_RECENT_DAYS, now = Date.now() } = {}) {
|
|
234
257
|
const index = readCodexIndex(codexDir);
|
|
235
258
|
const cutoff = now - Math.max(0, Number(days) || 0) * 86400 * 1000;
|
|
236
259
|
const out = [];
|
|
237
260
|
const skipped = { notHers: 0, scratch: 0, old: 0, empty: 0, unreadable: 0 };
|
|
238
|
-
for (const file of listRolloutFiles(codexDir, { days })) {
|
|
261
|
+
for (const file of listRolloutFiles(codexDir, { days, since: cutoff })) {
|
|
239
262
|
const r = readRollout(file);
|
|
240
263
|
if (!r) { skipped.unreadable += 1; continue; }
|
|
241
264
|
if (r.threadSource !== 'user') { skipped.notHers += 1; continue; }
|
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
// and it is a good one: never spawn anything and never ask OpenAI anything to
|
|
5
5
|
// draw a picture. Codex's figures ride the app-server the fleet is already
|
|
6
6
|
// running and arrive unprompted during a turn. The unstated cost of that rule is
|
|
7
|
-
// that the app only ever learns a number for work POWERUP ITSELF RAN.
|
|
8
|
-
// Codex in its own app all day
|
|
9
|
-
//
|
|
7
|
+
// that the app only ever learns a number for work POWERUP ITSELF RAN. Someone
|
|
8
|
+
// who runs Codex in its own app all day never gets the push, so the panel sits
|
|
9
|
+
// empty while their weekly limit is at 100% used.
|
|
10
10
|
//
|
|
11
11
|
// SO THERE IS A THIRD SOURCE, AND IT BREAKS NEITHER HALF OF THAT RULE. Codex
|
|
12
12
|
// writes every rate-limit report it receives into its own session log under
|
|
13
13
|
// CODEX_HOME, as a `rate_limits` object on the `token_count` events. Reading it
|
|
14
14
|
// starts no process, sends nothing anywhere, and touches no credential: it is a
|
|
15
|
-
// file on
|
|
16
|
-
// such records across
|
|
17
|
-
// the
|
|
15
|
+
// file on the disk that Codex put there. A busy Codex home holds hundreds of
|
|
16
|
+
// such records across its most recent sessions, the newest written within
|
|
17
|
+
// the minute.
|
|
18
18
|
//
|
|
19
19
|
// THE PUSH STILL WINS. A live notification is this second's truth and a log is
|
|
20
20
|
// the last thing anybody wrote down, so main/codex-usage.mjs reads this only when
|
|
@@ -111,7 +111,7 @@ function tail(file, bytes = TAIL_BYTES) {
|
|
|
111
111
|
*
|
|
112
112
|
* Lines are walked backwards because the last report is the one that is true. A
|
|
113
113
|
* report carrying no usable window is skipped rather than accepted: Codex emits
|
|
114
|
-
* an all-null snapshot for limit families that do not apply to the account (
|
|
114
|
+
* an all-null snapshot for limit families that do not apply to the account (a
|
|
115
115
|
* `premium` family, every field null), and taking one of those would blank a
|
|
116
116
|
* panel that had a real figure a moment earlier.
|
|
117
117
|
*/
|
package/main/codex-usage.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// Codex's figures ride the `codex app-server` THE FLEET IS ALREADY RUNNING:
|
|
12
12
|
// they arrive unprompted as `account/rateLimits/updated` during a turn.
|
|
13
13
|
//
|
|
14
|
-
// AND NEVER ASK FOR ONE EITHER
|
|
14
|
+
// AND NEVER ASK FOR ONE EITHER. This reader briefly
|
|
15
15
|
// also sent an explicit rate-limits read down that pipe whenever its figure
|
|
16
16
|
// went stale. It is deliberately gone, and this paragraph is here so the next
|
|
17
17
|
// reader does not put it back as an obvious improvement.
|
|
@@ -96,9 +96,9 @@ export class CodexUsage {
|
|
|
96
96
|
const limits = codexLimits(this._snapshot);
|
|
97
97
|
if (limits.length) return { limits, at: this._at };
|
|
98
98
|
// NOTHING HAS PUSHED, SO ASK WHAT CODEX ALREADY WROTE DOWN. This is the
|
|
99
|
-
// whole of: the push only ever arrives for work the app itself ran, and
|
|
100
|
-
// runs Codex in its own app
|
|
101
|
-
// weekly limit was full. main/codex-usage-file.mjs is a read of a file
|
|
99
|
+
// whole of: the push only ever arrives for work the app itself ran, and a
|
|
100
|
+
// person who runs Codex in its own app would see the panel stay empty
|
|
101
|
+
// while their weekly limit was full. main/codex-usage-file.mjs is a read of a file
|
|
102
102
|
// under this same CODEX_HOME. It starts no process and sends nothing
|
|
103
103
|
// anywhere, so neither rule above is bent.
|
|
104
104
|
return this._logged();
|
package/main/codex-watch.mjs
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
// THE WATCH THAT
|
|
1
|
+
// THE WATCH THAT KEEPS AN IMPORTED CODEX CONVERSATION CURRENT.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// writes another answer, the row's result follows it.
|
|
3
|
+
// Every minute this reads the user's recent Codex conversations and, for each
|
|
4
|
+
// one they already brought in, makes the row follow Codex: when Codex writes
|
|
5
|
+
// another answer, the row's result follows it. An asking row filed before this
|
|
6
|
+
// change is kept true the same way.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// IT NO LONGER ASKS ABOUT NEW ONES (w-db6f5e331e, 2026-10-05). It used to file
|
|
9
|
+
// an "Import into ...?" row for every new conversation in a project's folder.
|
|
10
|
+
// The founder, before launch: "after onboarding for both anthropic and openai
|
|
11
|
+
// harnesses, it should only import on command, not continuously". Importing is
|
|
12
|
+
// now the walk's last card and ⌘K import, for Claude Code and Codex alike.
|
|
12
13
|
import { readCodexThreads } from './codex-threads.mjs';
|
|
13
14
|
|
|
14
15
|
export const CODEX_POLL_MS = 60_000;
|
|
15
16
|
|
|
16
17
|
export function scanCodex({ store, readThreads = readCodexThreads, now = Date.now() } = {}) {
|
|
17
18
|
let threads = [];
|
|
18
|
-
try { ({ threads } = readThreads({ now })); } catch { return {
|
|
19
|
-
const asked = store.askAboutCodexThreads(threads, { now });
|
|
19
|
+
try { ({ threads } = readThreads({ now })); } catch { return { refreshed: 0, retold: 0, threads: 0 }; }
|
|
20
20
|
const refreshed = store.refreshCodexMirrors(threads, { now });
|
|
21
21
|
const retold = store.refreshCodexAsks(threads, { now });
|
|
22
|
-
return {
|
|
22
|
+
return { refreshed, retold, threads: threads.length };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function startCodexWatch({ store, readThreads = readCodexThreads, pollMs = CODEX_POLL_MS, firstDelayMs = 8_000 } = {}) {
|
package/main/config.mjs
CHANGED
|
@@ -59,10 +59,30 @@ const DEFAULTS = {
|
|
|
59
59
|
// How many sessions run at once. The queue absorbs the rest; when the plan
|
|
60
60
|
// window caps out, a longer queue is the visible, benign failure mode.
|
|
61
61
|
//
|
|
62
|
-
// THIS IS THE NUMBER FOR A MAX SUBSCRIPTION,
|
|
63
|
-
//
|
|
62
|
+
// THIS IS THE NUMBER FOR A MAX SUBSCRIPTION, the biggest plan. When nobody
|
|
63
|
+
// has set one, loadConfig below reads the plan
|
|
64
64
|
// Claude Code is signed in on and lowers this for a smaller one.
|
|
65
65
|
maxConcurrentSessions: DEFAULT_SESSIONS_AT_ONCE,
|
|
66
|
+
// LET AGENTBOX DECIDE HOW MANY RUN (w-e5225b62ba). On unless somebody has set
|
|
67
|
+
// a number themselves, which `loadConfig` works out below from whether one was
|
|
68
|
+
// ever written down. With it on, `maxConcurrentSessions` is not read at all:
|
|
69
|
+
// `Supervisor#_slotsPerAccount` asks main/machine.mjs for what this Mac
|
|
70
|
+
// carries and shares that out across the live accounts. `agentsNudge` is how
|
|
71
|
+
// many times "Something felt slow" has been pressed, and only ever subtracts.
|
|
72
|
+
agentsAuto: true,
|
|
73
|
+
agentsNudge: 0,
|
|
74
|
+
// HOLD HEAVY WORK WHEN MEMORY IS SHORT (w-3958c3753d). Off until somebody
|
|
75
|
+
// turns it on from the Agents page. With it on, every shell command a worker
|
|
76
|
+
// runs asks main/memory-gate-server.mjs first, and heavy ones wait their turn
|
|
77
|
+
// while the Mac is short of memory. `memoryGateSlots` is how many heavy
|
|
78
|
+
// commands may run at once; null is Auto, one per 8 GB.
|
|
79
|
+
memoryGate: false,
|
|
80
|
+
memoryGateSlots: null,
|
|
81
|
+
// STOP WHAT FINISHED AGENTS LEAVE RUNNING (main/leftovers.mjs). Its own
|
|
82
|
+
// switch, off until somebody turns it on; `cleanupLeftoversSince` is the
|
|
83
|
+
// moment they did, so nothing is stopped sooner than the idle time after it.
|
|
84
|
+
cleanupLeftovers: false,
|
|
85
|
+
cleanupLeftoversSince: null,
|
|
66
86
|
// What spawned sessions are allowed to do. Left unset on purpose, because the
|
|
67
87
|
// right default now depends on whether a store MCP server is present: see
|
|
68
88
|
// Supervisor#sessionArgsFor. Setting it here still wins over that, which is
|
|
@@ -80,11 +100,9 @@ const DEFAULTS = {
|
|
|
80
100
|
// PERSONAL PROJECTS AND THE DRIVE WERE BOTH DECLARED HERE AND BOTH ARE GONE
|
|
81
101
|
// (w-d19d6d387c, 2026-09-22). A personal project ran its sessions on the
|
|
82
102
|
// founder's message and nothing else, with no brief and no store; the drive
|
|
83
|
-
// asked "what next?" by itself on a project that had gone quiet.
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// that self-drives the product entirely. That should never have existed
|
|
87
|
-
// anyway, especially because we're open sourcing it soon."
|
|
103
|
+
// asked "what next?" by itself on a project that had gone quiet. There is no
|
|
104
|
+
// case where a project should run without a brief and a store, and code that
|
|
105
|
+
// drives a product on its own does not belong in an open-source app.
|
|
88
106
|
//
|
|
89
107
|
// A config still carrying `personalProducts` or `driveEnabled` is not an
|
|
90
108
|
// error; nothing reads either key now.
|
|
@@ -150,11 +168,20 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
|
|
|
150
168
|
// The account id, minted once per install. Written back immediately, because
|
|
151
169
|
// a generated id that is not saved is a different id next launch and the
|
|
152
170
|
// store would move out from under everything.
|
|
171
|
+
//
|
|
172
|
+
// AND A NEW INSTALL MAY CHOOSE CODEX FROM ITS FIRST LAUNCH (w-db6f5e331e).
|
|
173
|
+
// `engineChoice` is the moment from which a row may run on Codex; it exists
|
|
174
|
+
// so August rows in one old store never move engine unasked, and it could
|
|
175
|
+
// only be written by hand, so a new person with both engines never got
|
|
176
|
+
// Codex at all. A Mac with no config file has no old rows, so its first
|
|
177
|
+
// launch is that moment. An install with a config is left as it was.
|
|
178
|
+
const firstLaunch = fresh && !config.engineChoice ? new Date().toISOString() : null;
|
|
179
|
+
if (firstLaunch) config.engineChoice = firstLaunch;
|
|
153
180
|
if (typeof config.accountId !== 'string' || !config.accountId) {
|
|
154
181
|
config.accountId = crypto.randomUUID();
|
|
155
182
|
try {
|
|
156
183
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
157
|
-
fs.writeFileSync(file, `${JSON.stringify({ ...overrides, accountId: config.accountId }, null, 2)}\n`);
|
|
184
|
+
fs.writeFileSync(file, `${JSON.stringify({ ...overrides, accountId: config.accountId, ...(firstLaunch ? { engineChoice: firstLaunch } : {}) }, null, 2)}\n`);
|
|
158
185
|
} catch (err) {
|
|
159
186
|
console.warn(`zero: could not write the new account id to ${file}: ${err.message}`);
|
|
160
187
|
}
|
|
@@ -205,6 +232,11 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
|
|
|
205
232
|
config.codexFound = codex.found;
|
|
206
233
|
config.codexCertain = codex.certain !== false;
|
|
207
234
|
config.codexFrom = codex.from;
|
|
235
|
+
// AND AN OLDER INSTALL WITH CODEX ON IT OPENS THE CHOICE TOO (w-d5d632e503).
|
|
236
|
+
// Only a Mac with no config file did, so a person who installed before
|
|
237
|
+
// that, or who added Codex later, never saw Codex anywhere. Written at the
|
|
238
|
+
// end, once `appDir` is on the config to save through.
|
|
239
|
+
const codexOpened = codexChoiceToOpen(config, codex.found);
|
|
208
240
|
// HOW MANY AGENTS AT ONCE, WHEN NOBODY HAS SAID, IS THE PLAN AND THE MACHINE
|
|
209
241
|
// TOGETHER. Somebody on a smaller plan gets several sessions started for them,
|
|
210
242
|
// they all spend the same allowance, and what they see is agents that are slow
|
|
@@ -237,6 +269,16 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
|
|
|
237
269
|
// already in zero.config.json is untouched, for the same reason the plan cap
|
|
238
270
|
// leaves it alone.
|
|
239
271
|
config.machineSlots = machineSlots().slots;
|
|
272
|
+
// WHO IS DECIDING THE NUMBER, read off whether one was ever written down
|
|
273
|
+
// (w-e5225b62ba). A number in zero.config.json is somebody's own instruction
|
|
274
|
+
// about this Mac and keeps being obeyed; an install that never touched the
|
|
275
|
+
// stepper was already having its number filled in here, and Automatic is that
|
|
276
|
+
// same thing said out loud and kept up to date. An explicit `agentsAuto`
|
|
277
|
+
// outranks the guess, because turning Automatic back on after setting a number
|
|
278
|
+
// has to be possible.
|
|
279
|
+
if (typeof overrides.agentsAuto !== 'boolean') {
|
|
280
|
+
config.agentsAuto = typeof overrides.maxConcurrentSessions !== 'number';
|
|
281
|
+
}
|
|
240
282
|
if (typeof overrides.maxConcurrentSessions !== 'number') {
|
|
241
283
|
const plans = readPlans(effectiveProfiles(config.authProfiles, { home }), { home });
|
|
242
284
|
const slots = slotsForPlans(plans, config.machineSlots);
|
|
@@ -254,9 +296,27 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
|
|
|
254
296
|
// Where the file is, so anything that edits a setting can write it back
|
|
255
297
|
// without being handed appDir a second time.
|
|
256
298
|
config.appDir = appDir;
|
|
299
|
+
if (codexOpened) {
|
|
300
|
+
try {
|
|
301
|
+
saveConfig(config, { engineChoice: codexOpened });
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.warn(`zero: could not open the Codex choice in ${file}: ${err.message}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
257
306
|
return config;
|
|
258
307
|
}
|
|
259
308
|
|
|
309
|
+
/**
|
|
310
|
+
* THE MOMENT TO OPEN THE CODEX CHOICE AT, OR NULL. Now, when Codex is found
|
|
311
|
+
* and the key was never written. NOW is what keeps a row marked `codex` in
|
|
312
|
+
* August on Claude Code: it was written before this moment, so it is stale
|
|
313
|
+
* (`engineChoiceOnRowIsStale`). A moment already written is kept, and
|
|
314
|
+
* `"engineChoice": false` written by hand keeps the choice shut. */
|
|
315
|
+
export function codexChoiceToOpen(config, codexFound) {
|
|
316
|
+
if (!codexFound || config.engineChoice != null) return null;
|
|
317
|
+
return new Date().toISOString();
|
|
318
|
+
}
|
|
319
|
+
|
|
260
320
|
export function configFile(appDir) {
|
|
261
321
|
return path.join(appDir, CONFIG_NAME);
|
|
262
322
|
}
|
package/main/dev-window.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// A HOT-RELOADING WINDOW MUST NOT BE THE ONE
|
|
1
|
+
// A HOT-RELOADING WINDOW MUST NOT BE THE ONE THE USER WORKS IN.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// checked out or merged
|
|
3
|
+
// A window an agent starts against a vite dev server can end up running on the
|
|
4
|
+
// real store. Vite rebuilds that window whenever the source under it changes,
|
|
5
|
+
// and the source under it is the checkout every agent shares, which can be
|
|
6
|
+
// checked out or merged several times in a few minutes.
|
|
7
7
|
//
|
|
8
|
-
// The
|
|
8
|
+
// The change that hurts is one to App.tsx that adds, removes or moves a
|
|
9
9
|
// hook, which is what a branch switch does to it. React Fast Refresh keeps a
|
|
10
10
|
// component's state only while its hooks match, so App is rebuilt from nothing:
|
|
11
11
|
// `snap` is null again, App.tsx draws `<div className="boot">the app</div>`, and
|
|
12
|
-
// every child of App is thrown away and remade.
|
|
13
|
-
//
|
|
12
|
+
// every child of App is thrown away and remade. The window drops back to the
|
|
13
|
+
// loading page, and an open artifact closes and reopens. The
|
|
14
14
|
// page never navigates, which is why nothing in the dev server's log looked bad
|
|
15
15
|
// enough to be it. Six triggers were measured against a real window and exactly
|
|
16
16
|
// one was guilty: scripts/what-throws-her-out-of-agentbox.mjs.
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// WHAT IS STILL ALLOWED, because a guard that stops the work is a guard somebody
|
|
26
26
|
// deletes:
|
|
27
27
|
//
|
|
28
|
-
// Fixtures. Nothing in a fixtures window is
|
|
28
|
+
// Fixtures. Nothing in a fixtures window is real, so there is nothing to be
|
|
29
29
|
// thrown out of, and this is what screenshot and proof scripts use.
|
|
30
30
|
//
|
|
31
31
|
// A throwaway store. A script that points HOME at a temp directory has no
|
package/main/doc-scheme.mjs
CHANGED
|
@@ -124,3 +124,5 @@ export class DocGrants {
|
|
|
124
124
|
// main/zoom-keys.mjs) scales the window, and the pane is an ordinary iframe in
|
|
125
125
|
// that window, so an open file follows it like everything else. Do not put a
|
|
126
126
|
// listener back into a served page: nothing is injected into a file she reads.
|
|
127
|
+
// The one thing added to a page is its scrollbar's look, from outside and after
|
|
128
|
+
// it is served, and frame-scrollbars.mjs says why that is not this.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// SETTING UP SOMEBODY'S PLAN FOR THEM (w-9f6975906c, picked 2026-10-05).
|
|
2
|
+
//
|
|
3
|
+
// A person with a Claude or ChatGPT plan and no Claude Code or Codex used to
|
|
4
|
+
// reach a card with a link to an install page. Now the walk asks which plan
|
|
5
|
+
// they pay for and this does the rest: install the tool if it is missing, start
|
|
6
|
+
// its own sign-in, and wait until the tool itself says it is signed in. The one
|
|
7
|
+
// thing left for them is approving in their browser.
|
|
8
|
+
//
|
|
9
|
+
// EVERY COMMAND HERE IS THE TOOL'S OWN, and that is a rule, not a style.
|
|
10
|
+
// Anthropic does not allow third party apps to offer claude.ai login unless
|
|
11
|
+
// approved, so Agentbox never draws a sign-in of its own: it runs Claude Code's
|
|
12
|
+
// `auth login`, which opens Anthropic's page, and Codex's `login`, which opens
|
|
13
|
+
// OpenAI's. Measured 2026-10-05 into throwaway home folders with stdin closed:
|
|
14
|
+
// both installers finish with no prompts (Claude Code 24.6 s, Codex 9.7 s) and
|
|
15
|
+
// land in `~/.local/bin`, the first place each finder looks; `claude auth login
|
|
16
|
+
// --claudeai` runs `open <url>` with a localhost callback, so approving in the
|
|
17
|
+
// browser finishes it with nothing to paste.
|
|
18
|
+
//
|
|
19
|
+
// NO API KEY REACHES ANYTHING RUN HERE. An inherited ANTHROPIC_API_KEY makes
|
|
20
|
+
// Claude Code report itself signed in on the key and bill it, which is the
|
|
21
|
+
// opposite of setting up their plan; `terminalEnv` is the scrub the built-in
|
|
22
|
+
// terminal already uses.
|
|
23
|
+
|
|
24
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
25
|
+
import { terminalEnv } from './task-terminals.mjs';
|
|
26
|
+
|
|
27
|
+
const INSTALLERS = {
|
|
28
|
+
claude: { shell: '/bin/bash', url: 'https://claude.ai/install.sh', pipe: 'bash' },
|
|
29
|
+
codex: { shell: '/bin/sh', url: 'https://chatgpt.com/codex/install.sh', pipe: 'sh' },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function installCommand(engine) {
|
|
33
|
+
const i = INSTALLERS[engine];
|
|
34
|
+
return { file: i.shell, args: ['-c', `curl -fsSL ${i.url} | ${i.pipe}`] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The plan, never the API console: `--claudeai` is Claude Code's own name for it. */
|
|
38
|
+
export function signInCommand(engine, bin) {
|
|
39
|
+
return engine === 'claude'
|
|
40
|
+
? { file: bin, args: ['auth', 'login', '--claudeai'] }
|
|
41
|
+
: { file: bin, args: ['login'] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Exit 0 is signed in and 1 is not, for both (measured 2026-10-05). */
|
|
45
|
+
export function statusCommand(engine, bin) {
|
|
46
|
+
return engine === 'claude'
|
|
47
|
+
? { file: bin, args: ['auth', 'status', '--json'] }
|
|
48
|
+
: { file: bin, args: ['login', 'status'] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const LOG_LIMIT = 64 * 1024;
|
|
52
|
+
const STATUS_TIMEOUT = 15_000;
|
|
53
|
+
const POLL = 2_000;
|
|
54
|
+
const ANSI = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
55
|
+
|
|
56
|
+
function realSpawn(file, args, { env, onData, keepStdin = false } = {}) {
|
|
57
|
+
let child;
|
|
58
|
+
try {
|
|
59
|
+
child = nodeSpawn(file, args, { env, stdio: [keepStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'] });
|
|
60
|
+
} catch {
|
|
61
|
+
return { exit: Promise.resolve(127), kill() {} };
|
|
62
|
+
}
|
|
63
|
+
const feed = (d) => onData?.(String(d));
|
|
64
|
+
child.stdout?.on('data', feed);
|
|
65
|
+
child.stderr?.on('data', feed);
|
|
66
|
+
const exit = new Promise((resolve) => {
|
|
67
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
68
|
+
child.on('error', () => resolve(127));
|
|
69
|
+
});
|
|
70
|
+
return { exit, kill: () => { try { child.kill('SIGTERM'); } catch { /* already gone */ } } };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* One setup per tool, held for the life of the app.
|
|
77
|
+
*
|
|
78
|
+
* `find(engine)` is the finder, looking again from scratch (the caller passes
|
|
79
|
+
* `recheckClaude` / `recheckCodex`, which also move the config so the rest of
|
|
80
|
+
* the app runs on what was just installed). `spawn` and `wait` are seams for
|
|
81
|
+
* the tests; nothing in the app passes them.
|
|
82
|
+
*/
|
|
83
|
+
export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env = process.env, poll = POLL, onSignIn = () => {} } = {}) {
|
|
84
|
+
const runEnv = terminalEnv(env);
|
|
85
|
+
const jobs = new Map();
|
|
86
|
+
|
|
87
|
+
const fresh = (engine) => ({ engine, phase: 'idle', error: null, log: '', running: null, login: null, run: 0 });
|
|
88
|
+
const job = (engine) => { if (!jobs.has(engine)) jobs.set(engine, fresh(engine)); return jobs.get(engine); };
|
|
89
|
+
const say = (j, text) => { j.log = (j.log + String(text).replace(ANSI, '')).slice(-LOG_LIMIT); };
|
|
90
|
+
const lastLine = (text) => text.split('\n').map((l) => l.trim()).filter(Boolean).pop() ?? '';
|
|
91
|
+
|
|
92
|
+
async function signedIn(j, bin) {
|
|
93
|
+
const { file, args } = statusCommand(j.engine, bin);
|
|
94
|
+
const p = spawn(file, args, { env: runEnv });
|
|
95
|
+
const TIMED_OUT = Symbol('no answer');
|
|
96
|
+
const code = await Promise.race([p.exit, wait(STATUS_TIMEOUT).then(() => { p.kill(); return TIMED_OUT; })]);
|
|
97
|
+
// TOLD TO THE APP, which routes on it (w-d5d632e503): a Claude Code that
|
|
98
|
+
// is signed out beside a Codex that is signed in is a Mac that runs on
|
|
99
|
+
// Codex. Only a real yes or no; a check that never answered says nothing.
|
|
100
|
+
if (code === 0 || code === 1) onSignIn(j.engine, code === 0);
|
|
101
|
+
return code === 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function startLogin(j, bin) {
|
|
105
|
+
j.login?.kill();
|
|
106
|
+
const { file, args } = signInCommand(j.engine, bin);
|
|
107
|
+
const login = spawn(file, args, { env: runEnv, keepStdin: true, onData: (d) => say(j, d) });
|
|
108
|
+
j.login = login;
|
|
109
|
+
return login;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function go(j, run) {
|
|
113
|
+
const live = () => j.run === run;
|
|
114
|
+
let hit = find(j.engine);
|
|
115
|
+
if (!hit?.found) {
|
|
116
|
+
j.phase = 'installing';
|
|
117
|
+
const { file, args } = installCommand(j.engine);
|
|
118
|
+
const p = spawn(file, args, { env: runEnv, onData: (d) => say(j, d) });
|
|
119
|
+
j.running = p;
|
|
120
|
+
const code = await p.exit;
|
|
121
|
+
j.running = null;
|
|
122
|
+
if (!live()) return;
|
|
123
|
+
if (code !== 0) return fail(j, `The installer stopped: ${lastLine(j.log) || `exit ${code}`}`);
|
|
124
|
+
hit = find(j.engine);
|
|
125
|
+
if (!hit?.found) return fail(j, 'The installer finished, but the app still cannot find it.');
|
|
126
|
+
}
|
|
127
|
+
j.bin = hit.path;
|
|
128
|
+
j.phase = 'checking';
|
|
129
|
+
if (await signedIn(j, hit.path)) return live() && ready(j);
|
|
130
|
+
if (!live()) return;
|
|
131
|
+
j.phase = 'signing-in';
|
|
132
|
+
let login = startLogin(j, hit.path);
|
|
133
|
+
let gaveUp = false;
|
|
134
|
+
login.exit.then((code) => { if (login === j.login && code !== 0) gaveUp = true; });
|
|
135
|
+
for (;;) {
|
|
136
|
+
await wait(poll);
|
|
137
|
+
if (!live()) return;
|
|
138
|
+
if (await signedIn(j, hit.path)) { if (live()) ready(j); return; }
|
|
139
|
+
if (j.login !== login) {
|
|
140
|
+
login = j.login;
|
|
141
|
+
gaveUp = false;
|
|
142
|
+
login.exit.then((code) => { if (login === j.login && code !== 0) gaveUp = true; });
|
|
143
|
+
}
|
|
144
|
+
if (gaveUp) return fail(j, 'The sign-in was closed before it finished.');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ready(j) { j.login?.kill(); j.login = null; j.phase = 'ready'; j.error = null; }
|
|
149
|
+
function fail(j, error) { j.login?.kill(); j.login = null; j.phase = 'failed'; j.error = error; }
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
start(engine) {
|
|
153
|
+
const j = job(engine);
|
|
154
|
+
if (['installing', 'checking', 'signing-in'].includes(j.phase)) return this.status(engine);
|
|
155
|
+
j.run += 1; j.phase = 'checking'; j.error = null; j.log = '';
|
|
156
|
+
const run = j.run;
|
|
157
|
+
go(j, run).catch((e) => { if (j.run === run) fail(j, String(e?.message ?? e)); });
|
|
158
|
+
return this.status(engine);
|
|
159
|
+
},
|
|
160
|
+
/** Kill the sign-in that is waiting and start a new one, which opens the page again. */
|
|
161
|
+
signInAgain(engine) {
|
|
162
|
+
const j = job(engine);
|
|
163
|
+
if (j.phase === 'signing-in' && j.bin) startLogin(j, j.bin);
|
|
164
|
+
return this.status(engine);
|
|
165
|
+
},
|
|
166
|
+
cancel(engine) {
|
|
167
|
+
const j = job(engine);
|
|
168
|
+
j.run += 1;
|
|
169
|
+
j.running?.kill(); j.login?.kill();
|
|
170
|
+
j.running = null; j.login = null;
|
|
171
|
+
if (j.phase !== 'ready') j.phase = 'idle';
|
|
172
|
+
return this.status(engine);
|
|
173
|
+
},
|
|
174
|
+
status(engine) {
|
|
175
|
+
const j = job(engine);
|
|
176
|
+
return { engine, phase: j.phase, error: j.error, log: j.log };
|
|
177
|
+
},
|
|
178
|
+
/**
|
|
179
|
+
* WHETHER THIS TOOL CAN RUN AN AGENT RIGHT NOW: found, and signed in by its
|
|
180
|
+
* own account. The walk asks before it asks anything, so a Mac that is
|
|
181
|
+
* already set up never sees the plan question. */
|
|
182
|
+
async readiness(engine) {
|
|
183
|
+
const hit = find(engine);
|
|
184
|
+
if (!hit?.found) return { engine, found: false, signedIn: false };
|
|
185
|
+
return { engine, found: true, signedIn: await signedIn({ engine }, hit.path) };
|
|
186
|
+
},
|
|
187
|
+
/** The same question against a path already found, so asking it at
|
|
188
|
+
* launch does not search the Mac again. */
|
|
189
|
+
signedInNow(engine, bin) {
|
|
190
|
+
return signedIn({ engine }, bin);
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|