fix: restore bundled agent CLI resolution

- Upgrade EJClaw bundled Codex CLI to 0.124.0 for gpt-5.5 access
- Resolve Claude Agent SDK native binary packages via package.json
- Prefer glibc Claude binary before musl on Linux and align platform binary names
- Add regression coverage for optional package resolution
This commit is contained in:
ejclaw
2026-04-24 13:13:11 +09:00
parent f84dccfa10
commit 3eee842164
4 changed files with 74 additions and 20 deletions

View File

@@ -45,7 +45,7 @@ function platformCandidates(
return [
{
pkg: `@anthropic-ai/claude-agent-sdk-darwin-${arch}`,
file: arch === 'arm64' ? 'cli' : 'cli',
file: 'claude',
},
];
}
@@ -53,7 +53,7 @@ function platformCandidates(
return [
{
pkg: `@anthropic-ai/claude-agent-sdk-win32-${arch}`,
file: 'cli.exe',
file: 'claude.exe',
},
];
}
@@ -127,12 +127,22 @@ export function resolveBundledClaudeCodeExecutable(options?: {
* Default package directory resolver. Uses `require.resolve` against this
* module's location so it works regardless of whether agent-runner is invoked
* from its own node_modules layout or via a parent workspace.
*
* Important: the SDK's optional native-binary packages may not expose a bare
* package entrypoint, so `require.resolve(pkg)` can fail even when the package
* and binary are installed. Resolve `package.json` first, then fall back to the
* bare package only for package layouts that do expose an entrypoint.
*/
function defaultResolvePackageDir(pkg: string): string | null {
const req = createRequire(import.meta.url);
try {
const pkgJson = req.resolve(`${pkg}/package.json`);
return path.dirname(pkgJson);
} catch {
// Fall through to legacy/bare-entrypoint resolution below.
}
try {
// Resolve the package entrypoint instead of package.json because the SDK's
// exports map does not expose `./package.json`.
const req = createRequire(import.meta.url);
const entrypoint = req.resolve(pkg);
return path.dirname(entrypoint);
} catch {
@@ -142,5 +152,6 @@ function defaultResolvePackageDir(pkg: string): string | null {
export const __test__ = {
platformCandidates,
defaultResolvePackageDir,
ENV_OVERRIDE,
};

View File

@@ -1,8 +1,12 @@
import fs from 'fs';
import { createRequire } from 'module';
import path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import { resolveBundledClaudeCodeExecutable } from '../src/bundled-cli-path.js';
import {
__test__,
resolveBundledClaudeCodeExecutable,
} from '../src/bundled-cli-path.js';
describe('resolveBundledClaudeCodeExecutable', () => {
const origEnv = process.env.EJCLAW_CLAUDE_CLI_PATH;
@@ -83,10 +87,10 @@ describe('resolveBundledClaudeCodeExecutable', () => {
expect(result).toBe(path.join(muslDir, 'claude'));
});
it('resolves Darwin arm64 binary under `cli`', () => {
it('resolves Darwin arm64 binary under `claude`', () => {
const dir =
'/fake/node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64';
const existsSync = (p: string): boolean => p === path.join(dir, 'cli');
const existsSync = (p: string): boolean => p === path.join(dir, 'claude');
const result = resolveBundledClaudeCodeExecutable({
env: {},
existsSync,
@@ -95,13 +99,13 @@ describe('resolveBundledClaudeCodeExecutable', () => {
resolvePackageDir: (pkg) =>
pkg === '@anthropic-ai/claude-agent-sdk-darwin-arm64' ? dir : null,
});
expect(result).toBe(path.join(dir, 'cli'));
expect(result).toBe(path.join(dir, 'claude'));
});
it('resolves Windows binary under `cli.exe`', () => {
it('resolves Windows binary under `claude.exe`', () => {
const dir =
'C:\\\\fake\\\\node_modules\\\\@anthropic-ai\\\\claude-agent-sdk-win32-x64';
const binary = path.join(dir, 'cli.exe');
const binary = path.join(dir, 'claude.exe');
const existsSync = (p: string): boolean => p === binary;
const result = resolveBundledClaudeCodeExecutable({
env: {},
@@ -114,6 +118,17 @@ describe('resolveBundledClaudeCodeExecutable', () => {
expect(result).toBe(binary);
});
it('default resolver handles SDK optional packages without a bare entrypoint', () => {
if (process.platform !== 'linux' || process.arch !== 'x64') return;
const dir = __test__.defaultResolvePackageDir(
'@anthropic-ai/claude-agent-sdk-linux-x64',
);
if (!dir) return;
expect(fs.existsSync(path.join(dir, 'claude'))).toBe(true);
});
it('throws a descriptive error when no binary is found', () => {
const existsSync = () => false;
expect(() =>
@@ -127,6 +142,34 @@ describe('resolveBundledClaudeCodeExecutable', () => {
).toThrow(/Unable to locate a bundled Claude Code CLI binary/);
});
it('resolves the linux x64 optional package even when it has no JS entrypoint', () => {
if (process.platform !== 'linux' || process.arch !== 'x64') {
return;
}
const req = createRequire(import.meta.url);
let packageJsonPath: string;
try {
packageJsonPath = req.resolve(
'@anthropic-ai/claude-agent-sdk-linux-x64/package.json',
);
} catch {
return;
}
try {
req.resolve('@anthropic-ai/claude-agent-sdk-linux-x64');
return;
} catch {
// This is the regression case: package.json resolves, bare package does not.
}
const result = resolveBundledClaudeCodeExecutable({ env: {} });
expect(result).toBe(path.join(path.dirname(packageJsonPath), 'claude'));
expect(fs.existsSync(result)).toBe(true);
});
it('end-to-end: resolves the actual bundled binary on this host when installed', () => {
// Real-world smoke test. Some CI/host sandboxes do not install the optional
// per-platform SDK package, so treat "package not resolvable" as a host