import { buildProviderAwareCliEnv } from '@main/services/runtime/providerAwareCliEnv'; import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver'; import { execCli, killProcessTree, spawnCli } from '@main/utils/childProcess'; import { resolveInteractiveShellEnv } from '@main/utils/shellEnv'; import type { RuntimeProviderManagementApi, RuntimeProviderManagementConnectApiKeyInput, RuntimeProviderManagementConnectInput, RuntimeProviderManagementDirectoryResponse, RuntimeProviderManagementErrorDto, RuntimeProviderManagementForgetInput, RuntimeProviderManagementLoadDirectoryInput, RuntimeProviderManagementLoadSetupFormInput, RuntimeProviderManagementLoadModelsInput, RuntimeProviderManagementLoadViewInput, RuntimeProviderManagementModelsResponse, RuntimeProviderManagementModelTestResponse, RuntimeProviderManagementProviderResponse, RuntimeProviderManagementRuntimeId, RuntimeProviderManagementSetDefaultModelInput, RuntimeProviderManagementSetupFormResponse, RuntimeProviderManagementTestModelInput, RuntimeProviderManagementViewResponse, } from '@features/runtime-provider-management/contracts'; import type { ChildProcessWithoutNullStreams } from 'child_process'; const COMMAND_TIMEOUT_MS = 45_000; const PROBE_COMMAND_TIMEOUT_MS = 90_000; const COMMAND_ERROR_DETAIL_LIMIT = 1_600; type RuntimeProviderManagementErrorResponse = | RuntimeProviderManagementViewResponse | RuntimeProviderManagementDirectoryResponse | RuntimeProviderManagementProviderResponse | RuntimeProviderManagementSetupFormResponse | RuntimeProviderManagementModelsResponse | RuntimeProviderManagementModelTestResponse; function errorResponse( runtimeId: RuntimeProviderManagementRuntimeId, message: string, code: RuntimeProviderManagementErrorDto['code'] = 'runtime-unhealthy' ): T { return { schemaVersion: 1, runtimeId, error: { code, message, recoverable: true, }, } as T; } function extractJsonObject(raw: string): T { const start = raw.indexOf('{'); const end = raw.lastIndexOf('}'); if (start < 0 || end < start) { throw new Error('CLI did not return a JSON object'); } return JSON.parse(raw.slice(start, end + 1)) as T; } function tryExtractJsonObject(raw: string | null): T | null { if (!raw) { return null; } try { return extractJsonObject(raw); } catch { return null; } } function readErrorTextProperty(error: unknown, propertyName: 'stderr' | 'stdout'): string | null { if (!error || typeof error !== 'object' || !(propertyName in error)) { return null; } const value = (error as Record)[propertyName]; if (typeof value === 'string') { const trimmed = value.trim(); return trimmed ? trimmed : null; } return null; } function extractJsonObjectFromError(error: unknown): T | null { return ( tryExtractJsonObject(readErrorTextProperty(error, 'stdout')) ?? tryExtractJsonObject(readErrorTextProperty(error, 'stderr')) ); } function truncateCommandErrorDetail(message: string): string { if (message.length <= COMMAND_ERROR_DETAIL_LIMIT) { return message; } return `${message.slice(0, COMMAND_ERROR_DETAIL_LIMIT).trimEnd()}...`; } function normalizeCommandFailure(error: unknown): string { const stderr = readErrorTextProperty(error, 'stderr'); if (stderr) { return truncateCommandErrorDetail(stderr); } const stdout = readErrorTextProperty(error, 'stdout'); if (stdout) { return truncateCommandErrorDetail(stdout); } if (error instanceof Error && error.message.trim()) { return truncateCommandErrorDetail(error.message); } return 'Runtime provider management command failed'; } function normalizeProjectPath(projectPath: string | null | undefined): string | null { const normalized = projectPath?.trim(); return normalized ? normalized : null; } function appendProjectPathArgs(args: string[], projectPath: string | null): string[] { return projectPath ? [...args, '--project-path', projectPath] : args; } function appendOptionalArg(args: string[], name: string, value: string | null | undefined): void { const normalized = value?.trim(); if (normalized) { args.push(name, normalized); } } function runtimeProviderCommandOptions( options: T, projectPath: string | null ): T & { cwd?: string } { return projectPath ? { ...options, cwd: projectPath } : options; } async function resolveCliEnv(): Promise<{ binaryPath: string | null; env: NodeJS.ProcessEnv; }> { const shellEnv = await resolveInteractiveShellEnv(); const binaryPath = await ClaudeBinaryResolver.resolve(); if (!binaryPath) { return { binaryPath: null, env: { ...process.env, ...shellEnv, }, }; } const providerAware = await buildProviderAwareCliEnv({ binaryPath, providerId: 'opencode', shellEnv, connectionMode: 'augment', }); return { binaryPath, env: providerAware.env, }; } function collectSpawnOutput( child: ChildProcessWithoutNullStreams, stdinValue: string ): Promise<{ stdout: string; stderr: string; code: number | null }> { return new Promise((resolve, reject) => { const stdout: Buffer[] = []; const stderr: Buffer[] = []; let settled = false; const timeout = setTimeout(() => { if (settled) { return; } settled = true; killProcessTree(child, 'SIGKILL'); reject(new Error('Runtime provider management command timed out')); }, COMMAND_TIMEOUT_MS); child.stdout.on('data', (chunk: Buffer) => stdout.push(chunk)); child.stderr.on('data', (chunk: Buffer) => stderr.push(chunk)); child.once('error', (error) => { if (settled) { return; } settled = true; clearTimeout(timeout); reject(error); }); child.once('close', (code) => { if (settled) { return; } settled = true; clearTimeout(timeout); resolve({ stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'), code, }); }); child.stdin.write(stdinValue); child.stdin.end(); }); } export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProviderManagementApi { async loadView( input: RuntimeProviderManagementLoadViewInput ): Promise { const { binaryPath, env } = await resolveCliEnv(); if (!binaryPath) { return errorResponse( input.runtimeId, 'Multimodel runtime binary was not found.', 'runtime-missing' ); } const projectPath = normalizeProjectPath(input.projectPath); try { const { stdout } = await execCli( binaryPath, appendProjectPathArgs( ['runtime', 'providers', 'view', '--runtime', input.runtimeId, '--json', '--compact'], projectPath ), runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath) ); return extractJsonObject(stdout); } catch (error) { const response = extractJsonObjectFromError(error); if (response) { return response; } return errorResponse( input.runtimeId, normalizeCommandFailure(error) ); } } async loadProviderDirectory( input: RuntimeProviderManagementLoadDirectoryInput ): Promise { const { binaryPath, env } = await resolveCliEnv(); if (!binaryPath) { return errorResponse( input.runtimeId, 'Multimodel runtime binary was not found.', 'runtime-missing' ); } const projectPath = normalizeProjectPath(input.projectPath); const args = ['runtime', 'providers', 'directory', '--runtime', input.runtimeId, '--json']; appendOptionalArg(args, '--project-path', projectPath); appendOptionalArg(args, '--query', input.query ?? null); appendOptionalArg(args, '--filter', input.filter ?? null); if (typeof input.limit === 'number' && Number.isFinite(input.limit) && input.limit > 0) { args.push('--limit', String(Math.floor(input.limit))); } appendOptionalArg(args, '--cursor', input.cursor ?? null); if (input.refresh) { args.push('--refresh'); } try { const { stdout } = await execCli( binaryPath, args, runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath) ); return extractJsonObject(stdout); } catch (error) { const response = extractJsonObjectFromError(error); if (response) { return response; } return errorResponse( input.runtimeId, normalizeCommandFailure(error) ); } } async loadSetupForm( input: RuntimeProviderManagementLoadSetupFormInput ): Promise { const { binaryPath, env } = await resolveCliEnv(); if (!binaryPath) { return errorResponse( input.runtimeId, 'Multimodel runtime binary was not found.', 'runtime-missing' ); } const projectPath = normalizeProjectPath(input.projectPath); try { const { stdout } = await execCli( binaryPath, appendProjectPathArgs( [ 'runtime', 'providers', 'setup-form', '--runtime', input.runtimeId, '--provider', input.providerId, '--json', ], projectPath ), runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath) ); return extractJsonObject(stdout); } catch (error) { const response = extractJsonObjectFromError(error); if (response) { return response; } return errorResponse( input.runtimeId, normalizeCommandFailure(error) ); } } async connectProvider( input: RuntimeProviderManagementConnectInput ): Promise { const { binaryPath, env } = await resolveCliEnv(); if (!binaryPath) { return errorResponse( input.runtimeId, 'Multimodel runtime binary was not found.', 'runtime-missing' ); } const projectPath = normalizeProjectPath(input.projectPath); try { const child = spawnCli( binaryPath, appendProjectPathArgs( [ 'runtime', 'providers', 'connect', '--runtime', input.runtimeId, '--provider', input.providerId, '--stdin-json', '--json', ], projectPath ), runtimeProviderCommandOptions( { env, stdio: 'pipe' as const, }, projectPath ) ) as ChildProcessWithoutNullStreams; const result = await collectSpawnOutput( child, JSON.stringify({ method: input.method, apiKey: input.apiKey ?? null, metadata: input.metadata ?? {}, }) ); if (result.code === 0) { return extractJsonObject(result.stdout); } try { return extractJsonObject(result.stdout); } catch { return errorResponse( input.runtimeId, `Runtime provider connect command failed with exit code ${String(result.code ?? 'unknown')}.` ); } } catch (error) { const response = extractJsonObjectFromError(error); if (response) { return response; } return errorResponse( input.runtimeId, normalizeCommandFailure(error) ); } } async connectWithApiKey( input: RuntimeProviderManagementConnectApiKeyInput ): Promise { const { binaryPath, env } = await resolveCliEnv(); if (!binaryPath) { return errorResponse( input.runtimeId, 'Multimodel runtime binary was not found.', 'runtime-missing' ); } const projectPath = normalizeProjectPath(input.projectPath); try { const child = spawnCli( binaryPath, appendProjectPathArgs( [ 'runtime', 'providers', 'connect-api-key', '--runtime', input.runtimeId, '--provider', input.providerId, '--stdin-key', '--json', ], projectPath ), runtimeProviderCommandOptions( { env, stdio: 'pipe' as const, }, projectPath ) ) as ChildProcessWithoutNullStreams; const result = await collectSpawnOutput(child, input.apiKey); if (result.code === 0) { return extractJsonObject(result.stdout); } try { return extractJsonObject(result.stdout); } catch { return errorResponse( input.runtimeId, `Runtime provider connect command failed with exit code ${String(result.code ?? 'unknown')}.` ); } } catch (error) { const response = extractJsonObjectFromError(error); if (response) { return response; } return errorResponse( input.runtimeId, normalizeCommandFailure(error) ); } } async forgetCredential( input: RuntimeProviderManagementForgetInput ): Promise { const { binaryPath, env } = await resolveCliEnv(); if (!binaryPath) { return errorResponse( input.runtimeId, 'Multimodel runtime binary was not found.', 'runtime-missing' ); } const projectPath = normalizeProjectPath(input.projectPath); try { const { stdout } = await execCli( binaryPath, appendProjectPathArgs( [ 'runtime', 'providers', 'forget', '--runtime', input.runtimeId, '--provider', input.providerId, '--json', ], projectPath ), runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath) ); return extractJsonObject(stdout); } catch (error) { const response = extractJsonObjectFromError(error); if (response) { return response; } return errorResponse( input.runtimeId, normalizeCommandFailure(error) ); } } async loadModels( input: RuntimeProviderManagementLoadModelsInput ): Promise { const { binaryPath, env } = await resolveCliEnv(); if (!binaryPath) { return errorResponse( input.runtimeId, 'Multimodel runtime binary was not found.', 'runtime-missing' ); } const projectPath = normalizeProjectPath(input.projectPath); let args = [ 'runtime', 'providers', 'models', '--runtime', input.runtimeId, '--provider', input.providerId, '--json', ]; if (input.query?.trim()) { args.push('--query', input.query.trim()); } if (typeof input.limit === 'number' && Number.isFinite(input.limit) && input.limit > 0) { args.push('--limit', String(Math.floor(input.limit))); } args = appendProjectPathArgs(args, projectPath); try { const { stdout } = await execCli(binaryPath, args, { ...runtimeProviderCommandOptions({ env }, projectPath), timeout: COMMAND_TIMEOUT_MS, }); return extractJsonObject(stdout); } catch (error) { const response = extractJsonObjectFromError(error); if (response) { return response; } return errorResponse( input.runtimeId, normalizeCommandFailure(error) ); } } async testModel( input: RuntimeProviderManagementTestModelInput ): Promise { const { binaryPath, env } = await resolveCliEnv(); if (!binaryPath) { return errorResponse( input.runtimeId, 'Multimodel runtime binary was not found.', 'runtime-missing' ); } const projectPath = normalizeProjectPath(input.projectPath); try { const { stdout } = await execCli( binaryPath, appendProjectPathArgs( [ 'runtime', 'providers', 'test-model', '--runtime', input.runtimeId, '--provider', input.providerId, '--model', input.modelId, '--json', ], projectPath ), runtimeProviderCommandOptions({ env, timeout: PROBE_COMMAND_TIMEOUT_MS }, projectPath) ); return extractJsonObject(stdout); } catch (error) { const response = extractJsonObjectFromError(error); if (response) { return response; } return errorResponse( input.runtimeId, normalizeCommandFailure(error), 'model-test-failed' ); } } async setDefaultModel( input: RuntimeProviderManagementSetDefaultModelInput ): Promise { const { binaryPath, env } = await resolveCliEnv(); if (!binaryPath) { return errorResponse( input.runtimeId, 'Multimodel runtime binary was not found.', 'runtime-missing' ); } const projectPath = normalizeProjectPath(input.projectPath); try { const { stdout } = await execCli( binaryPath, appendProjectPathArgs( [ 'runtime', 'providers', 'set-default', '--runtime', input.runtimeId, '--provider', input.providerId, '--model', input.modelId, '--probe', '--compact', '--json', ], projectPath ), runtimeProviderCommandOptions({ env, timeout: PROBE_COMMAND_TIMEOUT_MS }, projectPath) ); return extractJsonObject(stdout); } catch (error) { const response = extractJsonObjectFromError(error); if (response) { return response; } return errorResponse( input.runtimeId, normalizeCommandFailure(error), 'model-test-failed' ); } } }