feat(server): bundle Codex and OpenCode binaries instead of requiring global installs
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { execSync, spawn } from "node:child_process";
|
||||
import { spawn } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import { promises } from "node:fs";
|
||||
@@ -69,8 +69,6 @@ import type {
|
||||
} from "../agent-sdk-types.js";
|
||||
import {
|
||||
applyProviderEnv,
|
||||
findExecutable,
|
||||
isProviderCommandAvailable,
|
||||
type ProviderRuntimeSettings,
|
||||
} from "../provider-launch-config.js";
|
||||
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
||||
@@ -358,22 +356,11 @@ type ClaudeAgentClientOptions = {
|
||||
|
||||
type ClaudeAgentSessionOptions = {
|
||||
defaults?: { agents?: Record<string, AgentDefinition> };
|
||||
claudePath: string | null;
|
||||
runtimeSettings?: ProviderRuntimeSettings;
|
||||
handle?: AgentPersistenceHandle;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
function resolveClaudeBinary(): string {
|
||||
const claudePath = findExecutable("claude");
|
||||
if (claudePath) {
|
||||
return claudePath;
|
||||
}
|
||||
throw new Error(
|
||||
"Claude CLI not found. Install claude or configure agents.providers.claude.command.mode='replace'."
|
||||
);
|
||||
}
|
||||
|
||||
function resolveClaudeSpawnCommand(
|
||||
spawnOptions: SpawnOptions,
|
||||
runtimeSettings?: ProviderRuntimeSettings
|
||||
@@ -1400,31 +1387,18 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
|
||||
private readonly defaults?: { agents?: Record<string, AgentDefinition> };
|
||||
private readonly logger: Logger;
|
||||
private readonly claudePath: string | null;
|
||||
private readonly runtimeSettings?: ProviderRuntimeSettings;
|
||||
|
||||
constructor(options: ClaudeAgentClientOptions) {
|
||||
this.defaults = options.defaults;
|
||||
this.logger = options.logger.child({ module: "agent", provider: "claude" });
|
||||
this.runtimeSettings = options.runtimeSettings;
|
||||
this.claudePath = findExecutable("claude");
|
||||
if (this.claudePath) {
|
||||
try {
|
||||
const version = execSync(`${this.claudePath} --version`, { encoding: "utf8" }).trim();
|
||||
this.logger.trace({ claudePath: this.claudePath, version }, "Resolved Claude binary");
|
||||
} catch {
|
||||
this.logger.trace({ claudePath: this.claudePath }, "Resolved Claude binary (version unknown)");
|
||||
}
|
||||
} else {
|
||||
this.logger.trace("Claude binary not found in PATH; SDK will use bundled binary");
|
||||
}
|
||||
}
|
||||
|
||||
async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
const claudeConfig = this.assertConfig(config);
|
||||
return new ClaudeAgentSession(claudeConfig, {
|
||||
defaults: this.defaults,
|
||||
claudePath: this.claudePath,
|
||||
runtimeSettings: this.runtimeSettings,
|
||||
logger: this.logger,
|
||||
});
|
||||
@@ -1443,7 +1417,6 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
const claudeConfig = this.assertConfig(mergedConfig);
|
||||
return new ClaudeAgentSession(claudeConfig, {
|
||||
defaults: this.defaults,
|
||||
claudePath: this.claudePath,
|
||||
runtimeSettings: this.runtimeSettings,
|
||||
handle,
|
||||
logger: this.logger,
|
||||
@@ -1478,11 +1451,11 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
const commandConfig = this.runtimeSettings?.command;
|
||||
if (commandConfig?.mode === "replace") {
|
||||
return isProviderCommandAvailable(commandConfig, resolveClaudeBinary);
|
||||
const command = this.runtimeSettings?.command;
|
||||
if (command?.mode === "replace") {
|
||||
return fs.existsSync(command.argv[0]);
|
||||
}
|
||||
return this.claudePath !== null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private assertConfig(config: AgentSessionConfig): ClaudeAgentConfig {
|
||||
@@ -1499,7 +1472,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
|
||||
private readonly config: ClaudeAgentConfig;
|
||||
private readonly defaults?: { agents?: Record<string, AgentDefinition> };
|
||||
private readonly claudePath: string | null;
|
||||
private readonly runtimeSettings?: ProviderRuntimeSettings;
|
||||
private readonly logger: Logger;
|
||||
private query: Query | null = null;
|
||||
@@ -1546,7 +1518,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
) {
|
||||
this.config = config;
|
||||
this.defaults = options.defaults;
|
||||
this.claudePath = options.claudePath;
|
||||
this.runtimeSettings = options.runtimeSettings;
|
||||
this.logger = options.logger;
|
||||
const handle = options.handle;
|
||||
@@ -2319,7 +2290,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
permissionMode: this.currentMode,
|
||||
agents: this.defaults?.agents,
|
||||
canUseTool: this.handlePermissionRequest,
|
||||
...(this.claudePath ? { pathToClaudeCodeExecutable: this.claudePath } : {}),
|
||||
// Use Claude Code preset system prompt and load CLAUDE.md files
|
||||
// Append provider-agnostic system prompt and orchestrator instructions for agents.
|
||||
systemPrompt: {
|
||||
|
||||
@@ -27,8 +27,10 @@ import type { Logger } from "pino";
|
||||
import { execSync, spawn } from "node:child_process";
|
||||
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Dirent } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
@@ -40,8 +42,6 @@ import {
|
||||
} from "./codex/tool-call-mapper.js";
|
||||
import {
|
||||
applyProviderEnv,
|
||||
findExecutable,
|
||||
isProviderCommandAvailable,
|
||||
resolveProviderCommandPrefix,
|
||||
type ProviderRuntimeSettings,
|
||||
} from "../provider-launch-config.js";
|
||||
@@ -150,13 +150,52 @@ function mergeCodexConfiguredDefaults(
|
||||
};
|
||||
}
|
||||
|
||||
const CODEX_TARGET_TRIPLE: Record<string, Record<string, string>> = {
|
||||
linux: { x64: "x86_64-unknown-linux-musl", arm64: "aarch64-unknown-linux-musl" },
|
||||
darwin: { x64: "x86_64-apple-darwin", arm64: "aarch64-apple-darwin" },
|
||||
win32: { x64: "x86_64-pc-windows-msvc", arm64: "aarch64-pc-windows-msvc" },
|
||||
};
|
||||
|
||||
const CODEX_PLATFORM_PACKAGE: Record<string, string> = {
|
||||
"x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
|
||||
"aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
|
||||
"x86_64-apple-darwin": "@openai/codex-darwin-x64",
|
||||
"aarch64-apple-darwin": "@openai/codex-darwin-arm64",
|
||||
"x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
|
||||
"aarch64-pc-windows-msvc": "@openai/codex-win32-arm64",
|
||||
};
|
||||
|
||||
function resolveBundledCodexBinary(): string | null {
|
||||
const targetTriple = CODEX_TARGET_TRIPLE[process.platform]?.[process.arch];
|
||||
if (!targetTriple) {
|
||||
return null;
|
||||
}
|
||||
const platformPackage = CODEX_PLATFORM_PACKAGE[targetTriple];
|
||||
if (!platformPackage) {
|
||||
return null;
|
||||
}
|
||||
const binaryName = process.platform === "win32" ? "codex.exe" : "codex";
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageJsonPath = require.resolve(`${platformPackage}/package.json`);
|
||||
const vendorRoot = path.join(path.dirname(packageJsonPath), "vendor");
|
||||
const binaryPath = path.join(vendorRoot, targetTriple, "codex", binaryName);
|
||||
if (existsSync(binaryPath)) {
|
||||
return binaryPath;
|
||||
}
|
||||
} catch {
|
||||
// Platform package not installed
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveCodexBinary(): string {
|
||||
const codexPath = findExecutable("codex");
|
||||
if (codexPath) {
|
||||
return codexPath;
|
||||
const bundled = resolveBundledCodexBinary();
|
||||
if (bundled) {
|
||||
return bundled;
|
||||
}
|
||||
throw new Error(
|
||||
"Codex CLI not found. Please install codex globally: npm install -g @openai/codex"
|
||||
`Bundled Codex binary not found for platform ${process.platform}-${process.arch}. Ensure the @openai/codex package and its platform-specific optional dependency are installed.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3235,6 +3274,10 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return isProviderCommandAvailable(this.runtimeSettings?.command, resolveCodexBinary);
|
||||
const command = this.runtimeSettings?.command;
|
||||
if (command?.mode === "replace") {
|
||||
return existsSync(command.argv[0]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client";
|
||||
import net from "node:net";
|
||||
import type { Logger } from "pino";
|
||||
@@ -29,8 +33,6 @@ import type {
|
||||
} from "../agent-sdk-types.js";
|
||||
import {
|
||||
applyProviderEnv,
|
||||
findExecutable,
|
||||
isProviderCommandAvailable,
|
||||
resolveProviderCommandPrefix,
|
||||
type ProviderRuntimeSettings,
|
||||
} from "../provider-launch-config.js";
|
||||
@@ -150,13 +152,40 @@ const OpencodeToolPartToTimelineItemSchema = OpencodeToolPartTimelineEnvelopeSch
|
||||
})
|
||||
);
|
||||
|
||||
const OPENCODE_PLATFORM_MAP: Record<string, string> = {
|
||||
darwin: "darwin",
|
||||
linux: "linux",
|
||||
win32: "windows",
|
||||
};
|
||||
|
||||
function resolveBundledOpenCodeBinary(): string | null {
|
||||
const platform = OPENCODE_PLATFORM_MAP[os.platform()];
|
||||
if (!platform) {
|
||||
return null;
|
||||
}
|
||||
const arch = os.arch();
|
||||
const packageName = `opencode-${platform}-${arch}`;
|
||||
const binaryName = platform === "windows" ? "opencode.exe" : "opencode";
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
||||
const binaryPath = path.join(path.dirname(packageJsonPath), "bin", binaryName);
|
||||
if (existsSync(binaryPath)) {
|
||||
return binaryPath;
|
||||
}
|
||||
} catch {
|
||||
// Platform package not installed
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveOpenCodeBinary(): string {
|
||||
const opencodePath = findExecutable("opencode");
|
||||
if (opencodePath) {
|
||||
return opencodePath;
|
||||
const bundled = resolveBundledOpenCodeBinary();
|
||||
if (bundled) {
|
||||
return bundled;
|
||||
}
|
||||
throw new Error(
|
||||
"OpenCode CLI not found. Please install opencode globally so Paseo can launch the provider."
|
||||
`Bundled OpenCode binary not found for platform ${os.platform()}-${os.arch()}. Ensure the opencode-ai package and its platform-specific optional dependency are installed.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -522,10 +551,11 @@ export class OpenCodeAgentClient implements AgentClient {
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return isProviderCommandAvailable(
|
||||
this.runtimeSettings?.command,
|
||||
resolveOpenCodeBinary
|
||||
);
|
||||
const command = this.runtimeSettings?.command;
|
||||
if (command?.mode === "replace") {
|
||||
return existsSync(command.argv[0]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private assertConfig(config: AgentSessionConfig): OpenCodeAgentConfig {
|
||||
|
||||
Reference in New Issue
Block a user