agentbox-app 0.1.11 → 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/README.md +24 -16
- package/bin/agentbox.mjs +18 -9
- package/briefs/message-rules.md +15 -4
- package/main/account-tooling.mjs +2 -1
- 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 -20
- package/main/analytics.mjs +30 -0
- package/main/approvals.mjs +14 -5
- package/main/claude-bin.mjs +45 -310
- 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 +14 -2
- package/main/claude-usage.mjs +82 -25
- package/main/codex-bin.mjs +51 -248
- package/main/codex-launch-env.mjs +31 -0
- 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 +40 -13
- package/main/corner-tag.mjs +489 -0
- package/main/crash-report.mjs +26 -3
- package/main/demo.mjs +17 -14
- package/main/engine-bin.mjs +595 -0
- package/main/engine-setup.mjs +27 -27
- package/main/feedback.mjs +11 -6
- package/main/fresh-user.mjs +1 -1
- package/main/git-change.mjs +8 -2
- 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/instruction-settings.mjs +47 -11
- package/main/ipc.mjs +175 -22
- package/main/local-app-frames.mjs +82 -0
- package/main/machine.mjs +1 -1
- package/main/main.mjs +120 -5
- 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 +48 -5
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-link.mjs +551 -0
- package/main/refresh-agent-models.mjs +2 -1
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +246 -0
- package/main/row-label.mjs +8 -7
- package/main/runs-on.mjs +90 -0
- package/main/serve.mjs +124 -10
- package/main/settings.mjs +171 -11
- package/main/shell-path.mjs +86 -0
- package/main/ship-queue.mjs +25 -7
- 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/home.mjs +37 -5
- package/main/store/project.mjs +15 -19
- package/main/store.mjs +10 -4
- package/main/supervisor.mjs +1030 -447
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +491 -82
- package/main/task-terminals.mjs +49 -8
- package/main/team/index.mjs +2 -2
- package/main/team/projects.mjs +1 -1
- package/main/updater.mjs +71 -9
- package/main/worktree-include.mjs +494 -0
- package/mcp/core/page.mjs +1 -1
- package/mcp/core/work.mjs +34 -3
- package/mcp/tools.mjs +2 -1
- 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/after-install.mjs +14 -0
- package/scripts/approval-server.sh +31 -7
- package/scripts/before-publish.mjs +12 -10
- package/shared/agent-import.mjs +3 -1
- package/shared/agents.d.mts +1 -0
- package/shared/bridge-map.mjs +19 -0
- package/shared/claude-commands.generated.mjs +8 -6
- package/shared/claude-models.generated.mjs +10 -8
- package/shared/codex-modes.mjs +1 -1
- package/shared/corner-tag.d.mts +14 -0
- package/shared/corner-tag.mjs +141 -0
- package/shared/demo-world.mjs +1 -1
- package/shared/engines.mjs +20 -5
- package/shared/fresh-user-home.mjs +12 -1
- package/shared/harness-definitions.mjs +103 -0
- package/shared/instruction-defaults.json +1 -1
- package/shared/notify-rules.d.mts +25 -0
- package/shared/notify-rules.mjs +19 -0
- package/shared/project-folder-check.mjs +3 -3
- 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 +10 -0
- package/shared/runs-on.mjs +55 -0
- package/shared/shortcuts.mjs +11 -5
- package/shared/spawn-trouble.mjs +58 -7
- 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 +28 -2
- 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-B-2Az952.css +0 -32
- package/renderer/dist/assets/index-D6qncgai.js +0 -1168
package/main/engine-setup.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { harnessFor } from './harnesses.mjs';
|
|
1
2
|
// SETTING UP SOMEBODY'S PLAN FOR THEM (w-9f6975906c, picked 2026-10-05).
|
|
2
3
|
//
|
|
3
4
|
// A person with a Claude or ChatGPT plan and no Claude Code or Codex used to
|
|
@@ -23,29 +24,18 @@
|
|
|
23
24
|
|
|
24
25
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
25
26
|
import { terminalEnv } from './task-terminals.mjs';
|
|
27
|
+
import { codexLaunchEnv } from './codex-launch-env.mjs';
|
|
26
28
|
|
|
27
|
-
|
|
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
|
-
}
|
|
29
|
+
export function installCommand(engine) { return harnessFor(engine).install(); }
|
|
36
30
|
|
|
37
31
|
/** The plan, never the API console: `--claudeai` is Claude Code's own name for it. */
|
|
38
32
|
export function signInCommand(engine, bin) {
|
|
39
|
-
return engine
|
|
40
|
-
? { file: bin, args: ['auth', 'login', '--claudeai'] }
|
|
41
|
-
: { file: bin, args: ['login'] };
|
|
33
|
+
return harnessFor(engine).signIn(bin);
|
|
42
34
|
}
|
|
43
35
|
|
|
44
36
|
/** Exit 0 is signed in and 1 is not, for both (measured 2026-10-05). */
|
|
45
37
|
export function statusCommand(engine, bin) {
|
|
46
|
-
return engine
|
|
47
|
-
? { file: bin, args: ['auth', 'status', '--json'] }
|
|
48
|
-
: { file: bin, args: ['login', 'status'] };
|
|
38
|
+
return harnessFor(engine).signInStatus(bin);
|
|
49
39
|
}
|
|
50
40
|
|
|
51
41
|
const LOG_LIMIT = 64 * 1024;
|
|
@@ -57,7 +47,8 @@ function realSpawn(file, args, { env, onData, keepStdin = false } = {}) {
|
|
|
57
47
|
let child;
|
|
58
48
|
try {
|
|
59
49
|
child = nodeSpawn(file, args, { env, stdio: [keepStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'] });
|
|
60
|
-
} catch {
|
|
50
|
+
} catch (error) {
|
|
51
|
+
onData?.(`${error.message}\n`);
|
|
61
52
|
return { exit: Promise.resolve(127), kill() {} };
|
|
62
53
|
}
|
|
63
54
|
const feed = (d) => onData?.(String(d));
|
|
@@ -65,7 +56,7 @@ function realSpawn(file, args, { env, onData, keepStdin = false } = {}) {
|
|
|
65
56
|
child.stderr?.on('data', feed);
|
|
66
57
|
const exit = new Promise((resolve) => {
|
|
67
58
|
child.on('exit', (code) => resolve(code ?? 1));
|
|
68
|
-
child.on('error', () => resolve(127));
|
|
59
|
+
child.on('error', (error) => { onData?.(`${error.message}\n`); resolve(127); });
|
|
69
60
|
});
|
|
70
61
|
return { exit, kill: () => { try { child.kill('SIGTERM'); } catch { /* already gone */ } } };
|
|
71
62
|
}
|
|
@@ -82,6 +73,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
82
73
|
*/
|
|
83
74
|
export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env = process.env, poll = POLL, onSignIn = () => {} } = {}) {
|
|
84
75
|
const runEnv = terminalEnv(env);
|
|
76
|
+
const toolEnv = (engine, bin) => engine === 'codex' ? codexLaunchEnv(bin, runEnv) : runEnv;
|
|
85
77
|
const jobs = new Map();
|
|
86
78
|
|
|
87
79
|
const fresh = (engine) => ({ engine, phase: 'idle', error: null, log: '', running: null, login: null, run: 0 });
|
|
@@ -91,20 +83,26 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
|
|
|
91
83
|
|
|
92
84
|
async function signedIn(j, bin) {
|
|
93
85
|
const { file, args } = statusCommand(j.engine, bin);
|
|
94
|
-
|
|
86
|
+
let output = '';
|
|
87
|
+
const p = spawn(file, args, { env: toolEnv(j.engine, bin), onData: (d) => { output = (output + String(d).replace(ANSI, '')).slice(-LOG_LIMIT); } });
|
|
95
88
|
const TIMED_OUT = Symbol('no answer');
|
|
96
89
|
const code = await Promise.race([p.exit, wait(STATUS_TIMEOUT).then(() => { p.kill(); return TIMED_OUT; })]);
|
|
97
90
|
// TOLD TO THE APP, which routes on it (w-d5d632e503): a Claude Code that
|
|
98
91
|
// is signed out beside a Codex that is signed in is a Mac that runs on
|
|
99
92
|
// Codex. Only a real yes or no; a check that never answered says nothing.
|
|
100
93
|
if (code === 0 || code === 1) onSignIn(j.engine, code === 0);
|
|
101
|
-
return code === 0;
|
|
94
|
+
if (code === 0 || code === 1) return code === 0;
|
|
95
|
+
if (output) say(j, output);
|
|
96
|
+
const name = j.engine === 'codex' ? 'Codex' : 'Claude Code';
|
|
97
|
+
throw new Error(code === TIMED_OUT
|
|
98
|
+
? `${name} did not answer the sign-in check. Try again.`
|
|
99
|
+
: `${name} could not check sign-in: ${lastLine(output) || `exit ${code}`}`);
|
|
102
100
|
}
|
|
103
101
|
|
|
104
102
|
function startLogin(j, bin) {
|
|
105
103
|
j.login?.kill();
|
|
106
104
|
const { file, args } = signInCommand(j.engine, bin);
|
|
107
|
-
const login = spawn(file, args, { env:
|
|
105
|
+
const login = spawn(file, args, { env: toolEnv(j.engine, bin), keepStdin: true, onData: (d) => say(j, d) });
|
|
108
106
|
j.login = login;
|
|
109
107
|
return login;
|
|
110
108
|
}
|
|
@@ -130,18 +128,19 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
|
|
|
130
128
|
if (!live()) return;
|
|
131
129
|
j.phase = 'signing-in';
|
|
132
130
|
let login = startLogin(j, hit.path);
|
|
133
|
-
let
|
|
134
|
-
|
|
131
|
+
let ended = false;
|
|
132
|
+
const watchLogin = (loginProcess) => loginProcess.exit.then(() => { if (loginProcess === j.login) ended = true; });
|
|
133
|
+
watchLogin(login);
|
|
135
134
|
for (;;) {
|
|
136
135
|
await wait(poll);
|
|
137
136
|
if (!live()) return;
|
|
138
137
|
if (await signedIn(j, hit.path)) { if (live()) ready(j); return; }
|
|
139
138
|
if (j.login !== login) {
|
|
140
139
|
login = j.login;
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
ended = false;
|
|
141
|
+
watchLogin(login);
|
|
143
142
|
}
|
|
144
|
-
if (
|
|
143
|
+
if (ended) return fail(j, `The sign-in was closed before it finished.${lastLine(j.log) ? ` ${lastLine(j.log)}` : ''}`);
|
|
145
144
|
}
|
|
146
145
|
}
|
|
147
146
|
|
|
@@ -182,12 +181,13 @@ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env =
|
|
|
182
181
|
async readiness(engine) {
|
|
183
182
|
const hit = find(engine);
|
|
184
183
|
if (!hit?.found) return { engine, found: false, signedIn: false };
|
|
185
|
-
return { engine, found: true, signedIn: await signedIn({ engine }, hit.path) };
|
|
184
|
+
try { return { engine, found: true, signedIn: await signedIn({ engine, log: '' }, hit.path) }; }
|
|
185
|
+
catch (error) { return { engine, found: true, signedIn: false, error: error.message }; }
|
|
186
186
|
},
|
|
187
187
|
/** The same question against a path already found, so asking it at
|
|
188
188
|
* launch does not search the Mac again. */
|
|
189
189
|
signedInNow(engine, bin) {
|
|
190
|
-
return signedIn({ engine }, bin);
|
|
190
|
+
return signedIn({ engine, log: '' }, bin).catch(() => false);
|
|
191
191
|
},
|
|
192
192
|
};
|
|
193
193
|
}
|
package/main/feedback.mjs
CHANGED
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
// to as a secret (`FEEDBACK_TO`, cloud/supabase/functions/feedback), so the
|
|
6
6
|
// address is in no file of this public repository and no install can show it.
|
|
7
7
|
//
|
|
8
|
-
// WHERE IT GOES
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// WHERE IT GOES SHIPS IN THE CODE (w-58e758c81e, 2026-10-09). It used to be
|
|
9
|
+
// baked only into the packaged Mac app, so the npm package and a git clone,
|
|
10
|
+
// the two installs the README offers, told everyone who pressed Send that
|
|
11
|
+
// feedback was not set up, and lost what they wrote. FEEDBACK_ADDRESS is the
|
|
12
|
+
// server function, not an inbox. A `feedbackUrl` in zero.config.json, the
|
|
13
|
+
// FEEDBACK_URL variable, or a `bakedFeedbackUrl` in a packaged package.json
|
|
14
|
+
// still outrank it.
|
|
12
15
|
//
|
|
13
16
|
// It is not behind the diagnostics switch. That switch is about what Agentbox
|
|
14
17
|
// sends on its own; this is something a person typed and pressed Send on.
|
|
@@ -16,11 +19,13 @@ import { createRequire } from 'node:module';
|
|
|
16
19
|
import { checkFeedback } from '../shared/feedback.mjs';
|
|
17
20
|
import { Name, readEnv } from '../shared/product-name.mjs';
|
|
18
21
|
|
|
22
|
+
export const FEEDBACK_ADDRESS = 'https://lywvthwkhkfzhqivvond.supabase.co/functions/v1/feedback';
|
|
23
|
+
|
|
19
24
|
function bakedUrl() {
|
|
20
25
|
try {
|
|
21
|
-
return createRequire(import.meta.url)('../package.json')?.bakedFeedbackUrl ??
|
|
26
|
+
return createRequire(import.meta.url)('../package.json')?.bakedFeedbackUrl ?? FEEDBACK_ADDRESS;
|
|
22
27
|
} catch {
|
|
23
|
-
return
|
|
28
|
+
return FEEDBACK_ADDRESS;
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
31
|
|
package/main/fresh-user.mjs
CHANGED
|
@@ -96,7 +96,7 @@ export function openFreshUser({
|
|
|
96
96
|
const notes = [];
|
|
97
97
|
if (withTools && !prepared.claudeBin) notes.push('Claude Code is not at ~/.local/bin/claude, so agents will not run in it.');
|
|
98
98
|
if (!prepared.keychain) notes.push('There is no ~/Library/Keychains here, so Claude Code will say it is not logged in.');
|
|
99
|
-
if (withAgents && !prepared.agents) notes.push('There is no ~/.claude on this
|
|
99
|
+
if (withAgents && !prepared.agents) notes.push('There is no ~/.claude on this computer, so it will have no agents to offer.');
|
|
100
100
|
|
|
101
101
|
return { ok: true, home: prepared.home, agents: prepared.agents, notes, pid: child?.pid ?? null };
|
|
102
102
|
}
|
package/main/git-change.mjs
CHANGED
|
@@ -42,7 +42,10 @@ const MAX_ROWS_PER_FILE = 20_000;
|
|
|
42
42
|
const NOT_CODE = /(^|\/)(node_modules|\.git)\//;
|
|
43
43
|
|
|
44
44
|
function git(cwd, args, { maxBuffer = 64 * 1024 * 1024 } = {}) {
|
|
45
|
-
|
|
45
|
+
// A user's `diff.mnemonicPrefix` rewrites a/ and b/ into c/ i/ o/ w/.
|
|
46
|
+
// The reader below only knows one prefix. Turn the setting off for this
|
|
47
|
+
// process so a checkout reads the same on every machine.
|
|
48
|
+
return execFileSync('git', ['--no-pager', '-c', 'diff.mnemonicPrefix=false', ...args], {
|
|
46
49
|
cwd,
|
|
47
50
|
encoding: 'utf8',
|
|
48
51
|
maxBuffer,
|
|
@@ -189,7 +192,10 @@ export function filesFromPatch(patch) {
|
|
|
189
192
|
const nameOn = (line, side) => {
|
|
190
193
|
const name = line.slice(4).trim();
|
|
191
194
|
if (name === '/dev/null') return null;
|
|
192
|
-
|
|
195
|
+
// a/ and b/ are the ordinary prefixes. c/ i/ o/ w/ are mnemonicPrefix,
|
|
196
|
+
// and 1/ 2/ are `git diff --no-index` under that same setting. One
|
|
197
|
+
// prefix comes off; a file that is itself named `b/foo` stays `b/foo`.
|
|
198
|
+
return name.replace(/^[abiowc12]\//, '');
|
|
193
199
|
};
|
|
194
200
|
for (const line of String(patch ?? '').split('\n')) {
|
|
195
201
|
if (line.startsWith('diff --git ')) { closeHunk(); file = null; oldName = null; continue; }
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { hookFailure } from '../../shared/hook-failure.mjs';
|
|
2
|
+
import { SAYING_CAP } from '../codex.mjs';
|
|
3
|
+
|
|
4
|
+
// The stream's own metadata, kept on the session object: the session id (what
|
|
5
|
+
// a personal reply resumes) and the final result message (what the founder
|
|
6
|
+
// reads). Recorded for every session because it is one parse we are already
|
|
7
|
+
// paying for; acted on where someone needs it.
|
|
8
|
+
export function captureStream(session, line) {
|
|
9
|
+
try {
|
|
10
|
+
const obj = JSON.parse(line);
|
|
11
|
+
if (obj.session_id) session.sessionId = obj.session_id;
|
|
12
|
+
if (obj.type === 'result') {
|
|
13
|
+
session.result = String(obj.result ?? '');
|
|
14
|
+
session.resultIsError = !!obj.is_error;
|
|
15
|
+
// A run cannot end with helpers still out. Cleared here as well as on
|
|
16
|
+
// exit so the number can never outlive the thing it counts. Unless
|
|
17
|
+
// something is still out in the background: then this was a turn
|
|
18
|
+
// ending, not the run, and the session stays open for it.
|
|
19
|
+
if (!session.child?.waitingOn?.().length) session.helperIds?.clear();
|
|
20
|
+
}
|
|
21
|
+
noteClaimAnswer(session, obj);
|
|
22
|
+
countHelpers(session, obj);
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Whether this run was refused its own row, off the claim call and its reply.
|
|
27
|
+
// `closingMessageIsTheAnswer` reads it: a refused worker is told to end saying
|
|
28
|
+
// nothing, and whatever it does say must not land on a row someone else holds.
|
|
29
|
+
// A later claim that succeeds clears it.
|
|
30
|
+
function noteClaimAnswer(session, obj) {
|
|
31
|
+
const blocks = Array.isArray(obj?.message?.content) ? obj.message.content : [];
|
|
32
|
+
for (const b of blocks) {
|
|
33
|
+
if (obj.type === 'assistant' && b?.type === 'tool_use' && /claim_work_item$/.test(b.name ?? '')
|
|
34
|
+
&& (!b.input?.id || b.input.id === session.itemId)) {
|
|
35
|
+
(session.claimCalls ??= new Set()).add(b.id);
|
|
36
|
+
}
|
|
37
|
+
if (obj.type === 'user' && b?.type === 'tool_result' && session.claimCalls?.has(b.tool_use_id)) {
|
|
38
|
+
const text = typeof b.content === 'string' ? b.content
|
|
39
|
+
: (b.content ?? []).map((c) => c?.text ?? '').join('');
|
|
40
|
+
if (/"claimed"\s*:\s*false/.test(text)) session.claimRefused = true;
|
|
41
|
+
else if (/"claimed"\s*:\s*true/.test(text)) session.claimRefused = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `--include-partial-messages` puts `stream_event` frames on the same stdout
|
|
48
|
+
* the finished messages come down. This keeps the CURRENT text block on the
|
|
49
|
+
* session as it grows, and nothing else: no thinking, no tool arguments, and
|
|
50
|
+
* nothing written to disk. It dies with the process, exactly like `tail`.
|
|
51
|
+
*
|
|
52
|
+
* ONE BLOCK AT A TIME, AND THAT IS THE POINT. A finished message becomes ONE
|
|
53
|
+
* trace line per text block (`traceStreamLine`), so holding exactly one block
|
|
54
|
+
* here means the live string and the traced string are the same string, and
|
|
55
|
+
* the thread can drop the live copy the moment the traced one arrives without
|
|
56
|
+
* guessing (`itemThread`, renderer/src/item-thread.ts).
|
|
57
|
+
*
|
|
58
|
+
* IT IS NOT CLEARED WHEN THE MESSAGE FINISHES, and that is deliberate. The
|
|
59
|
+
* trace is written on one channel and read on another, so clearing it here
|
|
60
|
+
* would blink the sentence off her screen for however long the read takes and
|
|
61
|
+
* then put it back. The renderer suppresses it the moment the thread has it,
|
|
62
|
+
* which is a comparison rather than a race.
|
|
63
|
+
*
|
|
64
|
+
* Returns true when the string moved, which is what earns a push.
|
|
65
|
+
*/
|
|
66
|
+
export function streamingText(session, line) {
|
|
67
|
+
try {
|
|
68
|
+
const obj = JSON.parse(line);
|
|
69
|
+
if (obj.type !== 'stream_event') return false;
|
|
70
|
+
const event = obj.event ?? {};
|
|
71
|
+
// A new message. Whatever the last one ended on is the last one's, and the
|
|
72
|
+
// trace has it by now.
|
|
73
|
+
if (event.type === 'message_start') {
|
|
74
|
+
session.saying = '';
|
|
75
|
+
session.sayingAt = 0;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (event.type === 'content_block_start') {
|
|
79
|
+
// Only a text block resets it. A tool call or a thought starting means
|
|
80
|
+
// this message has moved on from prose, and the block already typed
|
|
81
|
+
// stays on screen until the trace delivers the same words.
|
|
82
|
+
if (event.content_block?.type !== 'text') return false;
|
|
83
|
+
session.saying = '';
|
|
84
|
+
session.sayingAt = Date.now();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
|
|
88
|
+
const piece = String(event.delta.text ?? '');
|
|
89
|
+
if (!piece) return false;
|
|
90
|
+
if (!session.sayingAt) session.sayingAt = Date.now();
|
|
91
|
+
const next = (session.saying ?? '') + piece;
|
|
92
|
+
// Past the cap this stops being a sentence she is reading. Dropped
|
|
93
|
+
// rather than cut, because a cut string never matches the traced one and
|
|
94
|
+
// would sit under it on her screen forever.
|
|
95
|
+
session.saying = next.length > SAYING_CAP ? '' : next;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// HOW MANY HELPERS THIS SESSION HAS OUT RIGHT NOW.
|
|
105
|
+
//
|
|
106
|
+
// THE ONLY TWO EVENTS THAT MATTER, and they bracket a helper exactly. Measured
|
|
107
|
+
// on Claude Code 2.1.246 over a real four-helper run (the probe is written up
|
|
108
|
+
// in decisions.md, 08-26): `task_started` carries a fresh `task_id`, and
|
|
109
|
+
// `task_updated` carries a `patch.status` of `completed` for that same id. Four
|
|
110
|
+
// starts, four completions, no id seen twice and none left open. So a set of
|
|
111
|
+
// live ids, and its size is the number.
|
|
112
|
+
//
|
|
113
|
+
// A SET AND NOT A COUNTER, because a counter cannot survive a duplicate: the
|
|
114
|
+
// stream is read line by line off a pipe, and one repeated line would leave the
|
|
115
|
+
// page saying five helpers are working for the rest of the run with nothing
|
|
116
|
+
// able to correct it. Adding an id twice is free.
|
|
117
|
+
//
|
|
118
|
+
// NESTED HELPERS ARE COUNTED THE SAME. `spawn_depth` says how deep a helper
|
|
119
|
+
// sits, and this deliberately ignores it: a helper's helper is still a
|
|
120
|
+
// subagent working on her task.
|
|
121
|
+
//
|
|
122
|
+
// THE LEAD IS NOT IN THIS NUMBER AND NEVER HAS BEEN, which is what the line
|
|
123
|
+
// drawn off it was getting wrong until 2026-08-27. Only a `task_started` id is
|
|
124
|
+
// ever added, and the lead has no `task_started` of its own.It is, so
|
|
125
|
+
// `shortWord` in renderer/src/live-line.ts now says Subagents, unhyphenated on
|
|
126
|
+
// her call of 2026-08-27 and on a count of the spelling in the Claude Code
|
|
127
|
+
// build she runs. If this ever starts counting the lead as well, that word has
|
|
128
|
+
// to move with it. DID THIS RUN LEAVE HER ANYTHING TO READ?
|
|
129
|
+
//
|
|
130
|
+
// The rule that divides the two writers, kept in one named place because
|
|
131
|
+
// getting it wrong is silence, and silence is the failure that matters most:
|
|
132
|
+
//
|
|
133
|
+
// - said nothing at all -> the supervisor speaks for it (sayTheRunDied)
|
|
134
|
+
// - said something wrong -> the supervisor speaks for it, in OUR words
|
|
135
|
+
// - said something real -> it speaks for itself, untouched
|
|
136
|
+
//
|
|
137
|
+
// The middle line is the one that was missing. Both writers used to exclude an
|
|
138
|
+
// error result: `speaksForTheSession` because raw CLI text must never reach her
|
|
139
|
+
// row, and this one because it only looked for a result that was absent. Each
|
|
140
|
+
// was right on its own and together they left the case that matters most with
|
|
141
|
+
// no writer at all.
|
|
142
|
+
export function saidNothingSheCanUse(session) {
|
|
143
|
+
return session?.result == null || !!session.resultIsError;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function countHelpers(session, obj) {
|
|
147
|
+
if (obj?.type !== 'system') return;
|
|
148
|
+
if (obj.subtype === 'task_started' && obj.task_id) {
|
|
149
|
+
(session.helperIds ??= new Set()).add(obj.task_id);
|
|
150
|
+
} else if (obj.subtype === 'task_updated' && obj.task_id) {
|
|
151
|
+
// Anything that is not still running ends it. The measured value is
|
|
152
|
+
// `completed`, and a helper that fails or is cancelled has equally stopped
|
|
153
|
+
// working, so the test is what it is NOT rather than a list of endings we
|
|
154
|
+
// would have to keep in step with the CLI.
|
|
155
|
+
const status = obj.patch?.status;
|
|
156
|
+
if (status && status !== 'in_progress' && status !== 'running') session.helperIds?.delete(obj.task_id);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The persisted trace line: fuller than the tail. Tool calls carry their key
|
|
161
|
+
// input (the file written, the command run), because "tool: Bash" answers
|
|
162
|
+
// nothing when the founder asks what a session actually did.
|
|
163
|
+
export function traceStreamLine(line) {
|
|
164
|
+
try {
|
|
165
|
+
const obj = JSON.parse(line);
|
|
166
|
+
const at = new Date().toISOString().slice(11, 19);
|
|
167
|
+
const hook = hookFailure(obj);
|
|
168
|
+
if (hook) return `${at} ${hook}`;
|
|
169
|
+
if (obj.type === 'assistant' && obj.message?.content) {
|
|
170
|
+
const parts = [];
|
|
171
|
+
for (const c of obj.message.content) {
|
|
172
|
+
if (c.type === 'text' && c.text?.trim()) parts.push(c.text.trim());
|
|
173
|
+
if (c.type === 'tool_use') {
|
|
174
|
+
const input = c.input ?? {};
|
|
175
|
+
const hint = input.file_path ?? input.path ?? input.command ?? input.title ?? input.description ?? input.id ?? '';
|
|
176
|
+
parts.push(`[${c.name}] ${String(hint).slice(0, 200)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return parts.length ? parts.map((p) => `${at} ${p}`).join('\n') : null;
|
|
180
|
+
}
|
|
181
|
+
if (obj.type === 'result') {
|
|
182
|
+
const flavor = `${obj.subtype ?? 'ok'}${obj.is_error ? ' ERROR' : ''} · ${obj.num_turns ?? '?'} turns`;
|
|
183
|
+
return `\n${at} == RESULT (${flavor}) ==\n${String(obj.result ?? '').slice(0, 4000)}`;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Stream-json lines are verbose; the In Progress tail wants the gist.
|
|
192
|
+
export function summarizeStreamLine(line) {
|
|
193
|
+
try {
|
|
194
|
+
const obj = JSON.parse(line);
|
|
195
|
+
const hook = hookFailure(obj);
|
|
196
|
+
if (hook) return hook;
|
|
197
|
+
if (obj.type === 'assistant' && obj.message?.content) {
|
|
198
|
+
const text = obj.message.content.filter((c) => c.type === 'text').map((c) => c.text).join(' ');
|
|
199
|
+
const tools = obj.message.content.filter((c) => c.type === 'tool_use').map((c) => c.name);
|
|
200
|
+
if (tools.length) return `tool: ${tools.join(', ')}`;
|
|
201
|
+
if (text) return text.slice(0, 300);
|
|
202
|
+
}
|
|
203
|
+
if (obj.type === 'result') return `done: ${String(obj.result ?? '').slice(0, 200)}`;
|
|
204
|
+
return null;
|
|
205
|
+
} catch {
|
|
206
|
+
return line.slice(0, 200);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { harnessDefinition } from '../../shared/harness-definitions.mjs';
|
|
6
|
+
import { EFFORT_LEVELS } from '../../shared/effort-levels.mjs';
|
|
7
|
+
import { storeRootEnv } from '../store/home.mjs';
|
|
8
|
+
import { resolveClaudeBin } from '../claude-bin.mjs';
|
|
9
|
+
import { claudeModelRows } from '../claude-models.mjs';
|
|
10
|
+
import { linkAccountTooling } from '../account-tooling.mjs';
|
|
11
|
+
import { streamInputArgs, attachClaudeInput } from '../claude-input.mjs';
|
|
12
|
+
import { claudeLaunchEnv } from '../claude-launch-env.mjs';
|
|
13
|
+
import { claudeActivity } from '../agent-activity.mjs';
|
|
14
|
+
import { captureStream, streamingText, summarizeStreamLine, traceStreamLine } from './claude-stream.mjs';
|
|
15
|
+
import { definitionMethods, commonUpdatePlan } from './common.mjs';
|
|
16
|
+
const definition = harnessDefinition('claude');
|
|
17
|
+
|
|
18
|
+
export const claudeHarness = {
|
|
19
|
+
...definitionMethods(definition),
|
|
20
|
+
discover: resolveClaudeBin,
|
|
21
|
+
launchEnv: claudeLaunchEnv,
|
|
22
|
+
models: ({ bin } = {}) => claudeModelRows({ bin: bin ?? null }).map(m => ({ id: m.alias, label: m.label })),
|
|
23
|
+
defaultModel: () => null,
|
|
24
|
+
workspaceModel: () => null,
|
|
25
|
+
profileHome: () => null,
|
|
26
|
+
effortLevels: () => EFFORT_LEVELS.map(e => e.id),
|
|
27
|
+
profiles: sup => sup._profiles(),
|
|
28
|
+
signedIn: () => true,
|
|
29
|
+
profileEnv: (env, profile) => profile === 'default' ? env : { ...env, [definition.homeEnv]: profile },
|
|
30
|
+
signInFiles({ folder, home }) {
|
|
31
|
+
if (folder) return definition.credentialFiles.map(file => path.join(folder, file));
|
|
32
|
+
const dir = path.join(home, '.claude');
|
|
33
|
+
return [path.join(home, '.claude.json'), ...definition.credentialFiles.map(file => path.join(dir, file))];
|
|
34
|
+
},
|
|
35
|
+
tooling: profile => linkAccountTooling(profile),
|
|
36
|
+
spawn(sup, plan, { cwd, item, profile, env }) {
|
|
37
|
+
// The binary is the one this definition names, not a field spelled out
|
|
38
|
+
// here: that is the last place an engine of this family needed code.
|
|
39
|
+
return spawn(sup.config[definition.binKey], streamInputArgs(definition.run, plan.args), {
|
|
40
|
+
cwd,
|
|
41
|
+
env: { ...claudeLaunchEnv(sup.config[definition.binKey], env), STORE_ACCOUNT_ID: sup.config.accountId, ...storeRootEnv(sup.config.storeRoot),
|
|
42
|
+
...(profile !== 'default' ? { [definition.homeEnv]: profile } : {}),
|
|
43
|
+
ZERO_PRODUCT: item.product, ZERO_ITEM: item.id, ...sup.memoryGateEnv(item) },
|
|
44
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
readers: () => ({ capture: captureStream, stream: streamingText, summarize: summarizeStreamLine, trace: traceStreamLine, activity: claudeActivity }),
|
|
48
|
+
subscribe(child, _session, absorb) {
|
|
49
|
+
let buffer = '';
|
|
50
|
+
child.stdout.on('data', data => {
|
|
51
|
+
buffer += data.toString();
|
|
52
|
+
let index;
|
|
53
|
+
while ((index = buffer.indexOf('\n')) >= 0) {
|
|
54
|
+
const line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
|
|
55
|
+
if (line.trim()) absorb(line);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
attachInput: child => attachClaudeInput(child),
|
|
60
|
+
transcript(sup, rec) {
|
|
61
|
+
if (!rec?.sessionId) return null;
|
|
62
|
+
const home = rec.profile && rec.profile !== 'default' ? rec.profile : path.join(os.homedir(), '.claude');
|
|
63
|
+
const projects = path.join(home, 'projects');
|
|
64
|
+
const slug = String(rec.cwd ?? '').replace(/[^a-zA-Z0-9]/g, '-');
|
|
65
|
+
const direct = path.join(projects, slug, `${rec.sessionId}.jsonl`);
|
|
66
|
+
if (fs.existsSync(direct)) return direct;
|
|
67
|
+
try { for (const dir of fs.readdirSync(projects)) {
|
|
68
|
+
const file = path.join(projects, dir, `${rec.sessionId}.jsonl`);
|
|
69
|
+
if (fs.existsSync(file)) return file;
|
|
70
|
+
} } catch {}
|
|
71
|
+
return null;
|
|
72
|
+
},
|
|
73
|
+
change: () => null,
|
|
74
|
+
release: () => {},
|
|
75
|
+
memoryGate: (sup, item) => sup.memoryGateEnv(item),
|
|
76
|
+
smallModelArgs: (prompt, model) => ['-p', prompt, '--model', model],
|
|
77
|
+
updateSettings: ({ home }) => {
|
|
78
|
+
try { return JSON.parse(fs.readFileSync(path.join(process.env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'), 'settings.json'), 'utf8')); }
|
|
79
|
+
catch { return {}; }
|
|
80
|
+
},
|
|
81
|
+
updatePlan: options => commonUpdatePlan(definition, options, ({ bin, real, home, channel, name }) =>
|
|
82
|
+
real.startsWith(path.join(home, '.local/share/claude/versions') + '/')
|
|
83
|
+
? { name, file: bin, args: ['update'], feed: `https://downloads.claude.ai/claude-code-releases/${channel}`, format: 'text' } : null),
|
|
84
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { harnessDefinition } from '../../shared/harness-definitions.mjs';
|
|
3
|
+
import { resolveCodexBin } from '../codex-bin.mjs';
|
|
4
|
+
import { codexLaunchEnv } from '../codex-launch-env.mjs';
|
|
5
|
+
import { codexModels, codexDefaultModel, codexModelLevels } from '../codex-models.mjs';
|
|
6
|
+
import { codexTranscriptFile, workerThreadParams } from '../codex-session.mjs';
|
|
7
|
+
import { compactCodexThread } from '../codex-compaction.mjs';
|
|
8
|
+
import { captureCodexEvent, codexStreamingText, summarizeCodexEvent, traceCodexEvent, rememberCodexChange, changeFromCodexTurn } from '../codex.mjs';
|
|
9
|
+
import { codexActivity } from '../agent-activity.mjs';
|
|
10
|
+
import { runCodexCommand } from '../provider-commands.mjs';
|
|
11
|
+
import { definitionMethods, commonUpdatePlan } from './common.mjs';
|
|
12
|
+
const definition = harnessDefinition('codex');
|
|
13
|
+
|
|
14
|
+
export const codexHarness = {
|
|
15
|
+
...definitionMethods(definition),
|
|
16
|
+
discover: resolveCodexBin,
|
|
17
|
+
launchEnv: codexLaunchEnv,
|
|
18
|
+
models: (options = {}) => codexModels(options),
|
|
19
|
+
defaultModel: (options = {}) => codexDefaultModel(options),
|
|
20
|
+
workspaceModel: sup => sup._codexWorkspaceModel(),
|
|
21
|
+
profileHome: (sup, profile) => sup._codexProfileHome(profile),
|
|
22
|
+
effortLevels: (model, options = {}) => codexModelLevels(model, options),
|
|
23
|
+
profiles: sup => sup._codexProfiles(),
|
|
24
|
+
signedIn: (sup, profile) => sup._codexSignedIn(profile),
|
|
25
|
+
profileEnv: (env, profile, sup) => ({ ...env, [definition.homeEnv]: sup._codexProfileHome(profile) }),
|
|
26
|
+
tooling: () => null,
|
|
27
|
+
// Codex carries the person's standing instructions as a thread's
|
|
28
|
+
// `developerInstructions`, so it needs neither a flag nor a file. It says so
|
|
29
|
+
// rather than inheriting the refusal meant for an engine that has nowhere to
|
|
30
|
+
// put them (main/harnesses/instructions-file.mjs).
|
|
31
|
+
instructions: () => ({ by: 'thread' }),
|
|
32
|
+
spawn: (sup, plan, context) => sup._spawnCodexWorker(plan, context),
|
|
33
|
+
readers: () => ({ capture: captureCodexEvent, stream: codexStreamingText, summarize: summarizeCodexEvent, trace: traceCodexEvent, activity: codexActivity }),
|
|
34
|
+
subscribe(child, session, absorb) {
|
|
35
|
+
child.on('event', (method, params) => { rememberCodexChange(session, method, params); absorb(method, params); });
|
|
36
|
+
},
|
|
37
|
+
attachInput: () => {},
|
|
38
|
+
transcript: (sup, rec) => rec?.sessionId ? codexTranscriptFile(rec.sessionId, { home: sup._codexProfileHome(rec.profile) }) : null,
|
|
39
|
+
change: (session, options) => changeFromCodexTurn(session.codexChange ?? [], options),
|
|
40
|
+
release: (sup, profile) => sup._releaseIdleCodex(profile),
|
|
41
|
+
usage: sup => sup.codexUsage(),
|
|
42
|
+
async compact(sup, rec, cwd) {
|
|
43
|
+
const { client, handshake } = sup._codexServer(sup._codexProfileHome(rec.profile ?? 'default'));
|
|
44
|
+
const mcpServers = await sup._codexIsolation(client, handshake);
|
|
45
|
+
const params = workerThreadParams({ cwd, mcpServers, storeServer: null });
|
|
46
|
+
return compactCodexThread({ server: client, threadId: rec.sessionId, threadParams: params });
|
|
47
|
+
},
|
|
48
|
+
settleApproval: (sup, ...args) => sup.settleCodexApproval(...args),
|
|
49
|
+
approvalChanged: (sup, ...args) => sup.codexCardChanged(...args),
|
|
50
|
+
memoryGate: sup => sup.codexMemoryGateEnv(),
|
|
51
|
+
async runCommand(sup, { home, ...operation }) {
|
|
52
|
+
const { client, handshake } = sup._codexServer(home);
|
|
53
|
+
await handshake;
|
|
54
|
+
return runCodexCommand({ server: client, ...operation });
|
|
55
|
+
},
|
|
56
|
+
updateSettings: () => ({}),
|
|
57
|
+
updatePlan: options => commonUpdatePlan(definition, options, ({ bin, real, name }) => {
|
|
58
|
+
const standalone = real.match(/^(.*)\/packages\/standalone\/(?:releases|current)\//);
|
|
59
|
+
return standalone ? { name, file: '/bin/bash',
|
|
60
|
+
args: ['-o', 'pipefail', '-c', '/usr/bin/curl -fsSL --connect-timeout 10 --max-time 60 https://chatgpt.com/codex/install.sh | /bin/sh'],
|
|
61
|
+
env: { CODEX_HOME: standalone[1], CODEX_INSTALL_DIR: path.dirname(bin) },
|
|
62
|
+
feed: 'https://releases.openai.com/codex/channels/latest', format: 'codex' } : null;
|
|
63
|
+
}),
|
|
64
|
+
smallModelArgs: prompt => ['exec', '--skip-git-repo-check', '--ephemeral', '--ignore-user-config', '--ignore-rules', '-s', 'read-only', '-c', 'model_reasoning_effort=low', prompt],
|
|
65
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { instructionsFor } from './instructions-file.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* THE COMMAND LINE THAT STARTS A WORKER, BUILT FROM THE DEFINITION'S `run`.
|
|
6
|
+
*
|
|
7
|
+
* Every flag name here used to be a literal in main/supervisor.mjs, so the
|
|
8
|
+
* supervisor knew one engine's CLI by heart and a second one of the same family
|
|
9
|
+
* could not be added without editing it. The order is the order that shipped,
|
|
10
|
+
* and `tests/a-second-stream-json-engine-is-only-a-definition` holds it there,
|
|
11
|
+
* because flag order is the kind of thing a refactor silently rewrites.
|
|
12
|
+
*
|
|
13
|
+
* ABSENT MEANS CANNOT, not "default". An engine whose `run` has no `effort` is
|
|
14
|
+
* handed no effort word even when the task names one, which is how a harness
|
|
15
|
+
* says it lacks a capability without the call site asking who it is. `fork` is
|
|
16
|
+
* the one pair: a fork is a fork OF something, so it needs a resume as well.
|
|
17
|
+
*/
|
|
18
|
+
export function workerArgsFrom(run, { prompt, grants = [], effort = null, system = null, resumeId = null, fork = false } = {}) {
|
|
19
|
+
if (!run) return [];
|
|
20
|
+
const args = [];
|
|
21
|
+
if (run.print) args.push(run.print, prompt);
|
|
22
|
+
if (run.outputFormat) args.push(run.outputFormat.flag, run.outputFormat.value);
|
|
23
|
+
if (run.stream) args.push(...run.stream);
|
|
24
|
+
args.push(...grants);
|
|
25
|
+
if (run.effort && effort) args.push(run.effort, effort);
|
|
26
|
+
if (run.system && system) args.push(run.system, system);
|
|
27
|
+
if (run.resume && resumeId) args.push(run.resume, resumeId);
|
|
28
|
+
if (run.fork && fork && resumeId) args.push(run.fork);
|
|
29
|
+
return args;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function definitionMethods(definition) {
|
|
33
|
+
const scrub = definition.scrubPatterns.map(pattern => new RegExp(pattern));
|
|
34
|
+
return {
|
|
35
|
+
...definition,
|
|
36
|
+
binary: config => config?.[definition.binKey] ?? null,
|
|
37
|
+
workerArgs: options => workerArgsFrom(definition.run, options),
|
|
38
|
+
// WHERE THE PERSON'S STANDING INSTRUCTIONS GO on this engine. A flag for
|
|
39
|
+
// the one that has one, a file named in the definition for the ones that
|
|
40
|
+
// do not, and a refusal for an engine with neither — because that engine
|
|
41
|
+
// would answer against rules it was never given, and nothing on screen
|
|
42
|
+
// would say so.
|
|
43
|
+
instructions: context => instructionsFor(definition, context),
|
|
44
|
+
launchEnv: (_bin, env) => ({ ...env }),
|
|
45
|
+
install: () => ({ file: definition.installer.shell, args: ['-c', `curl -fsSL ${definition.installer.url} | ${definition.installer.pipe}`] }),
|
|
46
|
+
signIn: bin => ({ file: bin, args: [...definition.loginArgs] }),
|
|
47
|
+
signInStatus: bin => ({ file: bin, args: [...definition.statusArgs] }),
|
|
48
|
+
accountKey: profile => `${definition.accountPrefix}${profile}`,
|
|
49
|
+
workerEnv(input = process.env) {
|
|
50
|
+
const env = { ...input };
|
|
51
|
+
for (const key of Object.keys(env)) if (scrub.some(pattern => pattern.test(key))) delete env[key];
|
|
52
|
+
return env;
|
|
53
|
+
},
|
|
54
|
+
signInFiles: ({ folder }) => folder ? definition.credentialFiles.map(file => path.join(folder, file)) : [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Installation-specific work remains in each adapter. The common installers
|
|
59
|
+
// use the same prefix and cask checks for every registered tool.
|
|
60
|
+
export function commonUpdatePlan(definition, { bin, real, home, channel = 'latest' }, native) {
|
|
61
|
+
if (!path.isAbsolute(bin || '') || !real) return null;
|
|
62
|
+
const name = definition.label;
|
|
63
|
+
const brew = real.match(/^(\/opt\/homebrew|\/usr\/local)\/Caskroom\/([^/]+)\//);
|
|
64
|
+
if (brew && definition.casks.includes(brew[2])) return {
|
|
65
|
+
name, file: `${brew[1]}/bin/brew`, args: ['upgrade', '--cask', brew[2]],
|
|
66
|
+
feed: `https://formulae.brew.sh/api/cask/${brew[2]}.json`, format: 'brew',
|
|
67
|
+
};
|
|
68
|
+
const tail = `/lib/node_modules/${definition.package}/`;
|
|
69
|
+
const at = real.indexOf(tail);
|
|
70
|
+
if (at > 0) {
|
|
71
|
+
const prefix = real.slice(0, at);
|
|
72
|
+
return { name, file: path.join(prefix, 'bin/npm'),
|
|
73
|
+
args: ['install', '--global', '--prefix', prefix, `${definition.package}@${channel}`],
|
|
74
|
+
env: { PATH: `${prefix}/bin:${process.env.PATH || ''}` },
|
|
75
|
+
feed: `https://registry.npmjs.org/${definition.package}/${channel}`, format: 'npm' };
|
|
76
|
+
}
|
|
77
|
+
return native?.({ bin, real, home, channel, name }) ?? null;
|
|
78
|
+
}
|