feat: add provider runtime launcher settings for agent CLIs
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import {
|
||||
resolveProviderCommandPrefix,
|
||||
applyProviderEnv,
|
||||
type ProviderRuntimeSettings,
|
||||
} from "./provider-launch-config.js";
|
||||
|
||||
describe("resolveProviderCommandPrefix", () => {
|
||||
test("uses resolved default command in default mode", () => {
|
||||
const resolveDefault = vi.fn(() => "/usr/local/bin/claude");
|
||||
|
||||
const resolved = resolveProviderCommandPrefix(undefined, resolveDefault);
|
||||
|
||||
expect(resolveDefault).toHaveBeenCalledTimes(1);
|
||||
expect(resolved).toEqual({ command: "/usr/local/bin/claude", args: [] });
|
||||
});
|
||||
|
||||
test("appends args in append mode", () => {
|
||||
const resolveDefault = vi.fn(() => "/usr/local/bin/claude");
|
||||
|
||||
const resolved = resolveProviderCommandPrefix(
|
||||
{
|
||||
mode: "append",
|
||||
args: ["--chrome"],
|
||||
},
|
||||
resolveDefault
|
||||
);
|
||||
|
||||
expect(resolveDefault).toHaveBeenCalledTimes(1);
|
||||
expect(resolved).toEqual({
|
||||
command: "/usr/local/bin/claude",
|
||||
args: ["--chrome"],
|
||||
});
|
||||
});
|
||||
|
||||
test("replaces command in replace mode without resolving default", () => {
|
||||
const resolveDefault = vi.fn(() => "/usr/local/bin/claude");
|
||||
|
||||
const resolved = resolveProviderCommandPrefix(
|
||||
{
|
||||
mode: "replace",
|
||||
argv: ["docker", "run", "--rm", "my-wrapper"],
|
||||
},
|
||||
resolveDefault
|
||||
);
|
||||
|
||||
expect(resolveDefault).not.toHaveBeenCalled();
|
||||
expect(resolved).toEqual({
|
||||
command: "docker",
|
||||
args: ["run", "--rm", "my-wrapper"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyProviderEnv", () => {
|
||||
test("merges provider env overrides", () => {
|
||||
const base = {
|
||||
PATH: "/usr/bin",
|
||||
HOME: "/tmp",
|
||||
};
|
||||
const runtime: ProviderRuntimeSettings = {
|
||||
env: {
|
||||
HOME: "/custom/home",
|
||||
FOO: "bar",
|
||||
},
|
||||
};
|
||||
|
||||
const env = applyProviderEnv(base, runtime);
|
||||
|
||||
expect(env).toEqual({
|
||||
PATH: "/usr/bin",
|
||||
HOME: "/custom/home",
|
||||
FOO: "bar",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { AgentProvider } from "./agent-sdk-types.js";
|
||||
import { AgentProviderSchema } from "./provider-manifest.js";
|
||||
|
||||
const ProviderCommandDefaultSchema = z
|
||||
.object({
|
||||
mode: z.literal("default"),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ProviderCommandAppendSchema = z
|
||||
.object({
|
||||
mode: z.literal("append"),
|
||||
args: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ProviderCommandReplaceSchema = z
|
||||
.object({
|
||||
mode: z.literal("replace"),
|
||||
argv: z.array(z.string().min(1)).min(1),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const ProviderCommandSchema = z.discriminatedUnion("mode", [
|
||||
ProviderCommandDefaultSchema,
|
||||
ProviderCommandAppendSchema,
|
||||
ProviderCommandReplaceSchema,
|
||||
]);
|
||||
|
||||
export const ProviderRuntimeSettingsSchema = z
|
||||
.object({
|
||||
command: ProviderCommandSchema.optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const AgentProviderRuntimeSettingsMapSchema = z.record(
|
||||
AgentProviderSchema,
|
||||
ProviderRuntimeSettingsSchema
|
||||
);
|
||||
|
||||
export type ProviderCommand = z.infer<typeof ProviderCommandSchema>;
|
||||
export type ProviderRuntimeSettings = z.infer<typeof ProviderRuntimeSettingsSchema>;
|
||||
export type AgentProviderRuntimeSettingsMap = Partial<
|
||||
Record<AgentProvider, ProviderRuntimeSettings>
|
||||
>;
|
||||
|
||||
export type ProviderCommandPrefix = {
|
||||
command: string;
|
||||
args: string[];
|
||||
};
|
||||
|
||||
export function resolveProviderCommandPrefix(
|
||||
commandConfig: ProviderCommand | undefined,
|
||||
resolveDefaultCommand: () => string
|
||||
): ProviderCommandPrefix {
|
||||
if (!commandConfig || commandConfig.mode === "default") {
|
||||
return {
|
||||
command: resolveDefaultCommand(),
|
||||
args: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (commandConfig.mode === "append") {
|
||||
return {
|
||||
command: resolveDefaultCommand(),
|
||||
args: [...(commandConfig.args ?? [])],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
command: commandConfig.argv[0]!,
|
||||
args: commandConfig.argv.slice(1),
|
||||
};
|
||||
}
|
||||
|
||||
export function applyProviderEnv(
|
||||
baseEnv: Record<string, string | undefined>,
|
||||
runtimeSettings?: ProviderRuntimeSettings
|
||||
): Record<string, string | undefined> {
|
||||
return {
|
||||
...baseEnv,
|
||||
...(runtimeSettings?.env ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function isCommandAvailable(command: string): boolean {
|
||||
const normalized = command.trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalized.includes("/") || normalized.includes("\\")) {
|
||||
return existsSync(normalized);
|
||||
}
|
||||
|
||||
try {
|
||||
const output = execFileSync("which", [normalized], { encoding: "utf8" }).trim();
|
||||
return output.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isProviderCommandAvailable(
|
||||
commandConfig: ProviderCommand | undefined,
|
||||
resolveDefaultCommand: () => string
|
||||
): boolean {
|
||||
try {
|
||||
const prefix = resolveProviderCommandPrefix(commandConfig, resolveDefaultCommand);
|
||||
return isCommandAvailable(prefix.command);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
AgentProvider,
|
||||
ListModelsOptions,
|
||||
} from "./agent-sdk-types.js";
|
||||
import type { AgentProviderRuntimeSettingsMap } from "./provider-launch-config.js";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
import { ClaudeAgentClient } from "./providers/claude-agent.js";
|
||||
@@ -30,25 +31,45 @@ export interface ProviderDefinition extends AgentProviderDefinition {
|
||||
fetchModels: (options?: ListModelsOptions) => Promise<AgentModelDefinition[]>;
|
||||
}
|
||||
|
||||
export function buildProviderRegistry(logger: Logger): Record<AgentProvider, ProviderDefinition> {
|
||||
const claudeClient = new ClaudeAgentClient({ logger });
|
||||
const codexClient = new CodexAppServerAgentClient(logger);
|
||||
const opencodeClient = new OpenCodeAgentClient(logger);
|
||||
type BuildProviderRegistryOptions = {
|
||||
runtimeSettings?: AgentProviderRuntimeSettingsMap;
|
||||
};
|
||||
|
||||
export function buildProviderRegistry(
|
||||
logger: Logger,
|
||||
options?: BuildProviderRegistryOptions
|
||||
): Record<AgentProvider, ProviderDefinition> {
|
||||
const runtimeSettings = options?.runtimeSettings;
|
||||
const claudeClient = new ClaudeAgentClient({
|
||||
logger,
|
||||
runtimeSettings: runtimeSettings?.claude,
|
||||
});
|
||||
const codexClient = new CodexAppServerAgentClient(
|
||||
logger,
|
||||
runtimeSettings?.codex
|
||||
);
|
||||
const opencodeClient = new OpenCodeAgentClient(
|
||||
logger,
|
||||
runtimeSettings?.opencode
|
||||
);
|
||||
|
||||
return {
|
||||
claude: {
|
||||
...AGENT_PROVIDER_DEFINITIONS.find((d) => d.id === "claude")!,
|
||||
createClient: (logger: Logger) => new ClaudeAgentClient({ logger }),
|
||||
createClient: (logger: Logger) =>
|
||||
new ClaudeAgentClient({ logger, runtimeSettings: runtimeSettings?.claude }),
|
||||
fetchModels: (options) => claudeClient.listModels(options),
|
||||
},
|
||||
codex: {
|
||||
...AGENT_PROVIDER_DEFINITIONS.find((d) => d.id === "codex")!,
|
||||
createClient: (logger: Logger) => new CodexAppServerAgentClient(logger),
|
||||
createClient: (logger: Logger) =>
|
||||
new CodexAppServerAgentClient(logger, runtimeSettings?.codex),
|
||||
fetchModels: (options) => codexClient.listModels(options),
|
||||
},
|
||||
opencode: {
|
||||
...AGENT_PROVIDER_DEFINITIONS.find((d) => d.id === "opencode")!,
|
||||
createClient: (logger: Logger) => new OpenCodeAgentClient(logger),
|
||||
createClient: (logger: Logger) =>
|
||||
new OpenCodeAgentClient(logger, runtimeSettings?.opencode),
|
||||
fetchModels: (options) => opencodeClient.listModels(options),
|
||||
},
|
||||
};
|
||||
@@ -57,8 +78,11 @@ export function buildProviderRegistry(logger: Logger): Record<AgentProvider, Pro
|
||||
// Deprecated: Use buildProviderRegistry instead
|
||||
export const PROVIDER_REGISTRY: Record<AgentProvider, ProviderDefinition> = null as any;
|
||||
|
||||
export function createAllClients(logger: Logger): Record<AgentProvider, AgentClient> {
|
||||
const registry = buildProviderRegistry(logger);
|
||||
export function createAllClients(
|
||||
logger: Logger,
|
||||
options?: BuildProviderRegistryOptions
|
||||
): Record<AgentProvider, AgentClient> {
|
||||
const registry = buildProviderRegistry(logger, options);
|
||||
return {
|
||||
claude: registry.claude.createClient(logger),
|
||||
codex: registry.codex.createClient(logger),
|
||||
@@ -66,6 +90,12 @@ export function createAllClients(logger: Logger): Record<AgentProvider, AgentCli
|
||||
};
|
||||
}
|
||||
|
||||
export async function shutdownProviders(logger: Logger): Promise<void> {
|
||||
await OpenCodeServerManager.getInstance(logger).shutdown();
|
||||
export async function shutdownProviders(
|
||||
logger: Logger,
|
||||
options?: BuildProviderRegistryOptions
|
||||
): Promise<void> {
|
||||
await OpenCodeServerManager.getInstance(
|
||||
logger,
|
||||
options?.runtimeSettings?.opencode
|
||||
).shutdown();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { execSync, spawn } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import { promises } from "node:fs";
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type PermissionResult,
|
||||
type PermissionUpdate,
|
||||
type Query,
|
||||
type SpawnOptions,
|
||||
type SDKMessage,
|
||||
type SDKPartialAssistantMessage,
|
||||
type SDKResultMessage,
|
||||
@@ -56,6 +57,11 @@ import type {
|
||||
McpServerConfig,
|
||||
PersistedAgentDescriptor,
|
||||
} from "../agent-sdk-types.js";
|
||||
import {
|
||||
applyProviderEnv,
|
||||
isProviderCommandAvailable,
|
||||
type ProviderRuntimeSettings,
|
||||
} from "../provider-launch-config.js";
|
||||
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
||||
|
||||
const fsPromises = promises;
|
||||
@@ -131,15 +137,56 @@ type ClaudeOptions = Options;
|
||||
type ClaudeAgentClientOptions = {
|
||||
defaults?: { agents?: Record<string, AgentDefinition> };
|
||||
logger: Logger;
|
||||
runtimeSettings?: ProviderRuntimeSettings;
|
||||
};
|
||||
|
||||
type ClaudeAgentSessionOptions = {
|
||||
defaults?: { agents?: Record<string, AgentDefinition> };
|
||||
claudePath: string | null;
|
||||
runtimeSettings?: ProviderRuntimeSettings;
|
||||
handle?: AgentPersistenceHandle;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
function resolveClaudeBinary(): string {
|
||||
try {
|
||||
const claudePath = execSync("which claude", { encoding: "utf8" }).trim();
|
||||
if (claudePath) {
|
||||
return claudePath;
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
throw new Error(
|
||||
"Claude CLI not found. Install claude or configure agents.providers.claude.command.mode='replace'."
|
||||
);
|
||||
}
|
||||
|
||||
function resolveClaudeSpawnCommand(
|
||||
spawnOptions: SpawnOptions,
|
||||
runtimeSettings?: ProviderRuntimeSettings
|
||||
): { command: string; args: string[] } {
|
||||
const commandConfig = runtimeSettings?.command;
|
||||
if (!commandConfig || commandConfig.mode === "default") {
|
||||
return {
|
||||
command: spawnOptions.command,
|
||||
args: [...spawnOptions.args],
|
||||
};
|
||||
}
|
||||
|
||||
if (commandConfig.mode === "append") {
|
||||
return {
|
||||
command: spawnOptions.command,
|
||||
args: [...(commandConfig.args ?? []), ...spawnOptions.args],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
command: commandConfig.argv[0]!,
|
||||
args: [...commandConfig.argv.slice(1), ...spawnOptions.args],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function extractUserMessageText(content: unknown): string | null {
|
||||
if (typeof content === "string") {
|
||||
@@ -351,10 +398,12 @@ 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;
|
||||
try {
|
||||
this.claudePath = execSync("which claude", { encoding: "utf8" }).trim() || null;
|
||||
} catch {
|
||||
@@ -362,11 +411,39 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
}
|
||||
}
|
||||
|
||||
private applyRuntimeSettings(options: ClaudeOptions): ClaudeOptions {
|
||||
const hasEnvOverrides = Object.keys(this.runtimeSettings?.env ?? {}).length > 0;
|
||||
const commandMode = this.runtimeSettings?.command?.mode;
|
||||
const needsCustomSpawn =
|
||||
hasEnvOverrides || commandMode === "append" || commandMode === "replace";
|
||||
|
||||
if (!needsCustomSpawn) {
|
||||
return options;
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
spawnClaudeCodeProcess: (spawnOptions) => {
|
||||
const resolved = resolveClaudeSpawnCommand(
|
||||
spawnOptions,
|
||||
this.runtimeSettings
|
||||
);
|
||||
return spawn(resolved.command, resolved.args, {
|
||||
cwd: spawnOptions.cwd,
|
||||
env: applyProviderEnv(spawnOptions.env, this.runtimeSettings),
|
||||
signal: spawnOptions.signal,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
@@ -385,6 +462,7 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
return new ClaudeAgentSession(claudeConfig, {
|
||||
defaults: this.defaults,
|
||||
claudePath: this.claudePath,
|
||||
runtimeSettings: this.runtimeSettings,
|
||||
handle,
|
||||
logger: this.logger,
|
||||
});
|
||||
@@ -399,7 +477,10 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
...(this.claudePath ? { pathToClaudeCodeExecutable: this.claudePath } : {}),
|
||||
};
|
||||
|
||||
const claudeQuery = query({ prompt, options: claudeOptions });
|
||||
const claudeQuery = query({
|
||||
prompt,
|
||||
options: this.applyRuntimeSettings(claudeOptions),
|
||||
});
|
||||
try {
|
||||
const models: ModelInfo[] = await claudeQuery.supportedModels();
|
||||
return models.map((model) => ({
|
||||
@@ -451,6 +532,10 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
const commandConfig = this.runtimeSettings?.command;
|
||||
if (commandConfig?.mode === "replace") {
|
||||
return isProviderCommandAvailable(commandConfig, resolveClaudeBinary);
|
||||
}
|
||||
return this.claudePath !== null;
|
||||
}
|
||||
|
||||
@@ -469,6 +554,7 @@ 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;
|
||||
private input: Pushable<SDKUserMessage> | null = null;
|
||||
@@ -508,6 +594,7 @@ 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;
|
||||
|
||||
@@ -971,7 +1058,34 @@ class ClaudeAgentSession implements AgentSession {
|
||||
if (this.claudeSessionId) {
|
||||
base.resume = this.claudeSessionId;
|
||||
}
|
||||
return base;
|
||||
return this.applyRuntimeSettings(base);
|
||||
}
|
||||
|
||||
private applyRuntimeSettings(options: ClaudeOptions): ClaudeOptions {
|
||||
const hasEnvOverrides = Object.keys(this.runtimeSettings?.env ?? {}).length > 0;
|
||||
const commandMode = this.runtimeSettings?.command?.mode;
|
||||
const needsCustomSpawn =
|
||||
hasEnvOverrides || commandMode === "append" || commandMode === "replace";
|
||||
|
||||
if (!needsCustomSpawn) {
|
||||
return options;
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
spawnClaudeCodeProcess: (spawnOptions) => {
|
||||
const resolved = resolveClaudeSpawnCommand(
|
||||
spawnOptions,
|
||||
this.runtimeSettings
|
||||
);
|
||||
return spawn(resolved.command, resolved.args, {
|
||||
cwd: spawnOptions.cwd,
|
||||
env: applyProviderEnv(spawnOptions.env, this.runtimeSettings),
|
||||
signal: spawnOptions.signal,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeMcpServers(
|
||||
|
||||
@@ -39,6 +39,12 @@ import {
|
||||
mapCodexRolloutToolCall,
|
||||
mapCodexToolCallFromThreadItem,
|
||||
} from "./codex/tool-call-mapper.js";
|
||||
import {
|
||||
applyProviderEnv,
|
||||
isProviderCommandAvailable,
|
||||
resolveProviderCommandPrefix,
|
||||
type ProviderRuntimeSettings,
|
||||
} from "../provider-launch-config.js";
|
||||
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000;
|
||||
@@ -158,6 +164,15 @@ function resolveCodexBinary(): string {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveCodexLaunchPrefix(
|
||||
runtimeSettings?: ProviderRuntimeSettings
|
||||
): { command: string; args: string[] } {
|
||||
return resolveProviderCommandPrefix(
|
||||
runtimeSettings?.command,
|
||||
resolveCodexBinary
|
||||
);
|
||||
}
|
||||
|
||||
function resolveCodexHomeDir(): string {
|
||||
return process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
||||
}
|
||||
@@ -1750,7 +1765,8 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
constructor(
|
||||
config: AgentSessionConfig,
|
||||
private readonly resumeHandle: { sessionId: string; metadata?: Record<string, unknown> } | null,
|
||||
logger: Logger
|
||||
logger: Logger,
|
||||
private readonly spawnAppServer: () => ChildProcessWithoutNullStreams
|
||||
) {
|
||||
this.logger = logger.child({ module: "agent", provider: CODEX_PROVIDER });
|
||||
if (config.modeId === undefined) {
|
||||
@@ -1773,11 +1789,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
|
||||
async connect(): Promise<void> {
|
||||
if (this.connected) return;
|
||||
const binaryPath = resolveCodexBinary();
|
||||
const child = spawn(binaryPath, ["app-server"], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: process.env,
|
||||
});
|
||||
const child = this.spawnAppServer();
|
||||
this.client = new CodexAppServerClient(child, this.logger);
|
||||
this.client.setNotificationHandler((method, params) => this.handleNotification(method, params));
|
||||
this.registerRequestHandlers();
|
||||
@@ -2854,11 +2866,27 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
readonly provider = CODEX_PROVIDER;
|
||||
readonly capabilities = CODEX_APP_SERVER_CAPABILITIES;
|
||||
|
||||
constructor(private readonly logger: Logger) {}
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly runtimeSettings?: ProviderRuntimeSettings
|
||||
) {}
|
||||
|
||||
private spawnAppServer(): ChildProcessWithoutNullStreams {
|
||||
const launchPrefix = resolveCodexLaunchPrefix(this.runtimeSettings);
|
||||
return spawn(launchPrefix.command, [...launchPrefix.args, "app-server"], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: applyProviderEnv(process.env, this.runtimeSettings),
|
||||
});
|
||||
}
|
||||
|
||||
async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
const sessionConfig: AgentSessionConfig = { ...config, provider: CODEX_PROVIDER };
|
||||
const session = new CodexAppServerAgentSession(sessionConfig, null, this.logger);
|
||||
const session = new CodexAppServerAgentSession(
|
||||
sessionConfig,
|
||||
null,
|
||||
this.logger,
|
||||
() => this.spawnAppServer()
|
||||
);
|
||||
await session.connect();
|
||||
return session;
|
||||
}
|
||||
@@ -2871,7 +2899,12 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
provider: CODEX_PROVIDER,
|
||||
cwd: overrides?.cwd ?? storedConfig.cwd ?? process.cwd(),
|
||||
};
|
||||
const session = new CodexAppServerAgentSession(merged, handle, this.logger);
|
||||
const session = new CodexAppServerAgentSession(
|
||||
merged,
|
||||
handle,
|
||||
this.logger,
|
||||
() => this.spawnAppServer()
|
||||
);
|
||||
await session.connect();
|
||||
return session;
|
||||
}
|
||||
@@ -2879,10 +2912,7 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
async listPersistedAgents(
|
||||
options?: ListPersistedAgentsOptions
|
||||
): Promise<PersistedAgentDescriptor[]> {
|
||||
const binaryPath = resolveCodexBinary();
|
||||
const child = spawn(binaryPath, ["app-server"], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
const child = this.spawnAppServer();
|
||||
const client = new CodexAppServerClient(child, this.logger);
|
||||
|
||||
try {
|
||||
@@ -2957,10 +2987,7 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
}
|
||||
|
||||
async listModels(_options?: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
||||
const binaryPath = resolveCodexBinary();
|
||||
const child = spawn(binaryPath, ["app-server"], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
const child = this.spawnAppServer();
|
||||
const client = new CodexAppServerClient(child, this.logger);
|
||||
|
||||
try {
|
||||
@@ -3050,11 +3077,6 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
try {
|
||||
const codexPath = execSync("which codex", { encoding: "utf8" }).trim();
|
||||
return Boolean(codexPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return isProviderCommandAvailable(this.runtimeSettings?.command, resolveCodexBinary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ import type {
|
||||
McpServerConfig,
|
||||
PersistedAgentDescriptor,
|
||||
} from "../agent-sdk-types.js";
|
||||
import {
|
||||
applyProviderEnv,
|
||||
isProviderCommandAvailable,
|
||||
resolveProviderCommandPrefix,
|
||||
type ProviderRuntimeSettings,
|
||||
} from "../provider-launch-config.js";
|
||||
import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
|
||||
|
||||
const OPENCODE_CAPABILITIES: AgentCapabilityFlags = {
|
||||
@@ -63,6 +69,20 @@ type OpenCodeMcpConfig =
|
||||
|
||||
const MCP_ALREADY_PRESENT_ERROR_TOKENS = ["already", "exists", "connected"] as const;
|
||||
|
||||
function resolveOpenCodeBinary(): string {
|
||||
try {
|
||||
const opencodePath = execSync("which opencode", { encoding: "utf8" }).trim();
|
||||
if (opencodePath) {
|
||||
return opencodePath;
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
throw new Error(
|
||||
"OpenCode CLI not found. Please install opencode globally so Paseo can launch the provider."
|
||||
);
|
||||
}
|
||||
|
||||
function toOpenCodeMcpConfig(config: McpServerConfig): OpenCodeMcpConfig {
|
||||
if (config.type === "stdio") {
|
||||
return {
|
||||
@@ -122,15 +142,31 @@ export class OpenCodeServerManager {
|
||||
private port: number | null = null;
|
||||
private startPromise: Promise<{ port: number; url: string }> | null = null;
|
||||
private readonly logger: Logger;
|
||||
private readonly runtimeSettings?: ProviderRuntimeSettings;
|
||||
private readonly runtimeSettingsKey: string;
|
||||
|
||||
private constructor(logger: Logger) {
|
||||
private constructor(logger: Logger, runtimeSettings?: ProviderRuntimeSettings) {
|
||||
this.logger = logger;
|
||||
this.runtimeSettings = runtimeSettings;
|
||||
this.runtimeSettingsKey = JSON.stringify(runtimeSettings ?? {});
|
||||
}
|
||||
|
||||
static getInstance(logger: Logger): OpenCodeServerManager {
|
||||
static getInstance(
|
||||
logger: Logger,
|
||||
runtimeSettings?: ProviderRuntimeSettings
|
||||
): OpenCodeServerManager {
|
||||
const nextSettingsKey = JSON.stringify(runtimeSettings ?? {});
|
||||
if (!OpenCodeServerManager.instance) {
|
||||
OpenCodeServerManager.instance = new OpenCodeServerManager(logger);
|
||||
OpenCodeServerManager.instance = new OpenCodeServerManager(logger, runtimeSettings);
|
||||
OpenCodeServerManager.registerExitHandler();
|
||||
} else if (OpenCodeServerManager.instance.runtimeSettingsKey !== nextSettingsKey) {
|
||||
logger.warn(
|
||||
{
|
||||
existingRuntimeSettings: OpenCodeServerManager.instance.runtimeSettingsKey,
|
||||
requestedRuntimeSettings: nextSettingsKey,
|
||||
},
|
||||
"OpenCode server manager already initialized with different runtime settings"
|
||||
);
|
||||
}
|
||||
return OpenCodeServerManager.instance;
|
||||
}
|
||||
@@ -174,12 +210,20 @@ export class OpenCodeServerManager {
|
||||
private async startServer(): Promise<{ port: number; url: string }> {
|
||||
this.port = await findAvailablePort();
|
||||
const url = `http://127.0.0.1:${this.port}`;
|
||||
const launchPrefix = resolveProviderCommandPrefix(
|
||||
this.runtimeSettings?.command,
|
||||
resolveOpenCodeBinary
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = spawn("opencode", ["serve", "--port", String(this.port)], {
|
||||
this.server = spawn(
|
||||
launchPrefix.command,
|
||||
[...launchPrefix.args, "serve", "--port", String(this.port)],
|
||||
{
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: { ...process.env },
|
||||
});
|
||||
env: applyProviderEnv(process.env, this.runtimeSettings),
|
||||
}
|
||||
);
|
||||
|
||||
let started = false;
|
||||
const timeout = setTimeout(() => {
|
||||
@@ -242,10 +286,15 @@ export class OpenCodeAgentClient implements AgentClient {
|
||||
|
||||
private readonly serverManager: OpenCodeServerManager;
|
||||
private readonly logger: Logger;
|
||||
private readonly runtimeSettings?: ProviderRuntimeSettings;
|
||||
|
||||
constructor(logger: Logger) {
|
||||
constructor(logger: Logger, runtimeSettings?: ProviderRuntimeSettings) {
|
||||
this.logger = logger.child({ module: "agent", provider: "opencode" });
|
||||
this.serverManager = OpenCodeServerManager.getInstance(this.logger);
|
||||
this.runtimeSettings = runtimeSettings;
|
||||
this.serverManager = OpenCodeServerManager.getInstance(
|
||||
this.logger,
|
||||
runtimeSettings
|
||||
);
|
||||
}
|
||||
|
||||
async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
@@ -381,12 +430,10 @@ export class OpenCodeAgentClient implements AgentClient {
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
try {
|
||||
const opencodePath = execSync("which opencode", { encoding: "utf8" }).trim();
|
||||
return Boolean(opencodePath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return isProviderCommandAvailable(
|
||||
this.runtimeSettings?.command,
|
||||
resolveOpenCodeBinary
|
||||
);
|
||||
}
|
||||
|
||||
private assertConfig(config: AgentSessionConfig): OpenCodeAgentConfig {
|
||||
|
||||
@@ -64,6 +64,7 @@ import type {
|
||||
AgentClient,
|
||||
AgentProvider,
|
||||
} from "./agent/agent-sdk-types.js";
|
||||
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
||||
import { acquirePidLock, releasePidLock, isLocked, PidLockError } from "./pid-lock.js";
|
||||
import { isHostAllowed, type AllowedHostsConfig } from "./allowed-hosts.js";
|
||||
import {
|
||||
@@ -120,6 +121,7 @@ export type PaseoDaemonConfig = {
|
||||
voiceLlmModel?: string | null;
|
||||
dictationFinalTimeoutMs?: number;
|
||||
downloadTokenTtlMs?: number;
|
||||
agentProviderSettings?: AgentProviderRuntimeSettingsMap;
|
||||
};
|
||||
|
||||
export interface PaseoDaemon {
|
||||
@@ -274,7 +276,9 @@ export async function createPaseoDaemon(
|
||||
const agentStorage = new AgentStorage(config.agentStoragePath, logger);
|
||||
const agentManager = new AgentManager({
|
||||
clients: {
|
||||
...createAllClients(logger),
|
||||
...createAllClients(logger, {
|
||||
runtimeSettings: config.agentProviderSettings,
|
||||
}),
|
||||
...config.agentClients,
|
||||
},
|
||||
registry: agentStorage,
|
||||
@@ -496,7 +500,8 @@ export async function createPaseoDaemon(
|
||||
finalTimeoutMs: config.dictationFinalTimeoutMs,
|
||||
stt: dictationSttService,
|
||||
localModels: localModelConfig ?? undefined,
|
||||
}
|
||||
},
|
||||
config.agentProviderSettings
|
||||
);
|
||||
|
||||
const start = async () => {
|
||||
@@ -590,7 +595,9 @@ export async function createPaseoDaemon(
|
||||
await agentManager.flush().catch(() => undefined);
|
||||
detachAgentStoragePersistence();
|
||||
await agentStorage.flush().catch(() => undefined);
|
||||
await shutdownProviders(logger);
|
||||
await shutdownProviders(logger, {
|
||||
runtimeSettings: config.agentProviderSettings,
|
||||
});
|
||||
terminalManager.killAll();
|
||||
cleanupSpeechRuntime();
|
||||
await relayTransport?.stop().catch(() => undefined);
|
||||
|
||||
@@ -125,5 +125,6 @@ export function loadConfig(
|
||||
voiceLlmProvider,
|
||||
voiceLlmProviderExplicit,
|
||||
voiceLlmModel,
|
||||
agentProviderSettings: persisted.agents?.providers,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { PersistedConfigSchema } from "./persisted-config.js";
|
||||
|
||||
describe("PersistedConfigSchema agent provider runtime settings", () => {
|
||||
test("accepts provider command append args and env", () => {
|
||||
const parsed = PersistedConfigSchema.parse({
|
||||
agents: {
|
||||
providers: {
|
||||
claude: {
|
||||
command: {
|
||||
mode: "append",
|
||||
args: ["--chrome"],
|
||||
},
|
||||
env: {
|
||||
FOO: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.agents?.providers?.claude?.command?.mode).toBe("append");
|
||||
expect(parsed.agents?.providers?.claude?.env?.FOO).toBe("bar");
|
||||
});
|
||||
|
||||
test("accepts provider command replace argv", () => {
|
||||
const parsed = PersistedConfigSchema.parse({
|
||||
agents: {
|
||||
providers: {
|
||||
codex: {
|
||||
command: {
|
||||
mode: "replace",
|
||||
argv: ["docker", "run", "--rm", "my-codex-wrapper"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.agents?.providers?.codex?.command?.mode).toBe("replace");
|
||||
});
|
||||
|
||||
test("rejects replace command without argv", () => {
|
||||
const result = PersistedConfigSchema.safeParse({
|
||||
agents: {
|
||||
providers: {
|
||||
opencode: {
|
||||
command: {
|
||||
mode: "replace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
|
||||
import { AgentProviderRuntimeSettingsMapSchema } from "./agent/provider-launch-config.js";
|
||||
|
||||
const LogConfigSchema = z
|
||||
.object({
|
||||
@@ -122,6 +123,12 @@ export const PersistedConfigSchema = z
|
||||
.optional(),
|
||||
|
||||
providers: ProvidersSchema.optional(),
|
||||
agents: z
|
||||
.object({
|
||||
providers: AgentProviderRuntimeSettingsMapSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
features: z
|
||||
.object({
|
||||
dictation: FeatureDictationSchema.optional(),
|
||||
|
||||
@@ -48,6 +48,7 @@ import type {
|
||||
|
||||
export type AgentMcpTransportFactory = () => Promise<Transport>;
|
||||
import { buildProviderRegistry } from "./agent/provider-registry.js";
|
||||
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
||||
import { AgentManager } from "./agent/agent-manager.js";
|
||||
import type { ManagedAgent } from "./agent/agent-manager.js";
|
||||
import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
|
||||
@@ -297,6 +298,7 @@ export type SessionOptions = {
|
||||
defaultModelIds: LocalSpeechModelId[];
|
||||
};
|
||||
};
|
||||
agentProviderRuntimeSettings?: AgentProviderRuntimeSettingsMap;
|
||||
};
|
||||
|
||||
function convertPCMToWavBuffer(
|
||||
@@ -468,6 +470,7 @@ export class Session {
|
||||
private readonly unregisterVoiceCallerContext?: (agentId: string) => void;
|
||||
private readonly ensureVoiceMcpSocketForAgent?: (agentId: string) => Promise<string>;
|
||||
private readonly removeVoiceMcpSocketForAgent?: (agentId: string) => Promise<void>;
|
||||
private readonly agentProviderRuntimeSettings: AgentProviderRuntimeSettingsMap | undefined;
|
||||
private voiceModeAgentId: string | null = null;
|
||||
private voiceModeBaseConfig: VoiceModeBaseConfig | null = null;
|
||||
|
||||
@@ -488,6 +491,7 @@ export class Session {
|
||||
voice,
|
||||
voiceBridge,
|
||||
dictation,
|
||||
agentProviderRuntimeSettings,
|
||||
} = options;
|
||||
this.clientId = clientId;
|
||||
this.sessionId = uuidv4();
|
||||
@@ -515,13 +519,16 @@ export class Session {
|
||||
this.unregisterVoiceCallerContext = voiceBridge?.unregisterVoiceCallerContext;
|
||||
this.ensureVoiceMcpSocketForAgent = voiceBridge?.ensureVoiceMcpSocketForAgent;
|
||||
this.removeVoiceMcpSocketForAgent = voiceBridge?.removeVoiceMcpSocketForAgent;
|
||||
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
|
||||
this.abortController = new AbortController();
|
||||
this.sessionLogger = logger.child({
|
||||
module: "session",
|
||||
clientId: this.clientId,
|
||||
sessionId: this.sessionId,
|
||||
});
|
||||
this.providerRegistry = buildProviderRegistry(this.sessionLogger);
|
||||
this.providerRegistry = buildProviderRegistry(this.sessionLogger, {
|
||||
runtimeSettings: this.agentProviderRuntimeSettings,
|
||||
});
|
||||
|
||||
// Initialize per-session managers
|
||||
this.ttsManager = new TTSManager(this.sessionId, this.sessionLogger, tts);
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { AllowedHostsConfig } from "./allowed-hosts.js";
|
||||
import { isHostAllowed } from "./allowed-hosts.js";
|
||||
import { Session } from "./session.js";
|
||||
import type { AgentProvider } from "./agent/agent-sdk-types.js";
|
||||
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
||||
import { PushTokenStore } from "./push/token-store.js";
|
||||
import { PushService } from "./push/push-service.js";
|
||||
import type { SpeechToTextProvider, TextToSpeechProvider } from "./speech/speech-provider.js";
|
||||
@@ -92,6 +93,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
VoiceSpeakHandler
|
||||
>();
|
||||
private readonly voiceCallerContexts = new Map<string, VoiceCallerContext>();
|
||||
private readonly agentProviderRuntimeSettings: AgentProviderRuntimeSettingsMap | undefined;
|
||||
|
||||
constructor(
|
||||
server: HTTPServer,
|
||||
@@ -117,7 +119,8 @@ export class VoiceAssistantWebSocketServer {
|
||||
modelsDir: string;
|
||||
defaultModelIds: LocalSpeechModelId[];
|
||||
};
|
||||
}
|
||||
},
|
||||
agentProviderRuntimeSettings?: AgentProviderRuntimeSettingsMap
|
||||
) {
|
||||
this.logger = logger.child({ module: "websocket-server" });
|
||||
this.serverId = serverId;
|
||||
@@ -131,6 +134,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
this.terminalManager = terminalManager ?? null;
|
||||
this.voice = voice ?? null;
|
||||
this.dictation = dictation ?? null;
|
||||
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
|
||||
|
||||
const pushLogger = this.logger.child({ module: "push" });
|
||||
this.pushTokenStore = new PushTokenStore(
|
||||
@@ -257,6 +261,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
removeVoiceMcpSocketForAgent: this.voice?.removeVoiceMcpSocketForAgent,
|
||||
},
|
||||
dictation: this.dictation ?? undefined,
|
||||
agentProviderRuntimeSettings: this.agentProviderRuntimeSettings,
|
||||
});
|
||||
|
||||
this.sessions.set(ws, session);
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
},
|
||||
"endpoint": {
|
||||
"type": "string"
|
||||
},
|
||||
"publicEndpoint": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -102,6 +105,92 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"agents": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"providers": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"const": "default"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"mode"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"const": "append"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"mode"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"const": "replace"
|
||||
},
|
||||
"argv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"minItems": 1
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"mode",
|
||||
"argv"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"propertyNames": {
|
||||
"enum": [
|
||||
"claude",
|
||||
"codex",
|
||||
"opencode"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"features": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -112,10 +201,17 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"openai",
|
||||
"local"
|
||||
"allOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"openai",
|
||||
"local"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
@@ -156,11 +252,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"openai",
|
||||
"local"
|
||||
]
|
||||
"$ref": "#/definitions/PaseoConfigV1/properties/features/properties/dictation/properties/stt/properties/provider"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
@@ -173,11 +265,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"openai",
|
||||
"local"
|
||||
]
|
||||
"$ref": "#/definitions/PaseoConfigV1/properties/features/properties/dictation/properties/stt/properties/provider"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
@@ -193,6 +281,12 @@
|
||||
"nova",
|
||||
"shimmer"
|
||||
]
|
||||
},
|
||||
"speakerId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"speed": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -78,6 +78,83 @@ function Configuration() {
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Agent provider runtime settings</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Use <code className="font-mono">agents.providers</code> to customize how Paseo launches agent provider CLIs.
|
||||
This works for <code className="font-mono">claude</code>, <code className="font-mono">codex</code>, and
|
||||
<code className="font-mono"> opencode</code>.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
<code className="font-mono">command.mode</code> can be <code className="font-mono">default</code>,{' '}
|
||||
<code className="font-mono">append</code>, or <code className="font-mono">replace</code>. Use{' '}
|
||||
<code className="font-mono">env</code> to inject provider-specific environment variables.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-medium">Enable Claude Code Chrome MCP</h3>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"claude": {
|
||||
"command": {
|
||||
"mode": "append",
|
||||
"args": ["--chrome"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
|
||||
<h3 className="text-lg font-medium">Point Claude to Anthropic-compatible endpoints (z.ai example)</h3>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"claude": {
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
|
||||
"ANTHROPIC_AUTH_TOKEN": "auth token",
|
||||
"ANTHROPIC_API_KEY": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
|
||||
<h3 className="text-lg font-medium">Run Claude through Docker</h3>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Create a wrapper script that runs Claude in Docker, then tell Paseo to replace the Claude launch command
|
||||
with that script.
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"claude": {
|
||||
"command": {
|
||||
"mode": "replace",
|
||||
"argv": ["/Users/you/bin/claude-docker"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
docker run --rm -i \\
|
||||
-v "$PWD":"$PWD" \\
|
||||
-w "$PWD" \\
|
||||
-v "$HOME/.claude":"$HOME/.claude" \\
|
||||
ghcr.io/anthropics/claude-code:latest \\
|
||||
claude "$@"`}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Voice</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
|
||||
Reference in New Issue
Block a user