mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Fix provider binary diagnostics for command overrides (#1191)
* fix: align provider binary diagnostics * Fix provider launch tests on Windows * Separate provider launch availability checks
This commit is contained in:
@@ -1,25 +1,71 @@
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, describe, expect, test } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
checkProviderLaunchAvailable,
|
||||||
createProviderEnv,
|
createProviderEnv,
|
||||||
migrateProviderSettings,
|
migrateProviderSettings,
|
||||||
ProviderOverrideSchema,
|
ProviderOverrideSchema,
|
||||||
|
resolveProviderLaunch,
|
||||||
resolveProviderCommandPrefix,
|
resolveProviderCommandPrefix,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
} from "./provider-launch-config.js";
|
} from "./provider-launch-config.js";
|
||||||
|
|
||||||
|
const originalPath = process.env.PATH;
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env.PATH = originalPath;
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeTempDir(): string {
|
||||||
|
const dir = mkdtempSync(path.join(tmpdir(), "paseo-provider-launch-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestExecutable {
|
||||||
|
command: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createExecutable(dir: string, name: string, body = "echo test-version\n"): TestExecutable {
|
||||||
|
const command = process.platform === "win32" ? `${name}.cmd` : name;
|
||||||
|
const file = path.join(dir, command);
|
||||||
|
const contents = process.platform === "win32" ? "@echo test-version\r\n" : `#!/bin/sh\n${body}`;
|
||||||
|
writeFileSync(file, contents, "utf8");
|
||||||
|
chmodSync(file, 0o755);
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
path: file,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("resolveProviderCommandPrefix", () => {
|
describe("resolveProviderCommandPrefix", () => {
|
||||||
test("uses resolved default command in default mode", async () => {
|
test("uses resolved default command in default mode", async () => {
|
||||||
const resolveDefault = vi.fn(() => "/usr/local/bin/claude");
|
let calls = 0;
|
||||||
|
const resolveDefault = () => {
|
||||||
|
calls += 1;
|
||||||
|
return "/usr/local/bin/claude";
|
||||||
|
};
|
||||||
|
|
||||||
const resolved = await resolveProviderCommandPrefix(undefined, resolveDefault);
|
const resolved = await resolveProviderCommandPrefix(undefined, resolveDefault);
|
||||||
|
|
||||||
expect(resolveDefault).toHaveBeenCalledTimes(1);
|
expect(calls).toBe(1);
|
||||||
expect(resolved).toEqual({ command: "/usr/local/bin/claude", args: [] });
|
expect(resolved).toEqual({ command: "/usr/local/bin/claude", args: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("appends args in append mode", async () => {
|
test("appends args in append mode", async () => {
|
||||||
const resolveDefault = vi.fn(() => "/usr/local/bin/claude");
|
let calls = 0;
|
||||||
|
const resolveDefault = () => {
|
||||||
|
calls += 1;
|
||||||
|
return "/usr/local/bin/claude";
|
||||||
|
};
|
||||||
|
|
||||||
const resolved = await resolveProviderCommandPrefix(
|
const resolved = await resolveProviderCommandPrefix(
|
||||||
{
|
{
|
||||||
@@ -29,7 +75,7 @@ describe("resolveProviderCommandPrefix", () => {
|
|||||||
resolveDefault,
|
resolveDefault,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(resolveDefault).toHaveBeenCalledTimes(1);
|
expect(calls).toBe(1);
|
||||||
expect(resolved).toEqual({
|
expect(resolved).toEqual({
|
||||||
command: "/usr/local/bin/claude",
|
command: "/usr/local/bin/claude",
|
||||||
args: ["--chrome"],
|
args: ["--chrome"],
|
||||||
@@ -37,7 +83,11 @@ describe("resolveProviderCommandPrefix", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("replaces command in replace mode without resolving default", async () => {
|
test("replaces command in replace mode without resolving default", async () => {
|
||||||
const resolveDefault = vi.fn(() => "/usr/local/bin/claude");
|
let calls = 0;
|
||||||
|
const resolveDefault = () => {
|
||||||
|
calls += 1;
|
||||||
|
return "/usr/local/bin/claude";
|
||||||
|
};
|
||||||
|
|
||||||
const resolved = await resolveProviderCommandPrefix(
|
const resolved = await resolveProviderCommandPrefix(
|
||||||
{
|
{
|
||||||
@@ -47,7 +97,7 @@ describe("resolveProviderCommandPrefix", () => {
|
|||||||
resolveDefault,
|
resolveDefault,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(resolveDefault).not.toHaveBeenCalled();
|
expect(calls).toBe(0);
|
||||||
expect(resolved).toEqual({
|
expect(resolved).toEqual({
|
||||||
command: "docker",
|
command: "docker",
|
||||||
args: ["run", "--rm", "my-wrapper"],
|
args: ["run", "--rm", "my-wrapper"],
|
||||||
@@ -55,6 +105,117 @@ describe("resolveProviderCommandPrefix", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveProviderLaunch", () => {
|
||||||
|
test("uses replace override as the spawned command", async () => {
|
||||||
|
const binDir = makeTempDir();
|
||||||
|
const shim = createExecutable(binDir, "custom-provider");
|
||||||
|
|
||||||
|
const launch = await resolveProviderLaunch({
|
||||||
|
commandConfig: { mode: "replace", argv: [shim.command, "--wrapped"] },
|
||||||
|
defaultBinary: "provider",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(launch).toEqual({
|
||||||
|
command: shim.command,
|
||||||
|
args: ["--wrapped"],
|
||||||
|
source: "override",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps an absolute replace override when the path exists", async () => {
|
||||||
|
const binDir = makeTempDir();
|
||||||
|
const shim = createExecutable(binDir, "custom-provider", "exit 42\n");
|
||||||
|
|
||||||
|
const launch = await resolveProviderLaunch({
|
||||||
|
commandConfig: { mode: "replace", argv: [shim.path, "--wrapped"] },
|
||||||
|
defaultBinary: "provider",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(launch).toEqual({
|
||||||
|
command: shim.path,
|
||||||
|
args: ["--wrapped"],
|
||||||
|
source: "override",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolves append mode through the default binary", async () => {
|
||||||
|
const binDir = makeTempDir();
|
||||||
|
const binary = createExecutable(binDir, "default-provider");
|
||||||
|
process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ""}`;
|
||||||
|
|
||||||
|
const launch = await resolveProviderLaunch({
|
||||||
|
commandConfig: { mode: "append", args: ["--profile", "work"] },
|
||||||
|
defaultBinary: binary.command,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(launch).toEqual({
|
||||||
|
command: binary.command,
|
||||||
|
args: ["--profile", "work"],
|
||||||
|
source: "append",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolves the default binary when no override is configured", async () => {
|
||||||
|
const binDir = makeTempDir();
|
||||||
|
const binary = createExecutable(binDir, "default-provider");
|
||||||
|
process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ""}`;
|
||||||
|
|
||||||
|
const launch = await resolveProviderLaunch({
|
||||||
|
defaultBinary: binary.command,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(launch).toEqual({
|
||||||
|
command: binary.command,
|
||||||
|
args: [],
|
||||||
|
source: "default",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps the default command when the default binary is missing", async () => {
|
||||||
|
process.env.PATH = makeTempDir();
|
||||||
|
|
||||||
|
const launch = await resolveProviderLaunch({
|
||||||
|
defaultBinary: "paseo-provider-missing",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(launch).toEqual({
|
||||||
|
command: "paseo-provider-missing",
|
||||||
|
args: [],
|
||||||
|
source: "default",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkProviderLaunchAvailable", () => {
|
||||||
|
test("reports available with a resolved path", async () => {
|
||||||
|
const binDir = makeTempDir();
|
||||||
|
const binary = createExecutable(binDir, "default-provider");
|
||||||
|
process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ""}`;
|
||||||
|
|
||||||
|
const launch = await resolveProviderLaunch({
|
||||||
|
defaultBinary: binary.command,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(checkProviderLaunchAvailable(launch)).resolves.toEqual({
|
||||||
|
available: true,
|
||||||
|
resolvedPath: binary.path,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reports missing override commands as unavailable", async () => {
|
||||||
|
process.env.PATH = makeTempDir();
|
||||||
|
const launch = await resolveProviderLaunch({
|
||||||
|
commandConfig: { mode: "replace", argv: ["paseo-provider-missing"] },
|
||||||
|
defaultBinary: "provider",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(checkProviderLaunchAvailable(launch)).resolves.toEqual({
|
||||||
|
available: false,
|
||||||
|
resolvedPath: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("createProviderEnv", () => {
|
describe("createProviderEnv", () => {
|
||||||
test("merges provider env overrides", () => {
|
test("merges provider env overrides", () => {
|
||||||
const base = {
|
const base = {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { execFileSync } from "node:child_process";
|
import { isAbsolute } from "node:path";
|
||||||
import path from "node:path";
|
import { executableExists, findExecutable } from "../../utils/executable.js";
|
||||||
import { isCommandAvailable } from "../../utils/executable.js";
|
|
||||||
import { createExternalProcessEnv, type ProcessEnvRecord } from "../paseo-env.js";
|
import { createExternalProcessEnv, type ProcessEnvRecord } from "../paseo-env.js";
|
||||||
import type { AgentProvider } from "./agent-sdk-types.js";
|
import type { AgentProvider } from "./agent-sdk-types.js";
|
||||||
import { AgentProviderSchema } from "./provider-manifest.js";
|
import { AgentProviderSchema } from "./provider-manifest.js";
|
||||||
@@ -93,27 +92,121 @@ export interface ProviderCommandPrefix {
|
|||||||
args: string[];
|
args: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProviderLaunchSource = "default" | "append" | "override";
|
||||||
|
|
||||||
|
export interface ResolvedProviderLaunch {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
source: ProviderLaunchSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderLaunchAvailability {
|
||||||
|
available: boolean;
|
||||||
|
resolvedPath: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderLaunchDefault {
|
||||||
|
command: string;
|
||||||
|
resolvePath?: () => Promise<string | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLaunchDefault(
|
||||||
|
defaultBinary: string | ProviderLaunchDefault,
|
||||||
|
): ProviderLaunchDefault {
|
||||||
|
if (typeof defaultBinary === "string") {
|
||||||
|
return { command: defaultBinary };
|
||||||
|
}
|
||||||
|
return defaultBinary;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveLaunchPath(command: string): Promise<string | null> {
|
||||||
|
const found = await findExecutable(command);
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
if (isAbsolute(command)) {
|
||||||
|
return executableExists(command);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveDefaultLaunchPath(
|
||||||
|
defaultBinary: ProviderLaunchDefault,
|
||||||
|
): Promise<string | null> {
|
||||||
|
return defaultBinary.resolvePath
|
||||||
|
? await defaultBinary.resolvePath()
|
||||||
|
: await resolveLaunchPath(defaultBinary.command);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolveProviderLaunchOptions {
|
||||||
|
commandConfig?: ProviderCommand;
|
||||||
|
defaultBinary?: string | ProviderLaunchDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveProviderLaunch({
|
||||||
|
commandConfig,
|
||||||
|
defaultBinary,
|
||||||
|
}: ResolveProviderLaunchOptions): Promise<ResolvedProviderLaunch> {
|
||||||
|
if (commandConfig?.mode === "replace") {
|
||||||
|
const command = commandConfig.argv[0];
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
args: commandConfig.argv.slice(1),
|
||||||
|
source: "override",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultBinary === undefined) {
|
||||||
|
throw new Error("defaultBinary is required when provider command is not replaced");
|
||||||
|
}
|
||||||
|
const normalizedDefault = normalizeLaunchDefault(defaultBinary);
|
||||||
|
const args = commandConfig?.mode === "append" ? [...(commandConfig.args ?? [])] : [];
|
||||||
|
return {
|
||||||
|
command: normalizedDefault.command,
|
||||||
|
args,
|
||||||
|
source: commandConfig?.mode === "append" ? "append" : "default",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkProviderLaunchAvailable(
|
||||||
|
launch: ResolvedProviderLaunch,
|
||||||
|
defaultBinary?: ProviderLaunchDefault,
|
||||||
|
): Promise<ProviderLaunchAvailability> {
|
||||||
|
const resolvedPath =
|
||||||
|
defaultBinary && launch.source !== "override"
|
||||||
|
? await resolveDefaultLaunchPath(defaultBinary)
|
||||||
|
: await resolveLaunchPath(launch.command);
|
||||||
|
return {
|
||||||
|
available: resolvedPath !== null,
|
||||||
|
resolvedPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolveProviderCommandPrefix(
|
export async function resolveProviderCommandPrefix(
|
||||||
commandConfig: ProviderCommand | undefined,
|
commandConfig: ProviderCommand | undefined,
|
||||||
resolveDefaultCommand: () => string | Promise<string>,
|
resolveDefaultCommand: () => string | Promise<string>,
|
||||||
): Promise<ProviderCommandPrefix> {
|
): Promise<ProviderCommandPrefix> {
|
||||||
if (!commandConfig || commandConfig.mode === "default") {
|
if (commandConfig?.mode === "replace") {
|
||||||
|
const launch = await resolveProviderLaunch({
|
||||||
|
commandConfig,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
command: await resolveDefaultCommand(),
|
command: launch.command,
|
||||||
args: [],
|
args: launch.args,
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commandConfig.mode === "append") {
|
|
||||||
return {
|
|
||||||
command: await resolveDefaultCommand(),
|
|
||||||
args: [...(commandConfig.args ?? [])],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultCommand = await resolveDefaultCommand();
|
||||||
|
const launch = await resolveProviderLaunch({
|
||||||
|
commandConfig,
|
||||||
|
defaultBinary: {
|
||||||
|
command: defaultCommand,
|
||||||
|
resolvePath: async () => defaultCommand,
|
||||||
|
},
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
command: commandConfig.argv[0],
|
command: launch.command,
|
||||||
args: commandConfig.argv.slice(1),
|
args: launch.args,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,39 +306,27 @@ export function createProviderEnv(options: ProviderEnvOptions = {}): NodeJS.Proc
|
|||||||
return createExternalProcessEnv(spec.baseEnv ?? process.env, spec.envOverlay);
|
return createExternalProcessEnv(spec.baseEnv ?? process.env, spec.envOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findExecutable(name: string): string | null {
|
|
||||||
const trimmed = name.trim();
|
|
||||||
if (!trimmed) return null;
|
|
||||||
if (trimmed.includes("/") || trimmed.includes("\\")) {
|
|
||||||
try {
|
|
||||||
const { existsSync } = require("node:fs");
|
|
||||||
return existsSync(trimmed) ? trimmed : null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const cmd = process.platform === "win32" ? "where.exe" : "which";
|
|
||||||
const result = execFileSync(cmd, [trimmed], {
|
|
||||||
encoding: "utf8",
|
|
||||||
env: createProviderEnv({ baseEnv: process.env }),
|
|
||||||
windowsHide: true,
|
|
||||||
}).trim();
|
|
||||||
const lines = result.split(/\r?\n/).filter((l: string) => l.trim());
|
|
||||||
const candidate = lines.at(-1)?.trim() ?? null;
|
|
||||||
return candidate && path.isAbsolute(candidate) ? candidate : null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function isProviderCommandAvailable(
|
export async function isProviderCommandAvailable(
|
||||||
commandConfig: ProviderCommand | undefined,
|
commandConfig: ProviderCommand | undefined,
|
||||||
resolveDefaultCommand: () => string | Promise<string>,
|
resolveDefaultCommand: () => string | Promise<string>,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const prefix = await resolveProviderCommandPrefix(commandConfig, resolveDefaultCommand);
|
if (commandConfig?.mode === "replace") {
|
||||||
return isCommandAvailable(prefix.command);
|
const launch = await resolveProviderLaunch({
|
||||||
|
commandConfig,
|
||||||
|
});
|
||||||
|
const availability = await checkProviderLaunchAvailable(launch);
|
||||||
|
return availability.available;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultCommand = await resolveDefaultCommand();
|
||||||
|
const defaultBinary = {
|
||||||
|
command: defaultCommand,
|
||||||
|
resolvePath: async () => defaultCommand,
|
||||||
|
};
|
||||||
|
const launch = await resolveProviderLaunch({ commandConfig, defaultBinary });
|
||||||
|
const availability = await checkProviderLaunchAvailable(launch, defaultBinary);
|
||||||
|
return availability.available;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,13 +87,13 @@ import {
|
|||||||
type ToolCallTimelineItem,
|
type ToolCallTimelineItem,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
import {
|
import {
|
||||||
|
checkProviderLaunchAvailable,
|
||||||
createProviderEnvSpec,
|
createProviderEnvSpec,
|
||||||
resolveProviderCommandPrefix,
|
resolveProviderLaunch,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
} from "../provider-launch-config.js";
|
} from "../provider-launch-config.js";
|
||||||
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
||||||
import { appendOrReplaceGrowingAssistantMessage, runProviderTurn } from "./provider-runner.js";
|
import { appendOrReplaceGrowingAssistantMessage, runProviderTurn } from "./provider-runner.js";
|
||||||
import { findExecutable } from "../../../utils/executable.js";
|
|
||||||
import { platformShell, spawnProcess } from "../../../utils/spawn.js";
|
import { platformShell, spawnProcess } from "../../../utils/spawn.js";
|
||||||
|
|
||||||
function assertChildWithPipes(
|
function assertChildWithPipes(
|
||||||
@@ -862,13 +862,14 @@ export class ACPAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async resolveLaunchCommand(): Promise<{ command: string; args: string[] }> {
|
protected async resolveLaunchCommand(): Promise<{ command: string; args: string[] }> {
|
||||||
const resolved = await findExecutable(this.defaultCommand[0]);
|
const prefix = await resolveProviderLaunch({
|
||||||
const prefix = await resolveProviderCommandPrefix(this.runtimeSettings?.command, () => {
|
commandConfig: this.runtimeSettings?.command,
|
||||||
if (!resolved) {
|
defaultBinary: this.defaultCommand[0],
|
||||||
throw new Error(`${this.provider} command '${this.defaultCommand[0]}' not found`);
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
});
|
});
|
||||||
|
const availability = await checkProviderLaunchAvailable(prefix);
|
||||||
|
if (!availability.available) {
|
||||||
|
throw new Error(`${this.provider} command '${this.defaultCommand[0]}' not found`);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
command: prefix.command,
|
command: prefix.command,
|
||||||
args: [...prefix.args, ...this.defaultCommand.slice(1)],
|
args: [...prefix.args, ...this.defaultCommand.slice(1)],
|
||||||
@@ -1807,13 +1808,14 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async spawnProcess(): Promise<SpawnedACPProcess> {
|
private async spawnProcess(): Promise<SpawnedACPProcess> {
|
||||||
const resolved = await findExecutable(this.defaultCommand[0]);
|
const prefix = await resolveProviderLaunch({
|
||||||
const prefix = await resolveProviderCommandPrefix(this.runtimeSettings?.command, () => {
|
commandConfig: this.runtimeSettings?.command,
|
||||||
if (!resolved) {
|
defaultBinary: this.defaultCommand[0],
|
||||||
throw new Error(`${this.provider} command '${this.defaultCommand[0]}' not found`);
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
});
|
});
|
||||||
|
const availability = await checkProviderLaunchAvailable(prefix);
|
||||||
|
if (!availability.available) {
|
||||||
|
throw new Error(`${this.provider} command '${this.defaultCommand[0]}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
const command = prefix.command;
|
const command = prefix.command;
|
||||||
const args = [...prefix.args, ...this.defaultCommand.slice(1)];
|
const args = [...prefix.args, ...this.defaultCommand.slice(1)];
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { getClaudeModelsWithSettings, normalizeClaudeRuntimeModelId } from "./mo
|
|||||||
import { parsePartialJsonObject } from "./partial-json.js";
|
import { parsePartialJsonObject } from "./partial-json.js";
|
||||||
import { ClaudeSidechainTracker } from "./sidechain-tracker.js";
|
import { ClaudeSidechainTracker } from "./sidechain-tracker.js";
|
||||||
import {
|
import {
|
||||||
|
buildBinaryDiagnosticRows,
|
||||||
formatDiagnosticStatus,
|
formatDiagnosticStatus,
|
||||||
formatProviderDiagnostic,
|
formatProviderDiagnostic,
|
||||||
formatProviderDiagnosticError,
|
formatProviderDiagnosticError,
|
||||||
@@ -75,11 +76,13 @@ import {
|
|||||||
type PersistedAgentDescriptor,
|
type PersistedAgentDescriptor,
|
||||||
} from "../../agent-sdk-types.js";
|
} from "../../agent-sdk-types.js";
|
||||||
import {
|
import {
|
||||||
|
checkProviderLaunchAvailable,
|
||||||
createProviderEnv,
|
createProviderEnv,
|
||||||
createProviderEnvSpec,
|
createProviderEnvSpec,
|
||||||
|
resolveProviderLaunch,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
|
type ResolvedProviderLaunch,
|
||||||
} from "../../provider-launch-config.js";
|
} from "../../provider-launch-config.js";
|
||||||
import { findExecutable, isCommandAvailable } from "../../../../utils/executable.js";
|
|
||||||
import { withTimeout } from "../../../../utils/promise-timeout.js";
|
import { withTimeout } from "../../../../utils/promise-timeout.js";
|
||||||
import { execCommand } from "../../../../utils/spawn.js";
|
import { execCommand } from "../../../../utils/spawn.js";
|
||||||
import { composeSystemPromptParts } from "../../system-prompt.js";
|
import { composeSystemPromptParts } from "../../system-prompt.js";
|
||||||
@@ -1335,19 +1338,25 @@ export class ClaudeAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
const command = this.runtimeSettings?.command;
|
const launch = await resolveProviderLaunch({
|
||||||
if (command?.mode === "replace") {
|
commandConfig: this.runtimeSettings?.command,
|
||||||
return await isCommandAvailable(command.argv[0]);
|
defaultBinary: "claude",
|
||||||
}
|
});
|
||||||
return await isCommandAvailable("claude");
|
const availability = await checkProviderLaunchAvailable(launch);
|
||||||
|
return availability.available;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
||||||
try {
|
try {
|
||||||
const resolvedBinary = (await findExecutable("claude")) ?? "not found";
|
const launch = await resolveProviderLaunch({
|
||||||
const available = await this.isAvailable();
|
commandConfig: this.runtimeSettings?.command,
|
||||||
const version = await resolveClaudeVersion(this.runtimeSettings);
|
defaultBinary: "claude",
|
||||||
const auth = available ? await resolveClaudeAuth(this.runtimeSettings) : null;
|
});
|
||||||
|
const availability = await checkProviderLaunchAvailable(launch);
|
||||||
|
const available = availability.available;
|
||||||
|
const auth = available
|
||||||
|
? await resolveClaudeAuth(launch, availability, this.runtimeSettings)
|
||||||
|
: null;
|
||||||
let modelsValue = "Not checked";
|
let modelsValue = "Not checked";
|
||||||
let status = formatDiagnosticStatus(available);
|
let status = formatDiagnosticStatus(available);
|
||||||
|
|
||||||
@@ -1369,8 +1378,7 @@ export class ClaudeAgentClient implements AgentClient {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
diagnostic: formatProviderDiagnostic("Claude Code", [
|
diagnostic: formatProviderDiagnostic("Claude Code", [
|
||||||
{ label: "Binary", value: resolvedBinary },
|
...(await buildBinaryDiagnosticRows(launch, availability)),
|
||||||
...(version ? [{ label: "Version", value: version }] : []),
|
|
||||||
...(auth ? [{ label: "Auth", value: auth }] : []),
|
...(auth ? [{ label: "Auth", value: auth }] : []),
|
||||||
{ label: "Models", value: modelsValue },
|
{ label: "Models", value: modelsValue },
|
||||||
{ label: "Status", value: status },
|
{ label: "Status", value: status },
|
||||||
@@ -1392,59 +1400,24 @@ export class ClaudeAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resolveClaudeBinary(runtimeSettings?: ProviderRuntimeSettings): Promise<string> {
|
async function resolveClaudeBinary(runtimeSettings?: ProviderRuntimeSettings): Promise<string> {
|
||||||
const command = runtimeSettings?.command;
|
const launch = await resolveProviderLaunch({
|
||||||
if (command?.mode === "replace") {
|
commandConfig: runtimeSettings?.command,
|
||||||
const foundOverride = await findExecutable(command.argv[0]);
|
defaultBinary: "claude",
|
||||||
if (foundOverride) {
|
});
|
||||||
return foundOverride;
|
const availability = await checkProviderLaunchAvailable(launch);
|
||||||
}
|
if (availability.available) {
|
||||||
}
|
return availability.resolvedPath ?? launch.command;
|
||||||
|
|
||||||
const found = await findExecutable("claude");
|
|
||||||
if (found) {
|
|
||||||
return found;
|
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Claude binary not found. Install Claude Code (https://github.com/anthropics/claude-code) and ensure it is available in your shell PATH.",
|
"Claude binary not found. Install Claude Code (https://github.com/anthropics/claude-code) and ensure it is available in your shell PATH.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveClaudeVersion(
|
|
||||||
runtimeSettings?: ProviderRuntimeSettings,
|
|
||||||
): Promise<string | null> {
|
|
||||||
const command = runtimeSettings?.command;
|
|
||||||
const envSpec = createProviderEnvSpec({ runtimeSettings });
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (command?.mode === "replace") {
|
|
||||||
const { stdout } = await execCommand(
|
|
||||||
command.argv[0],
|
|
||||||
[...command.argv.slice(1), "--version"],
|
|
||||||
{ ...envSpec, timeout: 5_000 },
|
|
||||||
);
|
|
||||||
return stdout.trim() || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const executable = await findExecutable("claude");
|
|
||||||
if (!executable) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { stdout } = await execCommand(executable, ["--version"], {
|
|
||||||
...envSpec,
|
|
||||||
timeout: 5_000,
|
|
||||||
});
|
|
||||||
return stdout.trim() || null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveClaudeAuth(
|
async function resolveClaudeAuth(
|
||||||
|
launch: ResolvedProviderLaunch,
|
||||||
|
availability: { resolvedPath: string | null },
|
||||||
runtimeSettings?: ProviderRuntimeSettings,
|
runtimeSettings?: ProviderRuntimeSettings,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const command = runtimeSettings?.command;
|
|
||||||
|
|
||||||
const run = async (
|
const run = async (
|
||||||
executable: string,
|
executable: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
@@ -1464,16 +1437,8 @@ async function resolveClaudeAuth(
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result: { stdout: string; stderr: string };
|
const executable = availability.resolvedPath ?? launch.command;
|
||||||
if (command?.mode === "replace") {
|
const result = await run(executable, [...launch.args, "auth", "status"]);
|
||||||
result = await run(command.argv[0], [...command.argv.slice(1), "auth", "status"]);
|
|
||||||
} else {
|
|
||||||
const executable = await findExecutable("claude");
|
|
||||||
if (!executable) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
result = await run(executable, ["auth", "status"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const combined = [result.stdout, result.stderr]
|
const combined = [result.stdout, result.stderr]
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
|
|||||||
@@ -48,12 +48,14 @@ import {
|
|||||||
mapCodexToolCallFromThreadItem,
|
mapCodexToolCallFromThreadItem,
|
||||||
} from "./codex/tool-call-mapper.js";
|
} from "./codex/tool-call-mapper.js";
|
||||||
import {
|
import {
|
||||||
|
checkProviderLaunchAvailable,
|
||||||
createProviderEnv,
|
createProviderEnv,
|
||||||
createProviderEnvSpec,
|
createProviderEnvSpec,
|
||||||
resolveProviderCommandPrefix,
|
resolveProviderLaunch,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
|
type ResolvedProviderLaunch,
|
||||||
} from "../provider-launch-config.js";
|
} from "../provider-launch-config.js";
|
||||||
import { findExecutable, isCommandAvailable, probeExecutable } from "../../../utils/executable.js";
|
import { findExecutable, probeExecutable } from "../../../utils/executable.js";
|
||||||
import { createPathEquivalenceMatcher } from "../../../utils/path.js";
|
import { createPathEquivalenceMatcher } from "../../../utils/path.js";
|
||||||
import { spawnProcess } from "../../../utils/spawn.js";
|
import { spawnProcess } from "../../../utils/spawn.js";
|
||||||
import { extractCodexTerminalSessionId, nonEmptyString } from "./tool-call-mapper-utils.js";
|
import { extractCodexTerminalSessionId, nonEmptyString } from "./tool-call-mapper-utils.js";
|
||||||
@@ -78,6 +80,7 @@ import {
|
|||||||
formatDiagnosticStatus,
|
formatDiagnosticStatus,
|
||||||
formatProviderDiagnostic,
|
formatProviderDiagnostic,
|
||||||
formatProviderDiagnosticError,
|
formatProviderDiagnosticError,
|
||||||
|
buildBinaryDiagnosticRows,
|
||||||
resolveBinaryVersion,
|
resolveBinaryVersion,
|
||||||
toDiagnosticErrorMessage,
|
toDiagnosticErrorMessage,
|
||||||
} from "./diagnostic-utils.js";
|
} from "./diagnostic-utils.js";
|
||||||
@@ -441,21 +444,41 @@ export async function findDefaultCodexBinary(): Promise<string | null> {
|
|||||||
return (await findExecutable("codex")) ?? (await findCodexMicrosoftStoreBinary());
|
return (await findExecutable("codex")) ?? (await findCodexMicrosoftStoreBinary());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveCodexBinary(): Promise<string> {
|
|
||||||
const found = await findDefaultCodexBinary();
|
|
||||||
if (found) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
"Codex binary not found. Install the Codex CLI (https://github.com/openai/codex) and ensure it is available in your shell PATH.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveCodexLaunchPrefix(runtimeSettings?: ProviderRuntimeSettings): Promise<{
|
async function resolveCodexLaunchPrefix(runtimeSettings?: ProviderRuntimeSettings): Promise<{
|
||||||
command: string;
|
command: string;
|
||||||
args: string[];
|
args: string[];
|
||||||
}> {
|
}> {
|
||||||
return resolveProviderCommandPrefix(runtimeSettings?.command, resolveCodexBinary);
|
const launch = await resolveCodexLaunch(runtimeSettings);
|
||||||
|
const availability = await checkCodexLaunchAvailable(launch);
|
||||||
|
if (!availability.available) {
|
||||||
|
throw new Error(
|
||||||
|
"Codex binary not found. Install the Codex CLI (https://github.com/openai/codex) and ensure it is available in your shell PATH.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
command:
|
||||||
|
launch.source === "override" ? launch.command : (availability.resolvedPath ?? launch.command),
|
||||||
|
args: launch.args,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveCodexLaunch(
|
||||||
|
runtimeSettings?: ProviderRuntimeSettings,
|
||||||
|
): Promise<ResolvedProviderLaunch> {
|
||||||
|
return resolveProviderLaunch({
|
||||||
|
commandConfig: runtimeSettings?.command,
|
||||||
|
defaultBinary: {
|
||||||
|
command: "codex",
|
||||||
|
resolvePath: findDefaultCodexBinary,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkCodexLaunchAvailable(launch: ResolvedProviderLaunch) {
|
||||||
|
return checkProviderLaunchAvailable(launch, {
|
||||||
|
command: "codex",
|
||||||
|
resolvePath: findDefaultCodexBinary,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveCodexHomeDir(): string {
|
function resolveCodexHomeDir(): string {
|
||||||
@@ -5570,26 +5593,18 @@ export class CodexAppServerAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
const command = this.runtimeSettings?.command;
|
const launch = await resolveCodexLaunch(this.runtimeSettings);
|
||||||
if (command?.mode === "replace") {
|
const availability = await checkCodexLaunchAvailable(launch);
|
||||||
return await isCommandAvailable(command.argv[0]);
|
return availability.available;
|
||||||
}
|
|
||||||
return (await findDefaultCodexBinary()) !== null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
||||||
try {
|
try {
|
||||||
const available = await this.isAvailable();
|
const launch = await resolveCodexLaunch(this.runtimeSettings);
|
||||||
const resolvedBinary = await findDefaultCodexBinary();
|
const availability = await checkCodexLaunchAvailable(launch);
|
||||||
|
const available = availability.available;
|
||||||
const entries: Array<{ label: string; value: string }> = [
|
const entries: Array<{ label: string; value: string }> = [
|
||||||
{
|
...(await buildBinaryDiagnosticRows(launch, availability)),
|
||||||
label: "Binary",
|
|
||||||
value: resolvedBinary ?? "not found",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Version",
|
|
||||||
value: resolvedBinary ? await resolveBinaryVersion(resolvedBinary) : "unknown",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
let status = formatDiagnosticStatus(available);
|
let status = formatDiagnosticStatus(available);
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import { homedir } from "node:os";
|
|||||||
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
|
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
|
||||||
|
|
||||||
import type { AgentCapabilityFlags, AgentMode } from "../agent-sdk-types.js";
|
import type { AgentCapabilityFlags, AgentMode } from "../agent-sdk-types.js";
|
||||||
import type { ProviderRuntimeSettings } from "../provider-launch-config.js";
|
import {
|
||||||
import { findExecutable } from "../../../utils/executable.js";
|
checkProviderLaunchAvailable,
|
||||||
|
resolveProviderLaunch,
|
||||||
|
type ProviderRuntimeSettings,
|
||||||
|
} from "../provider-launch-config.js";
|
||||||
import {
|
import {
|
||||||
ACPAgentClient,
|
ACPAgentClient,
|
||||||
type ACPBeforeModeWriteResult,
|
type ACPBeforeModeWriteResult,
|
||||||
@@ -16,7 +19,7 @@ import {
|
|||||||
formatDiagnosticStatus,
|
formatDiagnosticStatus,
|
||||||
formatProviderDiagnostic,
|
formatProviderDiagnostic,
|
||||||
formatProviderDiagnosticError,
|
formatProviderDiagnosticError,
|
||||||
resolveBinaryVersion,
|
buildBinaryDiagnosticRows,
|
||||||
toDiagnosticErrorMessage,
|
toDiagnosticErrorMessage,
|
||||||
} from "./diagnostic-utils.js";
|
} from "./diagnostic-utils.js";
|
||||||
|
|
||||||
@@ -88,8 +91,12 @@ export class CopilotACPAgentClient extends ACPAgentClient {
|
|||||||
|
|
||||||
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
||||||
try {
|
try {
|
||||||
const available = await this.isAvailable();
|
const launch = await resolveProviderLaunch({
|
||||||
const resolvedBinary = await findExecutable("copilot");
|
commandConfig: this.runtimeSettings?.command,
|
||||||
|
defaultBinary: "copilot",
|
||||||
|
});
|
||||||
|
const availability = await checkProviderLaunchAvailable(launch);
|
||||||
|
const available = availability.available;
|
||||||
let modelsValue = "Not checked";
|
let modelsValue = "Not checked";
|
||||||
let status = formatDiagnosticStatus(available);
|
let status = formatDiagnosticStatus(available);
|
||||||
|
|
||||||
@@ -119,14 +126,7 @@ export class CopilotACPAgentClient extends ACPAgentClient {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
diagnostic: formatProviderDiagnostic("Copilot", [
|
diagnostic: formatProviderDiagnostic("Copilot", [
|
||||||
{
|
...(await buildBinaryDiagnosticRows(launch, availability)),
|
||||||
label: "Binary",
|
|
||||||
value: resolvedBinary ?? "not found",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Version",
|
|
||||||
value: resolvedBinary ? await resolveBinaryVersion(resolvedBinary) : "unknown",
|
|
||||||
},
|
|
||||||
{ label: "Models", value: modelsValue },
|
{ label: "Models", value: modelsValue },
|
||||||
{ label: "Status", value: status },
|
{ label: "Status", value: status },
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { createProviderEnvSpec, type ProviderRuntimeSettings } from "../provider-launch-config.js";
|
import {
|
||||||
|
createProviderEnvSpec,
|
||||||
|
type ProviderLaunchAvailability,
|
||||||
|
type ProviderRuntimeSettings,
|
||||||
|
type ResolvedProviderLaunch,
|
||||||
|
} from "../provider-launch-config.js";
|
||||||
import { execCommand } from "../../../utils/spawn.js";
|
import { execCommand } from "../../../utils/spawn.js";
|
||||||
|
|
||||||
interface DiagnosticEntry {
|
export interface DiagnosticEntry {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
@@ -138,6 +143,61 @@ export async function resolveBinaryVersion(binaryPath: string): Promise<string>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BinaryDiagnosticVersionCommand {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BinaryDiagnosticRowsOptions {
|
||||||
|
binaryLabel?: string;
|
||||||
|
versionCommand?: BinaryDiagnosticVersionCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveCommandVersion(invocation: BinaryDiagnosticVersionCommand): Promise<string> {
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await execCommand(invocation.command, invocation.args, {
|
||||||
|
...createProviderEnvSpec({ runtimeSettings: { env: invocation.env } }),
|
||||||
|
timeout: 5_000,
|
||||||
|
});
|
||||||
|
return stdout.trim() || stderr.trim() || "unknown";
|
||||||
|
} catch (error) {
|
||||||
|
return `error: ${toDiagnosticErrorMessage(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildBinaryDiagnosticRows(
|
||||||
|
launch: ResolvedProviderLaunch,
|
||||||
|
availability: ProviderLaunchAvailability,
|
||||||
|
options: BinaryDiagnosticRowsOptions = {},
|
||||||
|
): Promise<DiagnosticEntry[]> {
|
||||||
|
const defaultBinaryLabel = launch.source === "override" ? "Binary (override)" : "Binary";
|
||||||
|
const binaryLabel = options.binaryLabel ?? defaultBinaryLabel;
|
||||||
|
let version = "unknown";
|
||||||
|
if (options.versionCommand && availability.available) {
|
||||||
|
version = await resolveCommandVersion(options.versionCommand);
|
||||||
|
} else if (availability.available) {
|
||||||
|
version = await resolveCommandVersion({
|
||||||
|
command: availability.resolvedPath ?? launch.command,
|
||||||
|
args: [...launch.args, "--version"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: binaryLabel,
|
||||||
|
value: launch.command,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Resolved path",
|
||||||
|
value: availability.resolvedPath ?? "not found",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Version",
|
||||||
|
value: version,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export function formatConfiguredCommand(
|
export function formatConfiguredCommand(
|
||||||
defaultArgv: readonly string[],
|
defaultArgv: readonly string[],
|
||||||
runtimeSettings?: ProviderRuntimeSettings,
|
runtimeSettings?: ProviderRuntimeSettings,
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
|
|
||||||
import { findExecutable, isCommandAvailable } from "../../../utils/executable.js";
|
|
||||||
import { execCommand } from "../../../utils/spawn.js";
|
|
||||||
import type { AgentProvider } from "../agent-sdk-types.js";
|
import type { AgentProvider } from "../agent-sdk-types.js";
|
||||||
import { createProviderEnvSpec } from "../provider-launch-config.js";
|
import { checkProviderLaunchAvailable, resolveProviderLaunch } from "../provider-launch-config.js";
|
||||||
import {
|
import {
|
||||||
ACPAgentClient,
|
ACPAgentClient,
|
||||||
deriveModelDefinitionsFromACP,
|
deriveModelDefinitionsFromACP,
|
||||||
@@ -15,6 +13,7 @@ import {
|
|||||||
formatDiagnosticStatus,
|
formatDiagnosticStatus,
|
||||||
formatProviderDiagnostic,
|
formatProviderDiagnostic,
|
||||||
formatProviderDiagnosticError,
|
formatProviderDiagnosticError,
|
||||||
|
buildBinaryDiagnosticRows,
|
||||||
toDiagnosticErrorMessage,
|
toDiagnosticErrorMessage,
|
||||||
} from "./diagnostic-utils.js";
|
} from "./diagnostic-utils.js";
|
||||||
|
|
||||||
@@ -61,15 +60,18 @@ export class GenericACPAgentClient extends ACPAgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async isAvailable(): Promise<boolean> {
|
override async isAvailable(): Promise<boolean> {
|
||||||
return isCommandAvailable(this.command[0]);
|
const launch = await this.resolveConfiguredLaunch();
|
||||||
|
const availability = await checkProviderLaunchAvailable(launch);
|
||||||
|
return availability.available;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
||||||
const providerName = formatProviderName(this.label, this.providerId);
|
const providerName = formatProviderName(this.label, this.providerId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resolvedBinary = await findExecutable(this.command[0]);
|
const launch = await this.resolveConfiguredLaunch();
|
||||||
const available = resolvedBinary !== null;
|
const availability = await checkProviderLaunchAvailable(launch);
|
||||||
|
const available = availability.available;
|
||||||
const versionProbe = buildVersionProbeCommand(this.command);
|
const versionProbe = buildVersionProbeCommand(this.command);
|
||||||
const probeResult = available
|
const probeResult = available
|
||||||
? await this.runDiagnosticACPProbe()
|
? await this.runDiagnosticACPProbe()
|
||||||
@@ -85,17 +87,18 @@ export class GenericACPAgentClient extends ACPAgentClient {
|
|||||||
diagnostic: formatProviderDiagnostic(providerName, [
|
diagnostic: formatProviderDiagnostic(providerName, [
|
||||||
{ label: "Provider ID", value: this.providerId ?? "unknown" },
|
{ label: "Provider ID", value: this.providerId ?? "unknown" },
|
||||||
{ label: "Configured command", value: this.command.join(" ") },
|
{ label: "Configured command", value: this.command.join(" ") },
|
||||||
{ label: "Launcher binary", value: resolvedBinary ?? "not found" },
|
...(await buildBinaryDiagnosticRows(launch, availability, {
|
||||||
|
binaryLabel: "Launcher binary",
|
||||||
|
versionCommand: {
|
||||||
|
command: versionProbe.command,
|
||||||
|
args: versionProbe.args,
|
||||||
|
env: this.runtimeSettings?.env,
|
||||||
|
},
|
||||||
|
})),
|
||||||
{
|
{
|
||||||
label: "Version command",
|
label: "Version command",
|
||||||
value: formatCommand(versionProbe.command, versionProbe.args),
|
value: formatCommand(versionProbe.command, versionProbe.args),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Version",
|
|
||||||
value: resolvedBinary
|
|
||||||
? await resolveCommandVersion(versionProbe, this.runtimeSettings?.env)
|
|
||||||
: "unknown",
|
|
||||||
},
|
|
||||||
{ label: "ACP initialize", value: probeResult.initialize },
|
{ label: "ACP initialize", value: probeResult.initialize },
|
||||||
{ label: "ACP session/new", value: probeResult.session },
|
{ label: "ACP session/new", value: probeResult.session },
|
||||||
{ label: "Models", value: probeResult.models },
|
{ label: "Models", value: probeResult.models },
|
||||||
@@ -110,6 +113,13 @@ export class GenericACPAgentClient extends ACPAgentClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveConfiguredLaunch() {
|
||||||
|
return resolveProviderLaunch({
|
||||||
|
commandConfig: { mode: "replace", argv: this.command },
|
||||||
|
defaultBinary: this.command[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async runDiagnosticACPProbe(): Promise<ACPDiagnosticProbeResult> {
|
private async runDiagnosticACPProbe(): Promise<ACPDiagnosticProbeResult> {
|
||||||
let initializeValue = "Not checked";
|
let initializeValue = "Not checked";
|
||||||
let sessionValue = "Not checked";
|
let sessionValue = "Not checked";
|
||||||
@@ -277,21 +287,6 @@ function formatProbeError(currentValue: string, error: unknown): string {
|
|||||||
return `Error - ${toDiagnosticErrorMessage(error)}`;
|
return `Error - ${toDiagnosticErrorMessage(error)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveCommandVersion(
|
|
||||||
invocation: CommandInvocation,
|
|
||||||
env: Record<string, string> | undefined,
|
|
||||||
): Promise<string> {
|
|
||||||
try {
|
|
||||||
const { stdout, stderr } = await execCommand(invocation.command, invocation.args, {
|
|
||||||
...createProviderEnvSpec({ runtimeSettings: { env } }),
|
|
||||||
timeout: 5_000,
|
|
||||||
});
|
|
||||||
return stdout.trim() || stderr.trim() || "unknown";
|
|
||||||
} catch (error) {
|
|
||||||
return `error: ${toDiagnosticErrorMessage(error)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
||||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
type Session as OpenCodeSession,
|
type Session as OpenCodeSession,
|
||||||
type TextPartInput as OpenCodeTextPartInput,
|
type TextPartInput as OpenCodeTextPartInput,
|
||||||
} from "@opencode-ai/sdk/v2/client";
|
} from "@opencode-ai/sdk/v2/client";
|
||||||
import { findExecutable, isCommandAvailable } from "../../../utils/executable.js";
|
|
||||||
import { createPathEquivalenceMatcher } from "../../../utils/path.js";
|
import { createPathEquivalenceMatcher } from "../../../utils/path.js";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -46,7 +45,12 @@ import {
|
|||||||
type ToolCallDetail,
|
type ToolCallDetail,
|
||||||
type ToolCallTimelineItem,
|
type ToolCallTimelineItem,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
import { createProviderEnvSpec, type ProviderRuntimeSettings } from "../provider-launch-config.js";
|
import {
|
||||||
|
checkProviderLaunchAvailable,
|
||||||
|
createProviderEnvSpec,
|
||||||
|
resolveProviderLaunch,
|
||||||
|
type ProviderRuntimeSettings,
|
||||||
|
} from "../provider-launch-config.js";
|
||||||
import { withTimeout } from "../../../utils/promise-timeout.js";
|
import { withTimeout } from "../../../utils/promise-timeout.js";
|
||||||
import { execCommand } from "../../../utils/spawn.js";
|
import { execCommand } from "../../../utils/spawn.js";
|
||||||
import { buildToolCallDisplayModel } from "../../../shared/tool-call-display.js";
|
import { buildToolCallDisplayModel } from "../../../shared/tool-call-display.js";
|
||||||
@@ -56,7 +60,7 @@ import {
|
|||||||
formatDiagnosticStatus,
|
formatDiagnosticStatus,
|
||||||
formatProviderDiagnostic,
|
formatProviderDiagnostic,
|
||||||
formatProviderDiagnosticError,
|
formatProviderDiagnosticError,
|
||||||
resolveBinaryVersion,
|
buildBinaryDiagnosticRows,
|
||||||
toDiagnosticErrorMessage,
|
toDiagnosticErrorMessage,
|
||||||
} from "./diagnostic-utils.js";
|
} from "./diagnostic-utils.js";
|
||||||
import { runProviderTurn } from "./provider-runner.js";
|
import { runProviderTurn } from "./provider-runner.js";
|
||||||
@@ -1391,17 +1395,22 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
const command = this.runtimeSettings?.command;
|
const launch = await resolveProviderLaunch({
|
||||||
if (command?.mode === "replace") {
|
commandConfig: this.runtimeSettings?.command,
|
||||||
return await isCommandAvailable(command.argv[0]);
|
defaultBinary: "opencode",
|
||||||
}
|
});
|
||||||
return await isCommandAvailable("opencode");
|
const availability = await checkProviderLaunchAvailable(launch);
|
||||||
|
return availability.available;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
||||||
try {
|
try {
|
||||||
const available = await this.isAvailable();
|
const launch = await resolveProviderLaunch({
|
||||||
const resolvedBinary = await findExecutable("opencode");
|
commandConfig: this.runtimeSettings?.command,
|
||||||
|
defaultBinary: "opencode",
|
||||||
|
});
|
||||||
|
const availability = await checkProviderLaunchAvailable(launch);
|
||||||
|
const available = availability.available;
|
||||||
let serverStatus = "Not running";
|
let serverStatus = "Not running";
|
||||||
let modelsValue = "Not checked";
|
let modelsValue = "Not checked";
|
||||||
let status = formatDiagnosticStatus(available);
|
let status = formatDiagnosticStatus(available);
|
||||||
@@ -1414,12 +1423,19 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let authValue = "Not checked";
|
let authValue = "Not checked";
|
||||||
if (resolvedBinary) {
|
const authCommand = availability.available
|
||||||
|
? (availability.resolvedPath ?? launch.command)
|
||||||
|
: null;
|
||||||
|
if (authCommand) {
|
||||||
try {
|
try {
|
||||||
const { stdout, stderr } = await execCommand(resolvedBinary, ["auth", "list"], {
|
const { stdout, stderr } = await execCommand(
|
||||||
...createProviderEnvSpec(),
|
authCommand,
|
||||||
timeout: 5_000,
|
[...launch.args, "auth", "list"],
|
||||||
});
|
{
|
||||||
|
...createProviderEnvSpec(),
|
||||||
|
timeout: 5_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
const text = (stdout.trim() || stderr.trim()).trim();
|
const text = (stdout.trim() || stderr.trim()).trim();
|
||||||
authValue = text ? `\n ${text.replace(/\n/g, "\n ")}` : "(empty)";
|
authValue = text ? `\n ${text.replace(/\n/g, "\n ")}` : "(empty)";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1453,14 +1469,7 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
diagnostic: formatProviderDiagnostic("OpenCode", [
|
diagnostic: formatProviderDiagnostic("OpenCode", [
|
||||||
{
|
...(await buildBinaryDiagnosticRows(launch, availability)),
|
||||||
label: "Binary",
|
|
||||||
value: resolvedBinary ?? "not found",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Version",
|
|
||||||
value: resolvedBinary ? await resolveBinaryVersion(resolvedBinary) : "unknown",
|
|
||||||
},
|
|
||||||
{ label: "Server", value: serverStatus },
|
{ label: "Server", value: serverStatus },
|
||||||
{ label: "Auth", value: authValue },
|
{ label: "Auth", value: authValue },
|
||||||
{ label: "Models", value: modelsValue },
|
{ label: "Models", value: modelsValue },
|
||||||
|
|||||||
@@ -30,15 +30,19 @@ import {
|
|||||||
type PersistedAgentDescriptor,
|
type PersistedAgentDescriptor,
|
||||||
} from "../../agent-sdk-types.js";
|
} from "../../agent-sdk-types.js";
|
||||||
import { runProviderTurn } from "../provider-runner.js";
|
import { runProviderTurn } from "../provider-runner.js";
|
||||||
import type { ProviderRuntimeSettings } from "../../provider-launch-config.js";
|
import {
|
||||||
|
checkProviderLaunchAvailable,
|
||||||
|
resolveProviderLaunch,
|
||||||
|
type ProviderRuntimeSettings,
|
||||||
|
type ResolvedProviderLaunch,
|
||||||
|
} from "../../provider-launch-config.js";
|
||||||
import { renderPromptAttachmentAsText } from "../../prompt-attachments.js";
|
import { renderPromptAttachmentAsText } from "../../prompt-attachments.js";
|
||||||
import { composeSystemPromptParts } from "../../system-prompt.js";
|
import { composeSystemPromptParts } from "../../system-prompt.js";
|
||||||
import { findExecutable } from "../../../../utils/executable.js";
|
|
||||||
import {
|
import {
|
||||||
|
buildBinaryDiagnosticRows,
|
||||||
formatDiagnosticStatus,
|
formatDiagnosticStatus,
|
||||||
formatProviderDiagnostic,
|
formatProviderDiagnostic,
|
||||||
formatProviderDiagnosticError,
|
formatProviderDiagnosticError,
|
||||||
resolveBinaryVersion,
|
|
||||||
toDiagnosticErrorMessage,
|
toDiagnosticErrorMessage,
|
||||||
} from "../diagnostic-utils.js";
|
} from "../diagnostic-utils.js";
|
||||||
import {
|
import {
|
||||||
@@ -1484,8 +1488,9 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
const binary = await this.resolvePiBinary();
|
const launch = await this.resolvePiLaunch();
|
||||||
if (!binary) {
|
const availability = await checkProviderLaunchAvailable(launch);
|
||||||
|
if (!availability.available) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const runtimeSession = await this.runtime.startSession({ cwd: homedir() }).catch(() => null);
|
const runtimeSession = await this.runtime.startSession({ cwd: homedir() }).catch(() => null);
|
||||||
@@ -1503,16 +1508,16 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
|
|
||||||
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
||||||
try {
|
try {
|
||||||
const available = await this.isAvailable();
|
const launch = await this.resolvePiLaunch();
|
||||||
const binary = await this.resolvePiBinary();
|
const availability = await checkProviderLaunchAvailable(launch);
|
||||||
const version = binary ? await resolveBinaryVersion(binary) : "unknown";
|
const available = availability.available;
|
||||||
const authConfigPath = join(homedir(), ".pi", "agent", "auth.json");
|
const authConfigPath = join(homedir(), ".pi", "agent", "auth.json");
|
||||||
let modelsValue = "Not checked";
|
let modelsValue = "Not checked";
|
||||||
let configuredProvidersValue = "none";
|
let configuredProvidersValue = "none";
|
||||||
let mcpToolsValue = "Not checked";
|
let mcpToolsValue = "Not checked";
|
||||||
let status = formatDiagnosticStatus(available);
|
let status = formatDiagnosticStatus(available);
|
||||||
|
|
||||||
if (binary) {
|
if (availability.available) {
|
||||||
const runtimeSession = await this.runtime
|
const runtimeSession = await this.runtime
|
||||||
.startSession({ cwd: homedir() })
|
.startSession({ cwd: homedir() })
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -1550,8 +1555,7 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
diagnostic: formatProviderDiagnostic("Pi", [
|
diagnostic: formatProviderDiagnostic("Pi", [
|
||||||
{ label: "Binary", value: binary ?? "not found" },
|
...(await buildBinaryDiagnosticRows(launch, availability)),
|
||||||
{ label: "Version", value: version },
|
|
||||||
{ label: "Configured providers", value: configuredProvidersValue },
|
{ label: "Configured providers", value: configuredProvidersValue },
|
||||||
{
|
{
|
||||||
label: "Auth config (~/.pi/agent/auth.json)",
|
label: "Auth config (~/.pi/agent/auth.json)",
|
||||||
@@ -1601,11 +1605,10 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolvePiBinary(): Promise<string | null> {
|
private async resolvePiLaunch(): Promise<ResolvedProviderLaunch> {
|
||||||
const command = this.runtimeSettings?.command;
|
return resolveProviderLaunch({
|
||||||
if (command?.mode === "replace" && command.argv[0]) {
|
commandConfig: this.runtimeSettings?.command,
|
||||||
return await findExecutable(command.argv[0]);
|
defaultBinary: PI_BINARY_COMMAND,
|
||||||
}
|
});
|
||||||
return await findExecutable(PI_BINARY_COMMAND);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user