/** * Copilot Executor * * Main execution flow for running Claude Code with copilot-api proxy. * Uses local installation from ~/.ccs/copilot/ (managed by copilot-package-manager). */ import { spawn } from 'child_process'; import { CopilotConfig } from '../config/unified-config-types'; import { getGlobalEnvConfig } from '../config/unified-config-loader'; import { checkAuthStatus, isCopilotApiInstalled } from './copilot-auth'; import { isDaemonRunning, startDaemon } from './copilot-daemon'; import { ensureCopilotApi } from './copilot-package-manager'; import { normalizeCopilotConfigWithWarnings } from './copilot-model-normalizer'; import { CopilotStatus } from './types'; import { fail, info, ok, warn } from '../utils/ui'; import { getWebSearchHookEnv, appendThirdPartyWebSearchToolArgs, createWebSearchTraceContext, syncWebSearchMcpToConfigDir, } from '../utils/websearch-manager'; import { getImageAnalysisHookEnv } from '../utils/hooks'; import { stripClaudeCodeEnv } from '../utils/shell-executor'; /** * Get full copilot status (auth + daemon). */ export async function getCopilotStatus(config: CopilotConfig): Promise { const normalizedConfig = normalizeCopilotConfigWithWarnings(config).config; const [auth, daemonRunning] = await Promise.all([ checkAuthStatus(), isDaemonRunning(normalizedConfig.port), ]); return { auth, daemon: { running: daemonRunning, port: normalizedConfig.port, }, }; } /** * Generate environment variables for Claude Code to use copilot-api. * Uses model mapping for opus/sonnet/haiku tiers if configured. */ export function generateCopilotEnv( config: CopilotConfig, claudeConfigDir?: string ): Record { const normalizedConfig = normalizeCopilotConfigWithWarnings(config).config; // Use mapped models if configured, otherwise fall back to default model const opusModel = normalizedConfig.opus_model || normalizedConfig.model; const sonnetModel = normalizedConfig.sonnet_model || normalizedConfig.model; const haikuModel = normalizedConfig.haiku_model || normalizedConfig.model; // Use 127.0.0.1 instead of localhost for more reliable local connections // (bypasses DNS resolution and potential IPv6 issues) return { ANTHROPIC_BASE_URL: `http://127.0.0.1:${normalizedConfig.port}`, ANTHROPIC_AUTH_TOKEN: 'dummy', // copilot-api handles auth internally ANTHROPIC_MODEL: normalizedConfig.model, // Model tier mapping - allows different models for opus/sonnet/haiku ANTHROPIC_DEFAULT_OPUS_MODEL: opusModel, ANTHROPIC_DEFAULT_SONNET_MODEL: sonnetModel, ANTHROPIC_SMALL_FAST_MODEL: haikuModel, ANTHROPIC_DEFAULT_HAIKU_MODEL: haikuModel, // Disable non-essential traffic to avoid rate limiting DISABLE_NON_ESSENTIAL_MODEL_CALLS: '1', CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', ...(claudeConfigDir ? { CLAUDE_CONFIG_DIR: claudeConfigDir } : {}), }; } /** * Execute Claude Code with copilot-api proxy. * * @param config Copilot configuration * @param claudeArgs Arguments to pass to Claude CLI * @returns Exit code */ export async function executeCopilotProfile( config: CopilotConfig, claudeArgs: string[], claudeConfigDir?: string, claudeCliPath: string = 'claude' ): Promise { const { config: normalizedConfig, warnings } = normalizeCopilotConfigWithWarnings(config); if (warnings.length > 0) { warnings.forEach(({ message }) => console.log(warn(message))); console.log( warn('Run `ccs config` and save the Copilot section to persist these replacements.') ); console.log(''); } // Ensure copilot-api is installed (auto-install if missing, auto-update if outdated) try { await ensureCopilotApi(); } catch (error) { console.error(fail('Failed to install copilot-api.')); console.error(''); console.error(`Error: ${(error as Error).message}`); console.error(''); console.error('Try installing manually:'); console.error(' npm install -g copilot-api'); return 1; } // Check if copilot-api is installed (should be after ensureCopilotApi) if (!isCopilotApiInstalled()) { console.error(fail('copilot-api is not installed.')); console.error(''); console.error('Install/repair by running: ccs copilot start'); return 1; } // Check authentication const authStatus = await checkAuthStatus(); if (!authStatus.authenticated) { console.error(fail('Not authenticated with GitHub.')); console.error(''); console.error('Run: npx copilot-api auth'); console.error('Or: ccs copilot auth'); return 1; } // Check if daemon is running or needs to be started let daemonRunning = await isDaemonRunning(normalizedConfig.port); if (!daemonRunning) { if (normalizedConfig.auto_start) { console.log(info('Starting copilot-api daemon...')); const result = await startDaemon(normalizedConfig); if (!result.success) { console.error(fail(`Failed to start daemon: ${result.error}`)); return 1; } console.log(ok(`Daemon started on port ${normalizedConfig.port}`)); daemonRunning = true; } else { console.error(fail('copilot-api daemon is not running.')); console.error(''); console.error('Start the daemon:'); console.error(' ccs copilot start'); console.error('Fallback manual command:'); console.error(` npx copilot-api start --port ${normalizedConfig.port}`); console.error(''); console.error('Or enable auto_start in config:'); console.error(' ccs config (then enable auto_start in Copilot section)'); return 1; } } // Generate environment for Claude const copilotEnv = generateCopilotEnv(normalizedConfig, claudeConfigDir); // Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles const globalEnvConfig = getGlobalEnvConfig(); const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {}; // Merge with current environment (global env first, copilot overrides, then hook env vars) const webSearchEnv = getWebSearchHookEnv(); const imageAnalysisEnv = getImageAnalysisHookEnv('copilot'); const env = stripClaudeCodeEnv({ ...process.env, ...globalEnv, ...copilotEnv, ...webSearchEnv, ...imageAnalysisEnv, CCS_PROFILE_TYPE: 'copilot', }); console.log(info(`Using GitHub Copilot proxy (model: ${normalizedConfig.model})`)); console.log(''); syncWebSearchMcpToConfigDir(claudeConfigDir); // Spawn Claude CLI return new Promise((resolve) => { const launchArgs = appendThirdPartyWebSearchToolArgs(claudeArgs); const traceEnv = createWebSearchTraceContext({ launcher: 'copilot.executor', args: launchArgs, profile: 'copilot', profileType: 'copilot', claudeConfigDir, }); const proc = spawn(claudeCliPath, launchArgs, { stdio: 'inherit', env: { ...env, ...traceEnv }, shell: process.platform === 'win32', }); proc.on('close', (code) => { resolve(code ?? 0); }); proc.on('error', (err) => { console.error(fail(`Failed to start Claude: ${err.message}`)); resolve(1); }); }); }