openzoo 0.49.10 → 0.49.12
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/lib/launch.js +103 -7
- package/package.json +1 -1
package/lib/launch.js
CHANGED
|
@@ -68,15 +68,63 @@ export function claudeCodeBinDirs(home = os.homedir()) {
|
|
|
68
68
|
];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Is this file something execvp can actually RUN?
|
|
73
|
+
*
|
|
74
|
+
* The +x bit says "you are allowed to run this", not "this is runnable". A
|
|
75
|
+
* truncated download, a half-written install, or a plain text stub all carry
|
|
76
|
+
* the bit happily, and execvp then fails with ENOEXEC — which surfaced to a
|
|
77
|
+
* user as the bare string `openzoo: spawn ENOEXEC` with no path, no cause, and
|
|
78
|
+
* nothing to act on. A script is runnable when it starts with a shebang; a
|
|
79
|
+
* binary when it carries a known image magic. Anything else is a bad candidate
|
|
80
|
+
* and we should keep looking rather than hand it to spawn.
|
|
81
|
+
*/
|
|
82
|
+
export function isRunnableExecutable(file) {
|
|
83
|
+
let fd;
|
|
84
|
+
try {
|
|
85
|
+
fd = fs.openSync(file, 'r');
|
|
86
|
+
const buf = Buffer.alloc(4);
|
|
87
|
+
const n = fs.readSync(fd, buf, 0, 4, 0);
|
|
88
|
+
if (n < 2) return false; // empty/truncated
|
|
89
|
+
if (buf[0] === 0x23 && buf[1] === 0x21) return true; // "#!" script
|
|
90
|
+
if (buf[0] === 0x4d && buf[1] === 0x5a) return true; // "MZ" PE (win)
|
|
91
|
+
if (n < 4) return false;
|
|
92
|
+
const be = buf.readUInt32BE(0);
|
|
93
|
+
if (be === 0x7f454c46) return true; // ELF
|
|
94
|
+
// Mach-O thin (32/64, either endianness) and universal/fat.
|
|
95
|
+
if (be === 0xfeedface || be === 0xfeedfacf) return true;
|
|
96
|
+
if (be === 0xcefaedfe || be === 0xcffaedfe) return true;
|
|
97
|
+
if (be === 0xcafebabe || be === 0xbebafeca) return true;
|
|
98
|
+
return false;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
} finally {
|
|
102
|
+
if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* ignore */ } }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the Claude Code TERMINAL CLI.
|
|
108
|
+
*
|
|
109
|
+
* NOTE THE SEARCH ORDER: claudeCodeBinDirs() is tried BEFORE $PATH, because a
|
|
110
|
+
* Finder-launched process has no shell PATH. The cost is that a broken
|
|
111
|
+
* ~/.local/bin/claude — the exact file a half-finished install leaves behind —
|
|
112
|
+
* outranks a working one further down $PATH. So candidates are now format
|
|
113
|
+
* checked, and a rejected one does not stop the search; it is recorded so the
|
|
114
|
+
* caller can say WHICH file was wrong instead of printing an errno.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveClaudeCli(env = process.env, rejected = []) {
|
|
73
117
|
const name = 'claude' + (process.platform === 'win32' ? '.cmd' : '');
|
|
74
118
|
const extras = env.OPENZOO_CLAUDE_PATH_ONLY === '1' ? [] : claudeCodeBinDirs();
|
|
75
119
|
const pathDirs = String(env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
76
120
|
for (const dir of [...new Set([...extras, ...pathDirs])]) {
|
|
77
121
|
if (!dir) continue;
|
|
78
122
|
const f = path.join(dir, name);
|
|
79
|
-
try { fs.accessSync(f, fs.constants.X_OK);
|
|
123
|
+
try { fs.accessSync(f, fs.constants.X_OK); } catch { continue; }
|
|
124
|
+
// A .cmd shim on Windows is dispatched by the shell, not execvp, so the
|
|
125
|
+
// magic-number test does not apply there.
|
|
126
|
+
if (process.platform === 'win32' || isRunnableExecutable(f)) return f;
|
|
127
|
+
rejected.push(f);
|
|
80
128
|
}
|
|
81
129
|
return null;
|
|
82
130
|
}
|
|
@@ -231,8 +279,25 @@ export async function launchClaude(argv) {
|
|
|
231
279
|
const env = claudeZooEnv(process.env, { base });
|
|
232
280
|
|
|
233
281
|
if (terminal) {
|
|
234
|
-
const
|
|
235
|
-
|
|
282
|
+
const rejected = [];
|
|
283
|
+
let cli = resolveClaudeCli(process.env, rejected);
|
|
284
|
+
if (!cli && rejected.length) {
|
|
285
|
+
// THE FORMAT CHECK IS A TIE-BREAKER, NOT A VETO.
|
|
286
|
+
//
|
|
287
|
+
// isRunnableExecutable() knows four magics. Something legitimate that it
|
|
288
|
+
// has never seen — a packaging format we did not anticipate — would be
|
|
289
|
+
// refused here even though the OS would have run it happily, and we would
|
|
290
|
+
// have told the user to reinstall a CLI that was fine. When there is a
|
|
291
|
+
// real alternative the check earns its keep by preferring the good file;
|
|
292
|
+
// when this is the ONLY candidate, defer to the kernel and let the spawn
|
|
293
|
+
// below decide. A wrong heuristic must never be the sole reason we refuse.
|
|
294
|
+
cli = rejected[0];
|
|
295
|
+
console.error(`openzoo: ${cli} does not look like a runnable executable — trying it anyway.`);
|
|
296
|
+
}
|
|
297
|
+
if (!cli) {
|
|
298
|
+
console.error('openzoo: `claude` CLI not found on PATH — install Claude Code, or drop --terminal for the desktop app');
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
236
301
|
// ALWAYS-ON HUD via Claude Code's NATIVE status line (the title bar is owned
|
|
237
302
|
// by Claude Code and gets overwritten, so OSC there is useless). We write a
|
|
238
303
|
// tiny status script that reads the proxy's /v1/info, and merge a statusLine
|
|
@@ -350,9 +415,40 @@ export async function launchClaude(argv) {
|
|
|
350
415
|
console.error(' spend : live in the status line below (bottom of screen); receipts in ~/.openzoo/proxy.log');
|
|
351
416
|
console.error(' every turn pays x402.');
|
|
352
417
|
console.error('');
|
|
353
|
-
|
|
418
|
+
// spawn() THROWS SYNCHRONOUSLY on ENOEXEC — the 'error' listener below is
|
|
419
|
+
// never reached, which is why this reached a user as the unprefixed
|
|
420
|
+
// `openzoo: spawn ENOEXEC` from the top-level handler. Catch it here where
|
|
421
|
+
// `cli` is in scope and the message can name the file.
|
|
422
|
+
let child;
|
|
423
|
+
try {
|
|
424
|
+
child = spawn(cli, rest, { stdio: 'inherit', env });
|
|
425
|
+
} catch (e) {
|
|
426
|
+
try { restoreStatus?.(); } catch { /* ignore */ }
|
|
427
|
+
console.error(`openzoo: cannot execute ${cli} (${e?.code || e?.message})`);
|
|
428
|
+
console.error(' it is marked executable but the OS refused to run it.');
|
|
429
|
+
console.error(` check: file ${cli}`);
|
|
430
|
+
console.error(` head -c 2 ${cli} | xxd # a healthy script starts 2321 ("#!")`);
|
|
431
|
+
console.error(' usually a truncated install, a wrong-architecture binary,');
|
|
432
|
+
console.error(' or a shebang pointing at an interpreter that no longer exists.');
|
|
433
|
+
console.error(' fix: npm i -g @anthropic-ai/claude-code');
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
354
436
|
child.on('exit', (c) => { try { restoreStatus?.(); } catch { /* ignore */ } process.exit(c ?? 0); });
|
|
355
|
-
child.on('error', (e) => {
|
|
437
|
+
child.on('error', (e) => {
|
|
438
|
+
// ENOEXEC reaches here only if the format check passed and the kernel
|
|
439
|
+
// still refused it — a wrong-architecture binary, or a shebang pointing
|
|
440
|
+
// at an interpreter that is gone. Name the file either way; the bare
|
|
441
|
+
// errno is unactionable.
|
|
442
|
+
if (e?.code === 'ENOEXEC' || e?.code === 'EACCES') {
|
|
443
|
+
console.error(`openzoo: cannot execute ${cli} (${e.code})`);
|
|
444
|
+
console.error(' the file exists and is marked executable, but the OS refused to run it.');
|
|
445
|
+
console.error(` check: head -1 ${cli} and file ${cli}`);
|
|
446
|
+
console.error(' a shebang pointing at a missing interpreter (e.g. a removed node) does this.');
|
|
447
|
+
} else {
|
|
448
|
+
console.error(`openzoo: could not launch claude (${cli}): ${e.message}`);
|
|
449
|
+
}
|
|
450
|
+
process.exit(1);
|
|
451
|
+
});
|
|
356
452
|
return;
|
|
357
453
|
}
|
|
358
454
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.12",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|