agentbox-app 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +57 -20
- package/main/agent-updates.mjs +9 -22
- package/main/analytics.mjs +30 -0
- package/main/claude-bin.mjs +45 -311
- package/main/claude-input.mjs +9 -3
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +16 -16
- package/main/claude-usage.mjs +82 -25
- package/main/codex-bin.mjs +51 -249
- package/main/codex-session.mjs +38 -3
- package/main/codex-threads.mjs +41 -8
- package/main/codex-watch.mjs +67 -3
- package/main/config.mjs +26 -4
- package/main/corner-tag.mjs +489 -0
- package/main/crash-report.mjs +26 -3
- package/main/engine-bin.mjs +595 -0
- package/main/engine-setup.mjs +4 -15
- package/main/feedback.mjs +11 -6
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +84 -0
- package/main/harnesses/codex.mjs +65 -0
- package/main/harnesses/common.mjs +78 -0
- package/main/harnesses/instructions-file.mjs +116 -0
- package/main/harnesses/opencode.mjs +234 -0
- package/main/harnesses.mjs +65 -0
- package/main/importable-threads.mjs +38 -0
- package/main/ipc.mjs +165 -31
- package/main/local-app-frames.mjs +82 -0
- package/main/main.mjs +100 -4
- package/main/memory-gate-history.mjs +51 -12
- package/main/memory-gate-server.mjs +67 -11
- package/main/memory-gate.mjs +49 -8
- package/main/memory-log.mjs +124 -0
- package/main/notify.mjs +47 -4
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-link.mjs +551 -0
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +246 -0
- package/main/row-label.mjs +6 -6
- package/main/runs-on.mjs +32 -35
- package/main/serve.mjs +113 -9
- package/main/settings.mjs +148 -10
- package/main/shell-path.mjs +86 -0
- package/main/ship-queue.mjs +24 -6
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +27 -1
- package/main/start-hold.mjs +73 -0
- package/main/store.mjs +1 -1
- package/main/supervisor.mjs +832 -428
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +490 -98
- package/main/task-terminals.mjs +37 -7
- package/main/updater.mjs +71 -9
- package/main/worktree-include.mjs +494 -0
- package/package.json +10 -1
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
- package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
- package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
- package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
- package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
- package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
- package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
- package/renderer/dist/corner-tag.html +13 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +297 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/index.html +16 -3
- package/scripts/approval-server.sh +24 -5
- package/scripts/before-publish.mjs +12 -10
- package/shared/agent-import.mjs +2 -0
- package/shared/agents.d.mts +1 -0
- package/shared/bridge-map.mjs +18 -0
- package/shared/claude-commands.generated.mjs +6 -4
- package/shared/claude-models.generated.mjs +6 -4
- package/shared/corner-tag.d.mts +14 -0
- package/shared/corner-tag.mjs +141 -0
- package/shared/engines.mjs +20 -5
- package/shared/harness-definitions.mjs +103 -0
- package/shared/notify-rules.d.mts +25 -0
- package/shared/notify-rules.mjs +19 -0
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +2 -0
- package/shared/rank.mjs +50 -0
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/runs-on.d.mts +0 -10
- package/shared/runs-on.mjs +25 -49
- package/shared/shortcuts.mjs +10 -4
- package/shared/spawn-trouble.mjs +57 -6
- package/shared/terminal-input.d.mts +1 -0
- package/shared/terminal-input.mjs +30 -0
- package/shared/terminal-keys.d.mts +1 -0
- package/shared/terminal-keys.mjs +16 -0
- package/shared/terminal-size.d.mts +4 -0
- package/shared/terminal-size.mjs +29 -0
- package/shared/usage.d.mts +3 -0
- package/shared/usage.mjs +7 -0
- package/shared/where-it-runs-from.d.mts +43 -0
- package/shared/where-it-runs-from.mjs +155 -0
- package/shared/work-items.mjs +25 -1
- package/shared/work-lines.mjs +1 -1
- package/shared/xterm-invalid-range.d.mts +2 -0
- package/shared/xterm-invalid-range.mjs +58 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
- package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
package/main/agent-updates.mjs
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
// started only by the app's Update now action, never by a poll or terminal open.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
5
|
import {execFile} from 'node:child_process';
|
|
7
6
|
import {promisify} from 'node:util';
|
|
8
7
|
import {TaskTerminals,terminalEnv} from './task-terminals.mjs';
|
|
9
|
-
import {
|
|
8
|
+
import { harnessFor } from './harnesses.mjs';
|
|
9
|
+
import { harnessDefinition } from '../shared/harness-definitions.mjs';
|
|
10
10
|
const exec=promisify(execFile);
|
|
11
11
|
const valid=v=>typeof v==='string'&&/^\d+\.\d+\.\d+$/.test(v);
|
|
12
12
|
export function newerVersion(installed,latest){
|
|
@@ -15,28 +15,15 @@ export function newerVersion(installed,latest){
|
|
|
15
15
|
for(let i=0;i<3;i++){if(b[i]!==a[i])return b[i]>a[i];}return false;
|
|
16
16
|
}
|
|
17
17
|
export function updatePlan({engine,bin,real,home=os.homedir(),channel='latest'}){
|
|
18
|
-
if(!
|
|
19
|
-
|
|
20
|
-
const brew=real.match(/^(\/opt\/homebrew|\/usr\/local)\/Caskroom\/(claude-code(?:@latest)?|codex)\//);
|
|
21
|
-
if(brew&&((engine==='claude'&&brew[2].startsWith('claude-code'))||(engine==='codex'&&brew[2]==='codex')))
|
|
22
|
-
return {name,file:`${brew[1]}/bin/brew`,args:['upgrade','--cask',brew[2]],feed:`https://formulae.brew.sh/api/cask/${brew[2]}.json`,format:'brew'};
|
|
23
|
-
const pkg=engine==='claude'?'@anthropic-ai/claude-code':'@openai/codex';
|
|
24
|
-
const tail=`/lib/node_modules/${pkg}/`;
|
|
25
|
-
const at=real.indexOf(tail);
|
|
26
|
-
if(at>0){const prefix=real.slice(0,at);return {name,file:path.join(prefix,'bin/npm'),args:['install','--global','--prefix',prefix,`${pkg}@${channel}`],env:{PATH:`${prefix}/bin:${process.env.PATH||''}`},feed:`https://registry.npmjs.org/${pkg}/${channel}`,format:'npm'};}
|
|
27
|
-
if(engine==='claude'&&real.startsWith(path.join(home,'.local/share/claude/versions')+'/'))return {name,file:bin,args:['update'],feed:`https://downloads.claude.ai/claude-code-releases/${channel}`,format:'text'};
|
|
28
|
-
const standalone=real.match(/^(.*)\/packages\/standalone\/(?:releases|current)\//);
|
|
29
|
-
if(engine==='codex'&&standalone)return {name,file:'/bin/bash',args:['-o','pipefail','-c','/usr/bin/curl -fsSL --connect-timeout 10 --max-time 60 https://chatgpt.com/codex/install.sh | /bin/sh'],env:{CODEX_HOME:standalone[1],CODEX_INSTALL_DIR:path.dirname(bin)},feed:'https://releases.openai.com/codex/channels/latest',format:'codex'};
|
|
30
|
-
return null;
|
|
18
|
+
if(!harnessDefinition(engine))return null;
|
|
19
|
+
return harnessFor(engine).updatePlan({bin,real,home,channel});
|
|
31
20
|
}
|
|
32
|
-
const json=file=>{try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return {}}};
|
|
33
21
|
export async function installedAgent(engine,config){
|
|
34
|
-
const bin=
|
|
22
|
+
const harness=harnessFor(engine), bin=harness.binary(config);
|
|
35
23
|
if(!bin)throw Error('This agent is not installed.');
|
|
36
24
|
const home=config.home||os.homedir();
|
|
37
|
-
const settings=
|
|
38
|
-
const
|
|
39
|
-
const env=engine==='codex'?codexLaunchEnv(bin,cleanEnv):cleanEnv;
|
|
25
|
+
const settings=harness.updateSettings({home});
|
|
26
|
+
const env=harness.launchEnv(bin,terminalEnv());
|
|
40
27
|
// Version inspection must not itself auto-update anything.
|
|
41
28
|
const {stdout}=await exec(bin,['--version'],{timeout:8000,maxBuffer:16384,env:{...env,DISABLE_AUTOUPDATER:'1'}});
|
|
42
29
|
const version=stdout.match(/\b\d+\.\d+\.\d+(?:-[\w.-]+)?\b/)?.[0];
|
|
@@ -59,7 +46,7 @@ export class AgentUpdates{
|
|
|
59
46
|
}
|
|
60
47
|
status(engine){return this.entries.get(engine)||{engine,state:'unknown'};}
|
|
61
48
|
check(engine,force=false){
|
|
62
|
-
if(!
|
|
49
|
+
if(!harnessDefinition(engine))return Promise.reject(Error('Unknown agent.'));
|
|
63
50
|
if(this.starts.has(engine)||this.status(engine).state==='running')return Promise.resolve(this.status(engine));
|
|
64
51
|
if(this.checks.has(engine))return this.checks.get(engine);
|
|
65
52
|
const saved=this.status(engine);if(!force&&saved.checkedAt&&this.now()-saved.checkedAt<3600000)return Promise.resolve(saved);
|
|
@@ -105,7 +92,7 @@ export class AgentUpdates{
|
|
|
105
92
|
if(!this.plans.has(engine))throw Error('Click Update now first.');
|
|
106
93
|
switch(p.action){
|
|
107
94
|
case 'open':return this.terminals.read(engine,0);
|
|
108
|
-
case 'read':return this.terminals.read(engine,p.offset);
|
|
95
|
+
case 'read':return this.terminals.read(engine,p.offset,p.wait);
|
|
109
96
|
case 'write':return this.terminals.write(engine,p.data);
|
|
110
97
|
case 'resize':return this.terminals.resize(engine,p.cols,p.rows);
|
|
111
98
|
case 'close':return this.terminals.close(engine);
|
package/main/analytics.mjs
CHANGED
|
@@ -178,6 +178,36 @@ export function createAnalytics({ config, version, dir, client, env = process.en
|
|
|
178
178
|
report_version: Number(payload.v ?? 1),
|
|
179
179
|
error: payload.error ?? null,
|
|
180
180
|
happened_at: String(payload.ts ?? ''),
|
|
181
|
+
// THE VERSION IT CRASHED ON, WHICH IS NOT ALWAYS THE ONE SENDING IT.
|
|
182
|
+
// A report written by a dying process is drained on the NEXT launch,
|
|
183
|
+
// and that launch may be a newer app: `base.app_version` then files
|
|
184
|
+
// 0.1.11's crash against 0.1.12. It did, for three of the six window
|
|
185
|
+
// errors the launch report attributes to 0.1.12 (PostHog, 2026-10-08),
|
|
186
|
+
// and a version asked to answer for the bugs of the one before it is
|
|
187
|
+
// how a release that fixed something reads as a release that broke it.
|
|
188
|
+
// The report has carried the right answer all along.
|
|
189
|
+
app_version: String(payload.app?.version ?? base.app_version),
|
|
190
|
+
// WHY THE WINDOW DIED, which is the only thing a `renderer-gone`
|
|
191
|
+
// report is for and the one thing it was not carrying.
|
|
192
|
+
// `installCrashReports` collects Electron's `details.reason` and
|
|
193
|
+
// `details.exitCode` and says in its own comment why they are safe:
|
|
194
|
+
// the reason is Electron's closed vocabulary (`crashed`, `oom`,
|
|
195
|
+
// `killed`, `launch-failed` and the rest) and carries nothing of
|
|
196
|
+
// anyone's. They were written to disk and then dropped here, because
|
|
197
|
+
// this list names its properties by hand. So the launch report's one
|
|
198
|
+
// dead window says `render process gone` and nothing else, and `oom`
|
|
199
|
+
// after three and a half hours (a leak) could not be told apart from a
|
|
200
|
+
// hard fault (not a leak). Measured 2026-10-08.
|
|
201
|
+
//
|
|
202
|
+
// Absent rather than empty when there is nothing to say: a null reason
|
|
203
|
+
// on the thirteen crashes that never had one reads as an answer in a
|
|
204
|
+
// chart. A reason longer than Electron's longest word is a bug
|
|
205
|
+
// upstream rather than a reason, and is left out too.
|
|
206
|
+
...(typeof payload.reason === 'string' && payload.reason && payload.reason.length <= 40
|
|
207
|
+
? { crash_reason: payload.reason } : {}),
|
|
208
|
+
...(Number.isFinite(payload.exitCode) ? { exit_code: Number(payload.exitCode) } : {}),
|
|
209
|
+
...(typeof payload.processType === 'string' && payload.processType && payload.processType.length <= 40
|
|
210
|
+
? { process_type: payload.processType } : {}),
|
|
181
211
|
});
|
|
182
212
|
},
|
|
183
213
|
|
package/main/claude-bin.mjs
CHANGED
|
@@ -1,338 +1,72 @@
|
|
|
1
|
-
// Where Claude Code actually is, on a machine that is not
|
|
1
|
+
// Where Claude Code actually is, on a machine that is not ours.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// THIS FILE IS NOW THE NAMES AND NOTHING ELSE. The search, the registry entry
|
|
4
|
+
// that declares Claude Code, the measured notes about where it installs and the
|
|
5
|
+
// `found` / `certain` rule all live in main/engine-bin.mjs, which does the same
|
|
6
|
+
// job for every coding agent the app can run. Until 2026-10-08 this was 338
|
|
7
|
+
// lines and main/codex-bin.mjs was 279, and they were two copies of one idea:
|
|
8
|
+
// adding a harness meant a third file and three hundred lines of test.
|
|
8
9
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// decides which path gets spawned. `certain` is whether we are entitled to SAY
|
|
15
|
-
// it is missing, and everything below exists to make `certain` hard to earn.
|
|
16
|
-
//
|
|
17
|
-
// Four steps before an absence is ever claimed, cheapest first.
|
|
18
|
-
//
|
|
19
|
-
// KNOWN PATHS. A filesystem check is instant, spawns nothing, and is the right
|
|
20
|
-
// answer for most machines. The list is every normal home for this binary: the
|
|
21
|
-
// installer's own directory, Claude Code's local install (what
|
|
22
|
-
// `claude migrate-installer` leaves behind), Homebrew on Apple Silicon and on
|
|
23
|
-
// Intel, MacPorts, and the default bin of every package manager that is not npm.
|
|
24
|
-
//
|
|
25
|
-
// VERSION MANAGER DIRECTORIES. nvm, fnm, n and asdf each keep one bin directory
|
|
26
|
-
// PER INSTALLED NODE, under a name nobody can guess, so they are read rather
|
|
27
|
-
// than listed. A global npm install under any of them lands there and nowhere
|
|
28
|
-
// else, and this is the case the shell step used to be the only cover for.
|
|
29
|
-
//
|
|
30
|
-
// THE SHELL. The catch-all for installs no list can enumerate: volta, bun, a
|
|
31
|
-
// custom npm prefix. It must be a LOGIN, INTERACTIVE shell, because those
|
|
32
|
-
// managers put themselves on PATH from `.zprofile` and `.zshrc` and nowhere
|
|
33
|
-
// else. And it is needed at all because a double-clicked Electron app inherits
|
|
34
|
-
// launchd's PATH, which is /usr/bin:/bin:/usr/sbin:/sbin and has never heard of
|
|
35
|
-
// any of them. That is the whole reason an npm install is obvious in a terminal
|
|
36
|
-
// and invisible to the app. It is asked more than once now, because ONE probe
|
|
37
|
-
// that fails to start is not a fact about anybody's Mac.
|
|
10
|
+
// WHAT IS KEPT HERE is the vocabulary the rest of the app already imports --
|
|
11
|
+
// main/config.mjs, main/settings.mjs, scripts/read-claude-commands.mjs,
|
|
12
|
+
// scripts/read-claude-models.mjs -- plus `askShell` and `shellProbes`, which
|
|
13
|
+
// several tests reach for through this file because Claude Code is the binary the
|
|
14
|
+
// app will not open an inbox without.
|
|
38
15
|
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// this Mac, whatever our search of PATH did or did not manage, and the honest
|
|
44
|
-
// thing on the screen is silence rather than a sentence telling somebody to
|
|
45
|
-
// install software they are already running. A dangling `claude` symlink counts
|
|
46
|
-
// the same way, and it is a real state: the installer points ~/.local/bin/claude
|
|
47
|
-
// at a versioned file and swaps that file on every update.
|
|
16
|
+
// THE RULE THIS FILE ARGUED FOR AND engine-bin.mjs KEEPS: never tell somebody
|
|
17
|
+
// Claude Code is missing unless we are certain it is. `found` is whether a
|
|
18
|
+
// runnable file is really there, and it decides which path gets spawned.
|
|
19
|
+
// `certain` is whether we are entitled to SAY it is missing.
|
|
48
20
|
//
|
|
49
21
|
// Nothing here overrides the config file. An explicit `claudeBin` in
|
|
50
|
-
// zero.config.json wins untouched and is never searched around
|
|
51
|
-
// file promises explicit paths with no discovery, and a search that
|
|
52
|
-
// second-guesses a written setting would break that promise.
|
|
22
|
+
// zero.config.json wins untouched and is never searched around.
|
|
53
23
|
|
|
54
|
-
import fs from 'node:fs';
|
|
55
24
|
import os from 'node:os';
|
|
56
|
-
import
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// a machine that has always worked keeps resolving exactly as it did.
|
|
61
|
-
export function candidatePaths(home = os.homedir()) {
|
|
62
|
-
return [
|
|
63
|
-
path.join(home, '.local/bin/claude'), // the installer
|
|
64
|
-
path.join(home, '.claude/local/claude'), // claude migrate-installer
|
|
65
|
-
'/opt/homebrew/bin/claude', // Homebrew, Apple Silicon
|
|
66
|
-
'/usr/local/bin/claude', // Homebrew on Intel, and npm -g
|
|
67
|
-
path.join(home, '.bun/bin/claude'), // bun
|
|
68
|
-
path.join(home, '.volta/bin/claude'), // volta
|
|
69
|
-
path.join(home, '.asdf/shims/claude'), // asdf, through its shims
|
|
70
|
-
path.join(home, '.npm-global/bin/claude'), // the two npm prefixes people
|
|
71
|
-
path.join(home, '.npm-packages/bin/claude'), // actually write in a guide
|
|
72
|
-
path.join(home, '.yarn/bin/claude'), // yarn global
|
|
73
|
-
path.join(home, 'Library/pnpm/claude'), // pnpm's own bin on macOS
|
|
74
|
-
path.join(home, '.local/share/pnpm/claude'), // pnpm's own bin on Linux
|
|
75
|
-
path.join(home, '.deno/bin/claude'), // deno
|
|
76
|
-
path.join(home, 'bin/claude'), // a hand-rolled ~/bin on PATH
|
|
77
|
-
'/opt/local/bin/claude', // MacPorts
|
|
78
|
-
'/usr/bin/claude', // packaged by something system-wide
|
|
79
|
-
];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// One bin directory per installed Node, under a name no list can hold. Read
|
|
83
|
-
// rather than guessed, and a directory that is not there just contributes
|
|
84
|
-
// nothing. This is where a global npm install under nvm, fnm, n or asdf ends
|
|
85
|
-
// up, and until now only the shell could reach it.
|
|
86
|
-
export function versionedPaths(home = os.homedir(), readdir = defaultReaddir) {
|
|
87
|
-
const under = (dir, tail) => readdir(path.join(home, dir)).map((v) => path.join(home, dir, v, tail));
|
|
88
|
-
return [
|
|
89
|
-
...under('.nvm/versions/node', 'bin/claude'),
|
|
90
|
-
...under('.local/share/fnm/node-versions', 'installation/bin/claude'),
|
|
91
|
-
...under('Library/Application Support/fnm/node-versions', 'installation/bin/claude'),
|
|
92
|
-
...under('n/versions/node', 'bin/claude'),
|
|
93
|
-
...under('.asdf/installs/nodejs', 'bin/claude'),
|
|
94
|
-
];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function defaultReaddir(dir) {
|
|
98
|
-
try { return fs.readdirSync(dir); } catch { return []; }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// THE COPY THE CLAUDE APP KEEPS FOR ITSELF, one folder per version, newest
|
|
102
|
-
// first. Somebody who has only ever used Claude Code inside the Claude app has
|
|
103
|
-
// this and nothing else (w-9f6975906c, measured 2026-10-05: 2.1.209 there, and
|
|
104
|
-
// it shared the CLI's sign-in). It is searched LAST, after the shell, because it
|
|
105
|
-
// updates when the app does and a real install is fresher.
|
|
106
|
-
export function appCopyPaths(home = os.homedir(), readdir = defaultReaddir) {
|
|
107
|
-
const dir = path.join(home, 'Library/Application Support/Claude/claude-code');
|
|
108
|
-
return readdir(dir)
|
|
109
|
-
.filter((v) => /^\d+(\.\d+)*$/.test(v))
|
|
110
|
-
.sort(newestFirst)
|
|
111
|
-
.map((v) => path.join(dir, v, 'claude.app/Contents/MacOS/claude'));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function newestFirst(a, b) {
|
|
115
|
-
const x = a.split('.').map(Number), y = b.split('.').map(Number);
|
|
116
|
-
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
117
|
-
if ((x[i] ?? 0) !== (y[i] ?? 0)) return (y[i] ?? 0) - (x[i] ?? 0);
|
|
118
|
-
}
|
|
119
|
-
return 0;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// WHAT CLAUDE CODE LEAVES BEHIND. Any one of these means it has run on this
|
|
123
|
-
// Mac. None of them is a binary and none of them is ever spawned; they exist
|
|
124
|
-
// only to stop the app announcing an absence it cannot support.
|
|
125
|
-
export function evidencePaths(home = os.homedir()) {
|
|
126
|
-
return [
|
|
127
|
-
path.join(home, '.claude.json'), // its own config file
|
|
128
|
-
path.join(home, '.claude/settings.json'),
|
|
129
|
-
path.join(home, '.claude/projects'), // one folder per project it has opened
|
|
130
|
-
path.join(home, '.claude/statsig'),
|
|
131
|
-
path.join(home, '.claude/.credentials.json'), // it has been logged in
|
|
132
|
-
path.join(home, '.claude/history.jsonl'),
|
|
133
|
-
path.join(home, '.local/share/claude'), // the installer's version store
|
|
134
|
-
];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// A path that is a symlink to nothing. `existsSync` follows the link and says
|
|
138
|
-
// false, which is indistinguishable from nothing being there at all, and the
|
|
139
|
-
// two mean opposite things: an update that has swapped the versioned file out
|
|
140
|
-
// from under ~/.local/bin/claude leaves exactly this for a moment.
|
|
141
|
-
function defaultDangles(p) {
|
|
142
|
-
try { return !!fs.lstatSync(p) && !fs.existsSync(p); } catch { return false; }
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function installEvidence({ home = os.homedir(), exists = fs.existsSync, dangles = defaultDangles } = {}) {
|
|
146
|
-
for (const p of evidencePaths(home)) if (exists(p)) return p;
|
|
147
|
-
for (const p of candidatePaths(home)) if (dangles(p)) return p;
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
25
|
+
import {
|
|
26
|
+
ENGINES, findEngine, resolveEngine, forgetEngine, installEvidence as evidenceFor,
|
|
27
|
+
versionedBinPaths, askShell, shellProbes,
|
|
28
|
+
} from './engine-bin.mjs';
|
|
150
29
|
|
|
151
|
-
export
|
|
30
|
+
export { askShell, shellProbes };
|
|
152
31
|
|
|
153
|
-
|
|
154
|
-
// because it is the one that has her PATH in it; the other two because a
|
|
155
|
-
// $SHELL that will not start is a fact about $SHELL and not about Claude Code.
|
|
156
|
-
export function shellProbes(env = process.env) {
|
|
157
|
-
const preferred = env.SHELL || '/bin/zsh';
|
|
158
|
-
const out = [[preferred, '-lic', 4000], [preferred, '-lc', 2500]];
|
|
159
|
-
for (const other of ['/bin/zsh', '/bin/bash']) {
|
|
160
|
-
if (other !== preferred) out.push([other, '-lic', 2500]);
|
|
161
|
-
}
|
|
162
|
-
return out;
|
|
163
|
-
}
|
|
32
|
+
const CLAUDE = ENGINES.claude;
|
|
164
33
|
|
|
165
|
-
|
|
166
|
-
// Anything that is not an absolute path to a file that exists is treated as no
|
|
167
|
-
// path: `command -v` also prints shell functions and aliases, and a function
|
|
168
|
-
// name handed to spawn is a crash with a worse message than "not found".
|
|
169
|
-
//
|
|
170
|
-
// `answered` is the strict half. It is true only when every probe that ran came
|
|
171
|
-
// back with a real exit status, and at least one did. One shell that timed out
|
|
172
|
-
// alongside one that said no is not a machine we know anything about.
|
|
173
|
-
//
|
|
174
|
-
// `bin` is a parameter rather than the word `claude` baked into the command
|
|
175
|
-
// because Codex is a second engine (her ask, 2026-08-25) and it is found in the
|
|
176
|
-
// same way: same shells, same "two different silences" rule, same refusal to
|
|
177
|
-
// read a shell function name as a path. It is DEFAULTED, so every existing
|
|
178
|
-
// caller and every existing test asks about Claude Code as it always did, and
|
|
179
|
-
// that matters more here than it would anywhere else: this is the binary the
|
|
180
|
-
// app will not open an inbox without.
|
|
181
|
-
/**
|
|
182
|
-
* WHAT MAY BE HANDED TO A LOGIN SHELL, AND WHY THIS IS CHECKED AT ALL.
|
|
183
|
-
*
|
|
184
|
-
* The probe below interpolates `bin` into a string a LOGIN, INTERACTIVE shell
|
|
185
|
-
* executes, with her profile loaded, as her. Both callers today pass a literal
|
|
186
|
-
* ('claude' here, 'codex' in main/codex-bin.mjs), so nothing is exploitable
|
|
187
|
-
* now. But the entire direction of the second-engine work is a binary somebody
|
|
188
|
-
* can configure, and a `codexBin` read out of zero.config.json reaching this
|
|
189
|
-
* argument is one plausible slice away; at that point `command -v x; curl | sh`
|
|
190
|
-
* is a string in a file that runs itself.
|
|
191
|
-
*
|
|
192
|
-
* This function looks up program NAMES, so a name is all it takes. An allowlist
|
|
193
|
-
* rather than quoting, because quoting is a thing a later edit can undo without
|
|
194
|
-
* noticing and a shape check is not.
|
|
195
|
-
*/
|
|
196
|
-
const PROGRAM_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
34
|
+
export const INSTALL_URL = CLAUDE.installUrl;
|
|
197
35
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
let said = false;
|
|
203
|
-
let inconclusive = false;
|
|
204
|
-
for (const [shell, flags, timeout] of shellProbes(env)) {
|
|
205
|
-
let out = '';
|
|
206
|
-
try {
|
|
207
|
-
out = run(shell, [flags, `command -v ${bin}`], timeout);
|
|
208
|
-
} catch (e) {
|
|
209
|
-
// TWO DIFFERENT SILENCES, AND ONLY ONE OF THEM IS AN ANSWER.A shell that
|
|
210
|
-
// ran and exited non-zero has told us claude is not on PATH, and that is
|
|
211
|
-
// an answer. A shell that could not be started, or that ran past the
|
|
212
|
-
// timeout, has told us nothing at all, and reporting nothing as "not
|
|
213
|
-
// installed" is what put a line about missing software on the screen of
|
|
214
|
-
// people who have it.
|
|
215
|
-
if (exitedSayingNo(e)) said = true; else inconclusive = true;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
const line = String(out).split('\n').map((s) => s.trim()).filter(Boolean).pop();
|
|
219
|
-
if (line && line.startsWith('/') && exists(line)) return { path: line, answered: true };
|
|
220
|
-
said = true;
|
|
221
|
-
}
|
|
222
|
-
return { path: null, answered: said && !inconclusive };
|
|
36
|
+
/** Every normal home for this binary, in order; the first that exists wins. */
|
|
37
|
+
export function candidatePaths(home = os.homedir()) {
|
|
38
|
+
return CLAUDE.candidates(home, () => []);
|
|
223
39
|
}
|
|
224
40
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// of them is a fact about Claude Code.
|
|
229
|
-
function exitedSayingNo(e) {
|
|
230
|
-
if (!e || typeof e !== 'object') return false;
|
|
231
|
-
if (e.signal || e.code === 'ETIMEDOUT' || e.code === 'ENOENT') return false;
|
|
232
|
-
return typeof e.status === 'number';
|
|
41
|
+
/** One bin directory per installed Node, read rather than guessed. */
|
|
42
|
+
export function versionedPaths(home = os.homedir(), readdir = undefined) {
|
|
43
|
+
return versionedBinPaths('claude', home, readdir);
|
|
233
44
|
}
|
|
234
45
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
function shellSaid(result) {
|
|
239
|
-
if (typeof result === 'string') return { path: result, answered: true };
|
|
240
|
-
if (!result || typeof result !== 'object') return { path: null, answered: true };
|
|
241
|
-
return { path: result.path ?? null, answered: result.answered !== false };
|
|
46
|
+
/** The copy the Claude desktop app keeps for itself, newest version first. */
|
|
47
|
+
export function appCopyPaths(home = os.homedir(), readdir = undefined) {
|
|
48
|
+
return CLAUDE.appCopies(home, readdir);
|
|
242
49
|
}
|
|
243
50
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// stdin ignored on purpose: an interactive shell handed a live terminal can
|
|
248
|
-
// sit waiting for input, and this runs on the boot path.
|
|
249
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
250
|
-
timeout,
|
|
251
|
-
});
|
|
51
|
+
/** What Claude Code leaves behind. None of it is ever spawned. */
|
|
52
|
+
export function evidencePaths(home = os.homedir()) {
|
|
53
|
+
return CLAUDE.evidence(home, {});
|
|
252
54
|
}
|
|
253
55
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
//
|
|
257
|
-
// Returns { path, found, certain, from, searched, evidence }. `found` is the
|
|
258
|
-
// only claim about the world: it means a file is really there. `certain` says
|
|
259
|
-
// whether we are allowed to tell somebody it is NOT there, so a screen can stay
|
|
260
|
-
// quiet rather than announce an absence it has not established. They come apart
|
|
261
|
-
// in exactly one case that is not a failure, and it is the case worth keeping
|
|
262
|
-
// honest: she has set `claudeBin` to a path that does not exist, so the path
|
|
263
|
-
// stays HERS (anything that spawns it should fail naming the path she chose,
|
|
264
|
-
// not one we picked) while the screen says it was not found and offers the
|
|
265
|
-
// install link, the same as any other machine.
|
|
266
|
-
export function findClaudeBin({
|
|
267
|
-
configured = null,
|
|
268
|
-
home = os.homedir(),
|
|
269
|
-
exists = fs.existsSync,
|
|
270
|
-
shellLookup = askShell,
|
|
271
|
-
readdir = defaultReaddir,
|
|
272
|
-
dangles = defaultDangles,
|
|
273
|
-
} = {}) {
|
|
274
|
-
if (configured) {
|
|
275
|
-
return {
|
|
276
|
-
path: configured, found: exists(configured), certain: true, from: 'settings',
|
|
277
|
-
searched: [], evidence: null,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const searched = [...candidatePaths(home), ...versionedPaths(home, readdir)];
|
|
282
|
-
for (const candidate of searched) {
|
|
283
|
-
if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'disk', searched, evidence: null };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const fromShell = shellSaid(shellLookup());
|
|
287
|
-
if (fromShell.path) return { path: fromShell.path, found: true, certain: true, from: 'shell', searched, evidence: null };
|
|
288
|
-
|
|
289
|
-
for (const candidate of appCopyPaths(home, readdir)) {
|
|
290
|
-
if (exists(candidate)) return { path: candidate, found: true, certain: true, from: 'app', searched, evidence: null };
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// NOT FOUND, AND WHETHER THAT IS KNOWN. Every cheap path missed and every
|
|
294
|
-
// shell we could reach missed, so the last question is whether this Mac shows
|
|
295
|
-
// any sign of Claude Code anyway. If it does, we looked in the wrong places
|
|
296
|
-
// rather than at an empty machine, and nothing may be said out loud.
|
|
297
|
-
const evidence = installEvidence({ home, exists, dangles });
|
|
298
|
-
return {
|
|
299
|
-
path: null,
|
|
300
|
-
found: false,
|
|
301
|
-
certain: fromShell.answered && !evidence,
|
|
302
|
-
from: null,
|
|
303
|
-
searched,
|
|
304
|
-
evidence,
|
|
305
|
-
};
|
|
56
|
+
export function installEvidence({ home = os.homedir(), exists, dangles } = {}) {
|
|
57
|
+
return evidenceFor('claude', { home, env: {}, exists, dangles });
|
|
306
58
|
}
|
|
307
59
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
// up telling someone to install Claude Code and then still saying "not found"
|
|
312
|
-
// after they have. The sentence on that screen makes a promise and a cached
|
|
313
|
-
// answer breaks it.
|
|
314
|
-
//
|
|
315
|
-
// The shell is the one part worth holding, because it is up to three
|
|
316
|
-
// subprocesses and a screen she toggles a switch on reads settings again on
|
|
317
|
-
// every write. Thirty seconds is long enough that a burst of writes pays for it
|
|
318
|
-
// once, and short enough that it is never the reason someone is still stuck.
|
|
319
|
-
|
|
320
|
-
const SHELL_TTL = 30_000;
|
|
321
|
-
let shellAnswer = null;
|
|
322
|
-
let shellAskedAt = 0;
|
|
323
|
-
|
|
324
|
-
function rememberedShellLookup(now, lookup) {
|
|
325
|
-
if (shellAskedAt && now - shellAskedAt < SHELL_TTL) return shellAnswer;
|
|
326
|
-
shellAnswer = lookup();
|
|
327
|
-
shellAskedAt = now;
|
|
328
|
-
return shellAnswer;
|
|
60
|
+
/** The whole search. See main/engine-bin.mjs for what each answer means. */
|
|
61
|
+
export function findClaudeBin(where = {}) {
|
|
62
|
+
return findEngine('claude', where);
|
|
329
63
|
}
|
|
330
64
|
|
|
331
|
-
|
|
332
|
-
|
|
65
|
+
/** The same search with the shell step held for thirty seconds. */
|
|
66
|
+
export function resolveClaudeBin(configured = null, where = {}) {
|
|
67
|
+
return resolveEngine('claude', configured, where);
|
|
333
68
|
}
|
|
334
69
|
|
|
335
70
|
export function forgetClaudeBin() {
|
|
336
|
-
|
|
337
|
-
shellAskedAt = 0;
|
|
71
|
+
forgetEngine('claude');
|
|
338
72
|
}
|
package/main/claude-input.mjs
CHANGED
|
@@ -16,12 +16,18 @@ function turnClosed(message) {
|
|
|
16
16
|
// before the session is closed anyway. Measured wakes arrive in about a second.
|
|
17
17
|
const WAKE_GRACE_MS = 2 * 60 * 1000;
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// THE PROMPT LEAVES THE LINE WHEN REPLIES ARRIVE ON STDIN. Both the print flag
|
|
20
|
+
// and the input-format flags are read off the engine's `run` definition, so an
|
|
21
|
+
// engine of this family that spells either of them differently is a definition
|
|
22
|
+
// and not a change here. An engine with no `input` cannot take corrections on
|
|
23
|
+
// stdin, and gets its line back as it was.
|
|
24
|
+
export function streamInputArgs(run, args) {
|
|
25
|
+
if (!run?.input) return [...args];
|
|
20
26
|
const copy = [...args];
|
|
21
|
-
const index = copy.indexOf(
|
|
27
|
+
const index = copy.indexOf(run.print);
|
|
22
28
|
if (index < 0) throw Error('Missing print prompt');
|
|
23
29
|
copy.splice(index + 1, 1);
|
|
24
|
-
return [...copy,
|
|
30
|
+
return [...copy, ...run.input];
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
// Replay-user-messages acknowledges the exact UUID when Claude consumes it.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// An npm Claude Code from before the package shipped a native binary is a
|
|
2
|
+
// `#!/usr/bin/env node` script. Opened from the Dock, the app's PATH is
|
|
3
|
+
// launchd's four folders, and with nvm lazy-loaded no profile adds node, so the
|
|
4
|
+
// app found that Claude Code and every worker died with
|
|
5
|
+
// `env: node: No such file or directory`
|
|
6
|
+
// (tests/an-old-npm-claude-code-starts-as-a-worker-from-the-dock).
|
|
7
|
+
//
|
|
8
|
+
// Narrower than main/codex-launch-env.mjs on purpose. A worker's PATH is the
|
|
9
|
+
// PATH its agent's own commands run under, so it is changed only when this
|
|
10
|
+
// binary needs node and none is on it, and the node found is added at the end,
|
|
11
|
+
// where it cannot shadow anything the person's own PATH chose.
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { candidatePaths, versionedPaths } from './claude-bin.mjs';
|
|
16
|
+
|
|
17
|
+
function executable(file) {
|
|
18
|
+
try {
|
|
19
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
20
|
+
return fs.statSync(file).isFile();
|
|
21
|
+
} catch { return false; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Whether `bin`, followed through its links, is a script run by node. */
|
|
25
|
+
function runsOnNode(bin) {
|
|
26
|
+
try {
|
|
27
|
+
const fd = fs.openSync(fs.realpathSync(bin), 'r');
|
|
28
|
+
const head = Buffer.alloc(128);
|
|
29
|
+
const read = fs.readSync(fd, head, 0, head.length, 0);
|
|
30
|
+
fs.closeSync(fd);
|
|
31
|
+
const first = head.subarray(0, read).toString('utf8').split('\n')[0];
|
|
32
|
+
return /^#!.*\bnode\b/.test(first);
|
|
33
|
+
} catch { return false; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function claudeLaunchEnv(bin, source = process.env, {
|
|
37
|
+
home = source.HOME || os.homedir(), exists = executable, readdir,
|
|
38
|
+
} = {}) {
|
|
39
|
+
if (!bin || !runsOnNode(bin)) return { ...source };
|
|
40
|
+
const inherited = (source.PATH || '').split(path.delimiter).filter(Boolean);
|
|
41
|
+
if (inherited.some(dir => exists(path.join(dir, 'node')))) return { ...source };
|
|
42
|
+
// The node npm installed it with sits beside the link, or beside the real
|
|
43
|
+
// file's package; then the places a Node install usually lives.
|
|
44
|
+
let real = bin;
|
|
45
|
+
try { real = fs.realpathSync(bin); } catch { /* keep the link */ }
|
|
46
|
+
const directories = [...new Set([
|
|
47
|
+
path.dirname(bin), path.dirname(real),
|
|
48
|
+
...candidatePaths(home).map(path.dirname),
|
|
49
|
+
...versionedPaths(home, readdir).map(path.dirname),
|
|
50
|
+
])];
|
|
51
|
+
const nodeDir = directories.find(dir => exists(path.join(dir, 'node')));
|
|
52
|
+
if (!nodeDir) return { ...source };
|
|
53
|
+
return { ...source, PATH: [...inherited, nodeDir].join(path.delimiter) };
|
|
54
|
+
}
|