/** * CLIProxy Executor - Main Orchestrator * * Coordinates the full execution flow: * 1. Configuration resolution and validation * 2. Binary management and remote proxy checks * 3. Authentication and account management * 4. Proxy lifecycle (spawn/detect/join) * 5. Environment setup and proxy chains * 6. Claude CLI execution with cleanup handlers */ import { spawn, ChildProcess } from 'child_process'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { ProgressIndicator } from '../../utils/progress-indicator'; import { ok, fail, info, warn } from '../../utils/ui'; import { getCcsDir } from '../../utils/config-manager'; import { escapeShellArg } from '../../utils/shell-executor'; import { ensureCLIProxyBinary } from '../binary-manager'; import { generateConfig, getProviderConfig, ensureProviderSettings, getProviderSettingsPath, CLIPROXY_DEFAULT_PORT, validatePort, } from '../config-generator'; import { checkRemoteProxy } from '../remote-proxy-client'; import { isAuthenticated } from '../auth-handler'; import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS, ExecutorConfig } from '../types'; import { DEFAULT_BACKEND } from '../platform-detector'; import { configureProviderModel, getCurrentModel } from '../model-config'; import { resolveProxyConfig, PROXY_CLI_FLAGS } from '../proxy-config-resolver'; import { supportsModelConfig, isModelBroken, getModelIssueUrl, findModel } from '../model-catalog'; import { CodexReasoningProxy } from '../codex-reasoning-proxy'; import { ToolSanitizationProxy } from '../tool-sanitization-proxy'; import { findAccountByQuery, getProviderAccounts, setDefaultAccount, touchAccount, renameAccount, getDefaultAccount, } from '../account-manager'; import { ensureMcpWebSearch, installWebSearchHook, displayWebSearchStatus, } from '../../utils/websearch-manager'; import { loadOrCreateUnifiedConfig, getThinkingConfig } from '../../config/unified-config-loader'; import { installImageAnalyzerHook } from '../../utils/hooks'; import { HttpsTunnelProxy } from '../https-tunnel-proxy'; import { isKiroAuthMethod, KiroAuthMethod, normalizeKiroAuthMethod } from '../auth/auth-types'; // Import modular components import { waitForProxyReadyWithSpinner, spawnProxy } from './lifecycle-manager'; import { buildClaudeEnvironment, logEnvironment } from './env-resolver'; import { isNetworkError, handleNetworkError, handleTokenExpiration, handleQuotaCheck, } from './retry-handler'; import { checkOrJoinProxy, registerProxySession, setupCleanupHandlers } from './session-bridge'; import { parseThinkingOverride } from './thinking-arg-parser'; import { warnCrossProviderDuplicates, warnOAuthBanRisk, cleanupStaleAutoPauses, enforceProviderIsolation, restoreAutoPausedAccounts, } from '../account-safety'; import { ensureCliAntigravityResponsibility, hasAntigravityRiskAcceptanceFlag, ANTIGRAVITY_ACCEPT_RISK_FLAGS, } from '../antigravity-responsibility'; import { getWebSearchHookEnv } from '../../utils/websearch-manager'; import { buildThinkingStartupStatus, resolveRuntimeThinkingOverride, shouldDisableCodexReasoning, } from './thinking-override-resolver'; /** Default executor configuration */ const DEFAULT_CONFIG: ExecutorConfig = { port: CLIPROXY_DEFAULT_PORT, timeout: 5000, verbose: false, pollInterval: 100, }; /** * Execute Claude CLI with CLIProxy (main entry point) * * @param claudeCli Path to Claude CLI executable * @param provider CLIProxy provider (gemini, codex, agy, qwen) * @param args Arguments to pass to Claude CLI * @param config Optional executor configuration */ export async function execClaudeWithCLIProxy( claudeCli: string, provider: CLIProxyProvider, args: string[], config: Partial = {} ): Promise { // Filter out undefined values to prevent overwriting defaults const filteredConfig = Object.fromEntries( Object.entries(config).filter(([, v]) => v !== undefined) ) as Partial; const cfg = { ...DEFAULT_CONFIG, ...filteredConfig }; const verbose = cfg.verbose || args.includes('--verbose') || args.includes('-v'); // Validate Claude CLI exists before proceeding if (!fs.existsSync(claudeCli)) { console.error(fail(`Claude CLI not found at: ${claudeCli}`)); console.error(' Run "ccs doctor --fix" to reinstall or check your PATH'); process.exit(1); } const log = (msg: string) => { if (verbose) { console.error(`[cliproxy] ${msg}`); } }; // Helper: Extract unique providers from composite tiers const compositeProviders = cfg.isComposite && cfg.compositeTiers ? [...new Set(Object.values(cfg.compositeTiers).map((t) => t.provider))] : []; // 0. Resolve proxy configuration (CLI > ENV > config.yaml > defaults) const unifiedConfig = loadOrCreateUnifiedConfig(); // 0a. Runtime backend/provider validation const backend: CLIProxyBackend = unifiedConfig.cliproxy?.backend ?? DEFAULT_BACKEND; // Collect all providers to validate (default + composite tiers) const allProviders = [provider, ...compositeProviders]; for (const p of allProviders) { if (backend === 'original' && PLUS_ONLY_PROVIDERS.includes(p as CLIProxyProvider)) { console.error(''); console.error(fail(`${p} requires CLIProxyAPIPlus backend`)); console.error(''); console.error('To use this provider, either:'); console.error(' 1. Set `cliproxy.backend: plus` in ~/.ccs/config.yaml'); console.error(' 2. Use --backend=plus flag: ccs ' + p + ' --backend=plus'); console.error(''); throw new Error(`Provider ${p} requires Plus backend`); } } const cliproxyServerConfig = unifiedConfig.cliproxy_server; const { config: proxyConfig, remainingArgs: argsWithoutProxy } = resolveProxyConfig(args, { remote: cliproxyServerConfig?.remote ? { enabled: cliproxyServerConfig.remote.enabled, host: cliproxyServerConfig.remote.host, port: cliproxyServerConfig.remote.port, protocol: cliproxyServerConfig.remote.protocol, auth_token: cliproxyServerConfig.remote.auth_token, timeout: cliproxyServerConfig.remote.timeout, } : undefined, local: cliproxyServerConfig?.local ? { port: cliproxyServerConfig.local.port, auto_start: cliproxyServerConfig.local.auto_start, } : undefined, }); // Port resolution and validation if (cfg.port && cfg.port !== CLIPROXY_DEFAULT_PORT) { if (proxyConfig.port !== CLIPROXY_DEFAULT_PORT) { cfg.port = proxyConfig.port; } } else if (proxyConfig.port !== CLIPROXY_DEFAULT_PORT) { cfg.port = proxyConfig.port; } cfg.port = validatePort(cfg.port); log(`Proxy mode: ${proxyConfig.mode}`); if (proxyConfig.mode === 'remote') { log(`Remote host: ${proxyConfig.host}:${proxyConfig.port} (${proxyConfig.protocol})`); } // Setup WebSearch hooks ensureMcpWebSearch(); installWebSearchHook(); displayWebSearchStatus(); // Sync image analyzer hook from npm package to ~/.ccs/hooks/ installImageAnalyzerHook(); const providerConfig = getProviderConfig(provider); log(`Provider: ${providerConfig.displayName}`); warnOAuthBanRisk(provider); // Check remote proxy if configured let useRemoteProxy = false; if (proxyConfig.mode === 'remote' && proxyConfig.host) { const status = await checkRemoteProxy({ host: proxyConfig.host, port: proxyConfig.port, protocol: proxyConfig.protocol, authToken: proxyConfig.authToken, timeout: proxyConfig.timeout ?? 2000, allowSelfSigned: proxyConfig.allowSelfSigned ?? false, }); if (status.reachable) { useRemoteProxy = true; console.log( ok( `Connected to remote proxy at ${proxyConfig.host}:${proxyConfig.port} (${status.latencyMs}ms)` ) ); } else { console.error(warn(`Remote proxy unreachable: ${status.error}`)); if (proxyConfig.remoteOnly) { throw new Error('Remote proxy unreachable and --remote-only specified'); } if (proxyConfig.fallbackEnabled) { if (proxyConfig.autoStartLocal) { console.log(info('Falling back to local proxy...')); } else { if (process.stdin.isTTY) { const readline = await import('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise((resolve) => { rl.question('Start local proxy instead? [Y/n] ', resolve); }); rl.close(); if (answer.toLowerCase() === 'n') { throw new Error('Remote proxy unreachable and user declined fallback'); } } console.log(info('Starting local proxy...')); } } else { throw new Error('Remote proxy unreachable and fallback disabled'); } } } // Variables for local proxy mode let binaryPath: string | undefined; let sessionId: string | undefined; // 1. Ensure binary exists (downloads if needed) - SKIP for remote mode if (!useRemoteProxy) { const spinner = new ProgressIndicator('Preparing CLIProxy'); spinner.start(); try { binaryPath = await ensureCLIProxyBinary(verbose); spinner.succeed('CLIProxy binary ready'); } catch (error) { spinner.fail('Failed to prepare CLIProxy'); const err = error as Error; if (isNetworkError(err)) { handleNetworkError(err); } throw error; } } // 2. Handle special flags (simplified flag parsing - full implementation continues below) const forceAuth = argsWithoutProxy.includes('--auth'); const pasteCallback = argsWithoutProxy.includes('--paste-callback'); const portForward = argsWithoutProxy.includes('--port-forward'); const forceHeadless = argsWithoutProxy.includes('--headless'); if (pasteCallback && portForward) { console.error(fail('Cannot use --paste-callback with --port-forward')); console.error(' --paste-callback: Manually paste OAuth redirect URL'); console.error(' --port-forward: Use SSH port forwarding for callback'); process.exit(1); } const forceLogout = argsWithoutProxy.includes('--logout'); const forceConfig = argsWithoutProxy.includes('--config'); const addAccount = argsWithoutProxy.includes('--add'); const showAccounts = argsWithoutProxy.includes('--accounts'); const forceImport = argsWithoutProxy.includes('--import'); const acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(argsWithoutProxy); const incognitoFlag = argsWithoutProxy.includes('--incognito'); const noIncognitoFlag = argsWithoutProxy.includes('--no-incognito'); const kiroNoIncognitoConfig = provider === 'kiro' ? (unifiedConfig.cliproxy?.kiro_no_incognito ?? true) : false; const noIncognito = incognitoFlag ? false : noIncognitoFlag || kiroNoIncognitoConfig; // Parse --use flag let useAccount: string | undefined; const useIdx = argsWithoutProxy.indexOf('--use'); if ( useIdx !== -1 && argsWithoutProxy[useIdx + 1] && !argsWithoutProxy[useIdx + 1].startsWith('-') ) { useAccount = argsWithoutProxy[useIdx + 1]; } // Parse --nickname flag let setNickname: string | undefined; const nicknameIdx = argsWithoutProxy.indexOf('--nickname'); if ( nicknameIdx !== -1 && argsWithoutProxy[nicknameIdx + 1] && !argsWithoutProxy[nicknameIdx + 1].startsWith('-') ) { setNickname = argsWithoutProxy[nicknameIdx + 1]; } // Parse --kiro-auth-method flag let kiroAuthMethod: KiroAuthMethod | undefined; const kiroMethodIdx = argsWithoutProxy.indexOf('--kiro-auth-method'); if (kiroMethodIdx !== -1) { const rawMethod = argsWithoutProxy[kiroMethodIdx + 1]; if (!rawMethod || rawMethod.startsWith('-')) { console.error(fail('--kiro-auth-method requires a value')); console.error(' Supported values: aws, aws-authcode, google, github'); process.exitCode = 1; return; } const normalized = rawMethod.trim().toLowerCase(); if (!isKiroAuthMethod(normalized)) { console.error(fail(`Invalid --kiro-auth-method value: ${rawMethod}`)); console.error(' Supported values: aws, aws-authcode, google, github'); process.exitCode = 1; return; } kiroAuthMethod = normalizeKiroAuthMethod(normalized); } if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) { console.error(fail('--kiro-auth-method is only valid for ccs kiro')); process.exitCode = 1; return; } // Parse --thinking / --effort flags (aliases; first occurrence wins) const thinkingParse = parseThinkingOverride(argsWithoutProxy); if (thinkingParse.error) { const { flag } = thinkingParse.error; console.error(fail(`${flag} requires a value`)); if (provider === 'codex') { console.error(' Codex examples: --effort xhigh, --effort high, --effort medium'); console.error(' Alias: --thinking xhigh (same behavior)'); } else { console.error(' Examples: --thinking low, --thinking 8192, --thinking off'); console.error(' Levels: minimal, low, medium, high, xhigh, auto'); } process.exit(1); } const { thinkingOverride, thinkingSource } = resolveRuntimeThinkingOverride( thinkingParse.value, process.env.CCS_THINKING ); const thinkingCfg = getThinkingConfig(); if (thinkingParse.duplicateDisplays.length > 0) { console.warn( `[!] Multiple reasoning flags detected. Using first occurrence: ${thinkingParse.sourceDisplay}` ); } if (thinkingParse.sourceFlag === '--effort' && provider !== 'codex') { console.warn( warn( '`--effort` is primarily for codex. Continuing as alias of `--thinking` for compatibility.' ) ); } // Parse --1m / --no-1m flags for extended context (1M token window) let extendedContextOverride: boolean | undefined; const has1mFlag = argsWithoutProxy.includes('--1m') || argsWithoutProxy.some((arg) => arg.startsWith('--1m=')); const hasNo1mFlag = argsWithoutProxy.includes('--no-1m') || argsWithoutProxy.some((arg) => arg.startsWith('--no-1m=')); if (has1mFlag && hasNo1mFlag) { console.error(fail('Cannot use both --1m and --no-1m flags')); process.exit(1); } else if (has1mFlag) { extendedContextOverride = true; } else if (hasNo1mFlag) { extendedContextOverride = false; } // undefined = auto behavior (Gemini: on, others: off) // Handle --accounts if (showAccounts) { const accounts = getProviderAccounts(provider); if (accounts.length === 0) { console.log(info(`No accounts registered for ${providerConfig.displayName}`)); console.log(` Run "ccs ${provider} --auth" to add an account`); } else { console.log(`\n${providerConfig.displayName} Accounts:\n`); for (const acct of accounts) { const defaultMark = acct.isDefault ? ' (default)' : ''; const nickname = acct.nickname ? `[${acct.nickname}]` : ''; console.log(` ${nickname.padEnd(12)} ${acct.email || acct.id}${defaultMark}`); } console.log(`\n Use "ccs ${provider} --use " to switch accounts`); } process.exit(0); } // Handle --use if (useAccount) { const account = findAccountByQuery(provider, useAccount); if (!account) { console.error(fail(`Account not found: "${useAccount}"`)); const accounts = getProviderAccounts(provider); if (accounts.length > 0) { console.error(` Available accounts:`); for (const acct of accounts) { console.error(` - ${acct.nickname || acct.id} (${acct.email || 'no email'})`); } } process.exit(1); } setDefaultAccount(provider, account.id); touchAccount(provider, account.id); console.log(ok(`Switched to account: ${account.nickname || account.email || account.id}`)); } // Handle --nickname (rename account) if (setNickname && !addAccount) { const defaultAccount = getDefaultAccount(provider); if (!defaultAccount) { console.error(fail(`No account found for ${providerConfig.displayName}`)); console.error(` Run "ccs ${provider} --auth" to add an account first`); process.exit(1); } try { const success = renameAccount(provider, defaultAccount.id, setNickname); if (success) { console.log(ok(`Renamed account to: ${setNickname}`)); } else { console.error(fail('Failed to rename account')); process.exit(1); } } catch (err) { console.error(fail(err instanceof Error ? err.message : 'Failed to rename account')); process.exit(1); } process.exit(0); } // Handle --config if (forceConfig && supportsModelConfig(provider)) { // Block --config for composite variants (per-tier models in config.yaml) if (cfg.isComposite) { const variantName = cfg.profileName || provider; console.log( warn('Composite variants use per-tier config. Edit config.yaml to change tier models.') ); console.error(` Use "ccs cliproxy edit ${variantName}" to modify composite variants`); process.exit(1); } else { await configureProviderModel(provider, true, cfg.customSettingsPath); process.exit(0); } } // Handle --logout if (forceLogout) { const { clearAuth } = await import('../auth-handler'); if (clearAuth(provider)) { console.log(ok(`Logged out from ${providerConfig.displayName}`)); } else { console.log(info(`No authentication found for ${providerConfig.displayName}`)); } process.exit(0); } // Handle --import (Kiro only) if (forceImport) { if (provider !== 'kiro') { console.error(fail('--import is only available for Kiro')); console.error(` Run "ccs ${provider} --auth" to authenticate`); process.exit(1); } if (forceAuth) { console.error(fail('Cannot use --import with --auth')); console.error(' --import: Import existing token from Kiro IDE'); console.error(' --auth: Trigger new OAuth flow in browser'); process.exit(1); } if (forceLogout) { console.error(fail('Cannot use --import with --logout')); process.exit(1); } const { triggerOAuth } = await import('../auth-handler'); const authSuccess = await triggerOAuth(provider, { verbose, import: true, ...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}), ...(setNickname ? { nickname: setNickname } : {}), }); if (!authSuccess) { console.error(fail('Failed to import Kiro token from IDE')); console.error(' Make sure you are logged into Kiro IDE first'); process.exit(1); } process.exit(0); } // 3. Ensure OAuth completed (if provider requires it) const remoteAuthToken = proxyConfig.authToken?.trim(); const skipLocalAuth = useRemoteProxy && !!remoteAuthToken; if (skipLocalAuth) { log(`Using remote proxy authentication (skipping local OAuth)`); } if (provider === 'agy' && forceAuth && skipLocalAuth) { const acknowledged = await ensureCliAntigravityResponsibility({ context: 'oauth', acceptedByFlag: acceptAgyRisk, }); if (!acknowledged) { throw new Error( `Antigravity auth blocked. Re-run after completing confirmation or pass ${ANTIGRAVITY_ACCEPT_RISK_FLAGS[0]}.` ); } console.error(info('Remote proxy mode is active; local OAuth flow is skipped in --auth mode.')); return; } if (provider === 'agy' && !forceAuth) { const requiresAuthNow = providerConfig.requiresOAuth && !isAuthenticated(provider); if (skipLocalAuth || !requiresAuthNow) { const acknowledged = await ensureCliAntigravityResponsibility({ context: 'run', acceptedByFlag: acceptAgyRisk, }); if (!acknowledged) { console.error( fail( `Antigravity session blocked. Re-run after completing confirmation or pass ${ANTIGRAVITY_ACCEPT_RISK_FLAGS[0]}.` ) ); process.exit(1); } } } if (providerConfig.requiresOAuth && !skipLocalAuth) { log(`Checking authentication for ${provider}`); // Multi-provider auth check for composite variants if (compositeProviders.length > 0) { // Handle forceAuth for composite providers if (forceAuth) { const { triggerOAuth } = await import('../auth-handler'); const failures: string[] = []; for (const p of compositeProviders) { const authSuccess = await triggerOAuth(p, { verbose, add: addAccount, ...(acceptAgyRisk ? { acceptAgyRisk: true } : {}), ...(kiroAuthMethod && p === 'kiro' ? { kiroMethod: kiroAuthMethod } : {}), ...(forceHeadless ? { headless: true } : {}), ...(setNickname ? { nickname: setNickname } : {}), ...(noIncognito ? { noIncognito: true } : {}), ...(pasteCallback ? { pasteCallback: true } : {}), ...(portForward ? { portForward: true } : {}), }); if (!authSuccess) { failures.push(p); } } if (failures.length > 0) { const succeeded = compositeProviders.filter((p) => !failures.includes(p)); console.error(fail(`Auth failed for: ${failures.join(', ')}`)); if (succeeded.length > 0) { console.error(info(`Succeeded: ${succeeded.join(', ')}`)); } process.exit(1); } process.exit(0); } // Check for unauthenticated providers const unauthenticatedProviders: string[] = []; for (const p of compositeProviders) { if (!isAuthenticated(p)) { unauthenticatedProviders.push(p); } } if (unauthenticatedProviders.length > 0) { console.error(fail('Composite variant requires authentication for multiple providers:')); for (const p of unauthenticatedProviders) { console.error(` - ${p} (run "ccs ${p} --auth")`); } process.exit(1); } } else if (forceAuth || !isAuthenticated(provider)) { const { triggerOAuth } = await import('../auth-handler'); const authSuccess = await triggerOAuth(provider, { verbose, add: addAccount, ...(acceptAgyRisk ? { acceptAgyRisk: true } : {}), ...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}), ...(forceHeadless ? { headless: true } : {}), ...(setNickname ? { nickname: setNickname } : {}), ...(noIncognito ? { noIncognito: true } : {}), ...(pasteCallback ? { pasteCallback: true } : {}), ...(portForward ? { portForward: true } : {}), }); if (!authSuccess) { throw new Error(`Authentication required for ${providerConfig.displayName}`); } if (forceAuth) { process.exit(0); } } else { log(`${provider} already authenticated`); } // 3a. Proactive token refresh (multi-provider for composite) if (compositeProviders.length > 0) { for (const p of compositeProviders) { await handleTokenExpiration(p, verbose); } } else { await handleTokenExpiration(provider, verbose); } // 3a-1. Update lastUsedAt const usedAccount = getDefaultAccount(provider); if (usedAccount) { touchAccount(provider, usedAccount.id); } } // 3b. Preflight quota check (providers with quota-based rotation) if (!skipLocalAuth) { // Multi-tier quota check for composite variants (check if any tier uses a managed provider) if (compositeProviders.length > 0) { const managedQuotaProviders = ['agy', 'claude'] as const; for (const managedProvider of managedQuotaProviders) { if (compositeProviders.includes(managedProvider)) { await handleQuotaCheck(managedProvider); } } } else { await handleQuotaCheck(provider); } } // 3c. Account safety: enforce cross-provider isolation if (!skipLocalAuth) { cleanupStaleAutoPauses(); const isolated = enforceProviderIsolation(provider); if (isolated === 0) { // No enforcement — still warn about duplicates for awareness warnCrossProviderDuplicates(provider); } else { // 'exit' handlers must be synchronous — restoreAutoPausedAccounts uses sync fs APIs process.on('exit', () => { restoreAutoPausedAccounts(provider); }); } } // 4. First-run model configuration if (!cfg.isComposite && supportsModelConfig(provider) && !skipLocalAuth) { await configureProviderModel(provider, false, cfg.customSettingsPath); } // 5. Check for broken models (multi-tier for composite) if (compositeProviders.length > 0 && cfg.compositeTiers) { // Check all tier models in composite variant const tiers: Array<'opus' | 'sonnet' | 'haiku'> = ['opus', 'sonnet', 'haiku']; for (const tier of tiers) { const tierConfig = cfg.compositeTiers[tier]; if (tierConfig && isModelBroken(tierConfig.provider, tierConfig.model)) { const modelEntry = findModel(tierConfig.provider, tierConfig.model); const issueUrl = getModelIssueUrl(tierConfig.provider, tierConfig.model); console.error(''); console.error( warn( `${tier} tier: ${modelEntry?.name || tierConfig.model} has known issues with Claude Code` ) ); console.error(' Tool calls will fail. Consider changing the model in config.yaml.'); if (issueUrl) { console.error(` Tracking: ${issueUrl}`); } console.error(''); } } } else { const currentModel = getCurrentModel(provider, cfg.customSettingsPath); if (currentModel && isModelBroken(provider, currentModel)) { const modelEntry = findModel(provider, currentModel); const issueUrl = getModelIssueUrl(provider, currentModel); console.error(''); console.error(warn(`${modelEntry?.name || currentModel} has known issues with Claude Code`)); console.error(' Tool calls will fail. Use "gemini-3-pro-preview" instead.'); if (issueUrl) { console.error(` Tracking: ${issueUrl}`); } if (skipLocalAuth) { console.error(' Note: Model may be overridden by remote proxy configuration.'); } else { console.error(` Run "ccs ${provider} --config" to change model.`); } console.error(''); } } // 6. Ensure user settings file exists ensureProviderSettings(provider); // Local proxy mode: generate config, spawn/join proxy, track session let proxy: ChildProcess | null = null; let configPath: string | undefined; if (!useRemoteProxy) { log(`Generating config for ${provider}`); configPath = generateConfig(provider, cfg.port); log(`Config written: ${configPath}`); // 6a. Check or join existing proxy const { sessionId: existingSessionId, shouldSpawn } = await checkOrJoinProxy( cfg.port, cfg.timeout, verbose ); sessionId = existingSessionId; // 6b. Spawn new proxy if needed if (shouldSpawn && binaryPath) { proxy = spawnProxy(binaryPath, configPath, verbose); // 7. Wait for proxy readiness await waitForProxyReadyWithSpinner( cfg.port, cfg.timeout, cfg.pollInterval, backend, configPath ); // Register session if (proxy.pid) { sessionId = registerProxySession(cfg.port, proxy.pid, backend, verbose); } } } // 8. Setup HTTPS tunnel if needed let httpsTunnel: HttpsTunnelProxy | null = null; let tunnelPort: number | null = null; if (useRemoteProxy && proxyConfig.protocol === 'https' && proxyConfig.host) { try { httpsTunnel = new HttpsTunnelProxy({ remoteHost: proxyConfig.host, remotePort: proxyConfig.port, authToken: proxyConfig.authToken, verbose, allowSelfSigned: proxyConfig.allowSelfSigned ?? false, }); tunnelPort = await httpsTunnel.start(); log( `HTTPS tunnel started on port ${tunnelPort} → https://${proxyConfig.host}:${proxyConfig.port}` ); } catch (error) { const err = error as Error; console.error(warn(`Failed to start HTTPS tunnel: ${err.message}`)); throw new Error(`HTTPS tunnel startup failed: ${err.message}`); } } // 9. Setup tool sanitization proxy let toolSanitizationProxy: ToolSanitizationProxy | null = null; let toolSanitizationPort: number | null = null; // Build initial env vars to get ANTHROPIC_BASE_URL const initialEnvVars = buildClaudeEnvironment({ provider, useRemoteProxy, remoteConfig: proxyConfig.host ? { host: proxyConfig.host, port: proxyConfig.port, protocol: proxyConfig.protocol, authToken: proxyConfig.authToken, } : undefined, httpsTunnel: httpsTunnel ?? undefined, tunnelPort: tunnelPort ?? undefined, localPort: cfg.port, customSettingsPath: cfg.customSettingsPath, thinkingOverride, extendedContextOverride, verbose, isComposite: cfg.isComposite, compositeTiers: cfg.compositeTiers, compositeDefaultTier: cfg.compositeDefaultTier, }); if (initialEnvVars.ANTHROPIC_BASE_URL) { try { toolSanitizationProxy = new ToolSanitizationProxy({ upstreamBaseUrl: initialEnvVars.ANTHROPIC_BASE_URL, verbose, warnOnSanitize: true, }); toolSanitizationPort = await toolSanitizationProxy.start(); log(`Tool sanitization proxy active on port ${toolSanitizationPort}`); } catch (error) { const err = error as Error; toolSanitizationProxy = null; toolSanitizationPort = null; if (verbose) { console.error(warn(`Tool sanitization proxy disabled: ${err.message}`)); } } } const postSanitizationBaseUrl = toolSanitizationPort ? `http://127.0.0.1:${toolSanitizationPort}` : initialEnvVars.ANTHROPIC_BASE_URL; // 10. Setup Codex reasoning proxy (single-provider Codex only) let codexReasoningProxy: CodexReasoningProxy | null = null; let codexReasoningPort: number | null = null; // Composite variants require root model-routed endpoints, never provider-pinned codex endpoints. if (provider === 'codex' && !cfg.isComposite) { if (!postSanitizationBaseUrl) { log('ANTHROPIC_BASE_URL not set for Codex, reasoning proxy disabled'); } else { try { const traceEnabled = process.env.CCS_CODEX_REASONING_TRACE === '1' || process.env.CCS_CODEX_REASONING_TRACE === 'true'; const stripPathPrefix = useRemoteProxy ? '/api/provider/codex' : undefined; const codexThinkingOff = shouldDisableCodexReasoning(thinkingCfg, thinkingOverride); codexReasoningProxy = new CodexReasoningProxy({ upstreamBaseUrl: postSanitizationBaseUrl, verbose, defaultEffort: 'medium', disableEffort: codexThinkingOff, traceFilePath: traceEnabled ? path.join(getCcsDir(), 'codex-reasoning-proxy.log') : '', modelMap: { defaultModel: initialEnvVars.ANTHROPIC_MODEL, opusModel: initialEnvVars.ANTHROPIC_DEFAULT_OPUS_MODEL, sonnetModel: initialEnvVars.ANTHROPIC_DEFAULT_SONNET_MODEL, haikuModel: initialEnvVars.ANTHROPIC_DEFAULT_HAIKU_MODEL, }, stripPathPrefix, }); codexReasoningPort = await codexReasoningProxy.start(); log( `Codex reasoning proxy active: http://127.0.0.1:${codexReasoningPort}/api/provider/codex` ); } catch (error) { const err = error as Error; codexReasoningProxy = null; codexReasoningPort = null; if (verbose) { console.error(warn(`Codex reasoning proxy disabled: ${err.message}`)); } } } } // 11. Build final environment with all proxy chains const env = buildClaudeEnvironment({ provider, useRemoteProxy, remoteConfig: proxyConfig.host ? { host: proxyConfig.host, port: proxyConfig.port, protocol: proxyConfig.protocol, authToken: proxyConfig.authToken, } : undefined, httpsTunnel: httpsTunnel ?? undefined, tunnelPort: tunnelPort ?? undefined, codexReasoningProxy: codexReasoningProxy ?? undefined, codexReasoningPort: codexReasoningPort ?? undefined, toolSanitizationProxy: toolSanitizationProxy ?? undefined, toolSanitizationPort: toolSanitizationPort ?? undefined, localPort: cfg.port, customSettingsPath: cfg.customSettingsPath, thinkingOverride, extendedContextOverride, verbose, isComposite: cfg.isComposite, compositeTiers: cfg.compositeTiers, compositeDefaultTier: cfg.compositeDefaultTier, }); if (cfg.isComposite && cfg.compositeTiers && cfg.compositeDefaultTier) { const mode = useRemoteProxy ? proxyConfig.protocol === 'https' ? 'remote-https' : 'remote-http' : 'local'; const defaultTierProvider = cfg.compositeTiers[cfg.compositeDefaultTier]?.provider ?? provider; log( `Composite self-check: mode=${mode}, baseUrl=${env.ANTHROPIC_BASE_URL || 'unset'}, defaultTier=${cfg.compositeDefaultTier}, defaultProvider=${defaultTierProvider}` ); } const webSearchEnv = getWebSearchHookEnv(); logEnvironment(env, webSearchEnv, verbose); // 11b. Print thinking status feedback (TTY only, non-piped sessions) if (process.stderr.isTTY) { const { thinkingLabel, sourceLabel } = buildThinkingStartupStatus( thinkingCfg, thinkingOverride, thinkingSource, thinkingParse.sourceDisplay ); console.error(`[i] Thinking: ${thinkingLabel} (${sourceLabel})`); } // 12. Filter CCS-specific flags before passing to Claude CLI const ccsFlags = [ '--auth', '--paste-callback', '--port-forward', '--headless', '--logout', '--config', '--add', '--accounts', '--use', '--nickname', '--kiro-auth-method', '--thinking', '--effort', '--1m', '--no-1m', '--incognito', '--no-incognito', '--import', '--accept-agr-risk', '--accept-antigravity-risk', '--settings', ...PROXY_CLI_FLAGS, ]; const claudeArgs = argsWithoutProxy.filter((arg, idx) => { if (ccsFlags.includes(arg)) return false; if (arg.startsWith('--thinking=')) return false; if (arg.startsWith('--effort=')) return false; if (arg.startsWith('--1m=') || arg.startsWith('--no-1m=')) return false; if ( argsWithoutProxy[idx - 1] === '--use' || argsWithoutProxy[idx - 1] === '--nickname' || argsWithoutProxy[idx - 1] === '--kiro-auth-method' || argsWithoutProxy[idx - 1] === '--thinking' || argsWithoutProxy[idx - 1] === '--effort' ) return false; return true; }); const isWindows = process.platform === 'win32'; const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli); const settingsPath = cfg.customSettingsPath ? cfg.customSettingsPath.replace(/^~/, os.homedir()) : getProviderSettingsPath(provider); let claude: ChildProcess; if (needsShell) { const cmdString = [claudeCli, '--settings', settingsPath, ...claudeArgs] .map(escapeShellArg) .join(' '); claude = spawn(cmdString, { stdio: 'inherit', windowsHide: true, shell: true, env, }); } else { claude = spawn(claudeCli, ['--settings', settingsPath, ...claudeArgs], { stdio: 'inherit', windowsHide: true, env, }); } // 12b. Start runtime quota monitor (adaptive polling during session) if (!skipLocalAuth) { const { startQuotaMonitor } = await import('../quota-manager'); const monitorAccount = getDefaultAccount(provider); if (monitorAccount) { startQuotaMonitor(provider, monitorAccount.id); } } // 13. Setup cleanup handlers setupCleanupHandlers( claude, sessionId, cfg.port, codexReasoningProxy, toolSanitizationProxy, httpsTunnel, verbose ); } // Re-export utility functions for backwards compatibility export { isPortAvailable, findAvailablePort } from './lifecycle-manager'; export default execClaudeWithCLIProxy;