feat: add provider availability check before creating sessions

This commit is contained in:
Mohamed Boudra
2026-02-02 21:35:50 +07:00
parent e9325d92be
commit 303a82d1c7
7 changed files with 51 additions and 1 deletions

View File

@@ -340,6 +340,10 @@ export class AgentManager {
agentId: resolvedAgentId,
});
const client = this.requireClient(normalizedConfig.provider);
const available = await client.isAvailable();
if (!available) {
throw new Error(`Provider '${normalizedConfig.provider}' is not available. Please ensure the CLI is installed.`);
}
const session = await client.createSession(normalizedConfig);
return this.registerSession(
session,

View File

@@ -287,4 +287,9 @@ export interface AgentClient {
resumeSession(handle: AgentPersistenceHandle, overrides?: Partial<AgentSessionConfig>): Promise<AgentSession>;
listModels(options?: ListModelsOptions): Promise<AgentModelDefinition[]>;
listPersistedAgents?(options?: ListPersistedAgentsOptions): Promise<PersistedAgentDescriptor[]>;
/**
* Check if this provider is available (CLI binary is installed).
* Returns true if available, false otherwise.
*/
isAvailable(): Promise<boolean>;
}

View File

@@ -1,3 +1,4 @@
import { execSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import fs from "node:fs";
import { promises } from "node:fs";
@@ -403,6 +404,15 @@ export class ClaudeAgentClient implements AgentClient {
return descriptors;
}
async isAvailable(): Promise<boolean> {
try {
const claudePath = execSync("which claude", { encoding: "utf8" }).trim();
return Boolean(claudePath);
} catch {
return false;
}
}
private assertConfig(config: AgentSessionConfig): ClaudeAgentConfig {
if (config.provider !== "claude") {
throw new Error(`ClaudeAgentClient received config for provider '${config.provider}'`);

View File

@@ -1729,4 +1729,13 @@ export class CodexAppServerAgentClient implements AgentClient {
await client.dispose();
}
}
async isAvailable(): Promise<boolean> {
try {
const codexPath = execSync("which codex", { encoding: "utf8" }).trim();
return Boolean(codexPath);
} catch {
return false;
}
}
}

View File

@@ -4800,6 +4800,15 @@ export class CodexMcpAgentClient implements AgentClient {
await client.dispose();
}
}
async isAvailable(): Promise<boolean> {
try {
const codexPath = execSync("which codex", { encoding: "utf8" }).trim();
return Boolean(codexPath);
} catch {
return false;
}
}
}
export const __test__ = {

View File

@@ -1,4 +1,4 @@
import { spawn, type ChildProcess } from "node:child_process";
import { execSync, spawn, type ChildProcess } from "node:child_process";
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client";
import net from "node:net";
import type { Logger } from "pino";
@@ -318,6 +318,15 @@ export class OpenCodeAgentClient implements AgentClient {
return [];
}
async isAvailable(): Promise<boolean> {
try {
const opencodePath = execSync("which opencode", { encoding: "utf8" }).trim();
return Boolean(opencodePath);
} catch {
return false;
}
}
private assertConfig(config: AgentSessionConfig): OpenCodeAgentConfig {
if (config.provider !== "opencode") {
throw new Error(`OpenCodeAgentClient received config for provider '${config.provider}'`);

View File

@@ -664,6 +664,10 @@ class FakeAgentClient implements AgentClient {
{ provider: this.provider, id: "test-model", label: "Test Model", isDefault: true },
];
}
async isAvailable(): Promise<boolean> {
return true;
}
}
export function createTestAgentClients(): Record<string, AgentClient> {