mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 02:14:32 +00:00
Phases 01+02 of #1162. Splits executor flag parsing/validation out of the 1428-LOC orchestrator into a focused module: New files: - src/cliproxy/executor/arg-parser.ts (~500 LOC): readOptionValue, hasGitLabTokenLoginFlag, getGitLabTokenLoginFlagName, CCS_FLAGS + filterCcsFlags, ParsedExecutorFlags + parseExecutorFlags, validateFlagCombinations (process.exit semantics preserved for parity). - src/cliproxy/executor/__tests__/arg-parser.test.ts: 45 unit tests. - src/cliproxy/executor/__tests__/index-characterization.test.ts: TDD baseline (16 pass + 7 skipped scenarios at the spawn/dynamic-import boundary; mock simplification deferred to a follow-up phase). index.ts: 1428 -> 1175 LOC (-253). Re-exports preserved at module bottom for backwards compatibility. Behavior unchanged; full test suite passes (1824/1824). Refs #1162
1169 lines
40 KiB
TypeScript
1169 lines
40 KiB
TypeScript
/**
|
|
* 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, getWindowsEscapedCommandShell } from '../../utils/shell-executor';
|
|
import {
|
|
ensureCLIProxyBinary,
|
|
getConfiguredBackend,
|
|
getPlusBackendUnavailableMessage,
|
|
} from '../binary-manager';
|
|
import {
|
|
generateConfig,
|
|
getProviderConfig,
|
|
ensureProviderSettings,
|
|
getProviderSettingsPath,
|
|
CLIPROXY_DEFAULT_PORT,
|
|
validatePort,
|
|
} from '../config/config-generator';
|
|
import { checkRemoteProxy } from '../services/remote-proxy-client';
|
|
import { isAuthenticated } from '../auth/auth-handler';
|
|
import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS, ExecutorConfig } from '../types';
|
|
import { configureProviderModel, getCurrentModel } from '../config/model-config';
|
|
import { reconcileCodexModelForActivePlan } from '../ai-providers/codex-plan-compatibility';
|
|
import { resolveProxyConfig } from '../proxy/proxy-config-resolver';
|
|
import {
|
|
supportsModelConfig,
|
|
isModelBroken,
|
|
getModelIssueUrl,
|
|
findModel,
|
|
getSuggestedReplacementModel,
|
|
} from '../model-catalog';
|
|
import { CodexReasoningProxy } from '../ai-providers/codex-reasoning-proxy';
|
|
import { ToolSanitizationProxy } from '../proxy/tool-sanitization-proxy';
|
|
import {
|
|
findAccountByQuery,
|
|
getProviderAccounts,
|
|
setDefaultAccount,
|
|
touchAccount,
|
|
renameAccount,
|
|
getDefaultAccount,
|
|
} from '../accounts/account-manager';
|
|
import { formatAccountDisplayName } from '../accounts/email-account-identity';
|
|
import {
|
|
ensureWebSearchMcpOrThrow,
|
|
displayWebSearchStatus,
|
|
appendThirdPartyWebSearchToolArgs,
|
|
createWebSearchTraceContext,
|
|
} from '../../utils/websearch-manager';
|
|
import {
|
|
ensureImageAnalysisMcpOrThrow,
|
|
syncImageAnalysisMcpToConfigDir,
|
|
appendThirdPartyImageAnalysisToolArgs,
|
|
} from '../../utils/image-analysis';
|
|
import {
|
|
appendBrowserToolArgs,
|
|
type BrowserLaunchOverride,
|
|
ensureBrowserMcpOrThrow,
|
|
getBlockedBrowserOverrideWarning,
|
|
getEffectiveClaudeBrowserAttachConfig,
|
|
resolveBrowserExposure,
|
|
resolveBrowserLaunchFlagResolution,
|
|
resolveOptionalBrowserAttachRuntime,
|
|
syncBrowserMcpToConfigDir,
|
|
} from '../../utils/browser';
|
|
import {
|
|
getBrowserConfig,
|
|
loadOrCreateUnifiedConfig,
|
|
getThinkingConfig,
|
|
} from '../../config/unified-config-loader';
|
|
import { HttpsTunnelProxy } from '../proxy/https-tunnel-proxy';
|
|
import { resolveProfileContinuityInheritance } from '../../auth/profile-continuity-inheritance';
|
|
|
|
// Import modular components
|
|
import { waitForProxyReadyWithSpinner, spawnProxy } from './lifecycle-manager';
|
|
import {
|
|
buildClaudeEnvironment,
|
|
logEnvironment,
|
|
resolveCliproxyImageAnalysisEnv,
|
|
} from './env-resolver';
|
|
import {
|
|
isNetworkError,
|
|
handleNetworkError,
|
|
handleTokenExpiration,
|
|
handleQuotaCheck,
|
|
} from './retry-handler';
|
|
import { MANAGED_QUOTA_PROVIDERS, type ManagedQuotaProvider } from '../quota/quota-manager';
|
|
import { checkOrJoinProxy, registerProxySession, setupCleanupHandlers } from './session-bridge';
|
|
import {
|
|
warnCrossProviderDuplicates,
|
|
warnOAuthBanRisk,
|
|
cleanupStaleAutoPauses,
|
|
enforceProviderIsolation,
|
|
restoreAutoPausedAccounts,
|
|
} from '../accounts/account-safety';
|
|
import {
|
|
ensureCliAntigravityResponsibility,
|
|
ANTIGRAVITY_ACCEPT_RISK_FLAGS,
|
|
} from '../auth/antigravity-responsibility';
|
|
import { getWebSearchHookEnv } from '../../utils/websearch-manager';
|
|
import {
|
|
buildThinkingStartupStatus,
|
|
resolveRuntimeThinkingOverride,
|
|
shouldDisableCodexReasoning,
|
|
} from './thinking-override-resolver';
|
|
import { shouldStartHttpsTunnel } from './https-tunnel-policy';
|
|
import { filterCcsFlags, parseExecutorFlags, validateFlagCombinations } from './arg-parser';
|
|
|
|
function resolveRuntimeQuotaMonitorProviders(
|
|
provider: CLIProxyProvider,
|
|
compositeProviders: CLIProxyProvider[]
|
|
): ManagedQuotaProvider[] {
|
|
const candidates = compositeProviders.length > 0 ? compositeProviders : [provider];
|
|
const resolved: ManagedQuotaProvider[] = [];
|
|
|
|
for (const candidate of candidates) {
|
|
if (
|
|
MANAGED_QUOTA_PROVIDERS.includes(candidate as ManagedQuotaProvider) &&
|
|
!resolved.includes(candidate as ManagedQuotaProvider)
|
|
) {
|
|
resolved.push(candidate as ManagedQuotaProvider);
|
|
}
|
|
}
|
|
|
|
return resolved;
|
|
}
|
|
|
|
/** Default executor configuration */
|
|
const DEFAULT_CONFIG: ExecutorConfig = {
|
|
port: CLIPROXY_DEFAULT_PORT,
|
|
timeout: 5000,
|
|
verbose: false,
|
|
pollInterval: 100,
|
|
};
|
|
|
|
// readOptionValue, hasGitLabTokenLoginFlag, CCS_FLAGS, filterCcsFlags are
|
|
// re-exported from ./arg-parser via the export block at the bottom of this file
|
|
// for backwards compatibility with external callers.
|
|
|
|
/**
|
|
* 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<ExecutorConfig> = {}
|
|
): Promise<void> {
|
|
// Filter out undefined values to prevent overwriting defaults
|
|
const filteredConfig = Object.fromEntries(
|
|
Object.entries(config).filter(([, v]) => v !== undefined)
|
|
) as Partial<ExecutorConfig>;
|
|
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();
|
|
|
|
// Collect all providers to validate (default + composite tiers)
|
|
const allProviders = [provider, ...compositeProviders];
|
|
|
|
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,
|
|
management_key: cliproxyServerConfig.remote.management_key,
|
|
timeout: cliproxyServerConfig.remote.timeout,
|
|
}
|
|
: undefined,
|
|
local: cliproxyServerConfig?.local
|
|
? {
|
|
port: cliproxyServerConfig.local.port,
|
|
auto_start: cliproxyServerConfig.local.auto_start,
|
|
}
|
|
: undefined,
|
|
});
|
|
let browserLaunchOverride: BrowserLaunchOverride | undefined;
|
|
let argsWithoutBrowserFlags = argsWithoutProxy;
|
|
try {
|
|
const browserLaunchFlags = resolveBrowserLaunchFlagResolution(argsWithoutProxy);
|
|
browserLaunchOverride = browserLaunchFlags.override;
|
|
argsWithoutBrowserFlags = browserLaunchFlags.argsWithoutFlags;
|
|
} catch (error) {
|
|
console.error(fail((error as Error).message));
|
|
process.exit(1);
|
|
return;
|
|
}
|
|
const browserConfig = getBrowserConfig();
|
|
const browserAttachConfig = getEffectiveClaudeBrowserAttachConfig(browserConfig);
|
|
const claudeBrowserExposure = resolveBrowserExposure(
|
|
{
|
|
enabled: browserAttachConfig.enabled,
|
|
policy: browserConfig.claude.policy,
|
|
},
|
|
browserLaunchOverride
|
|
);
|
|
const blockedBrowserOverrideWarning = getBlockedBrowserOverrideWarning(
|
|
'Claude Browser Attach',
|
|
claudeBrowserExposure
|
|
);
|
|
if (blockedBrowserOverrideWarning) {
|
|
console.error(warn(blockedBrowserOverrideWarning));
|
|
}
|
|
|
|
// 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 first-class CCS WebSearch runtime
|
|
ensureWebSearchMcpOrThrow();
|
|
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
|
|
const browserAttachRuntime =
|
|
browserAttachConfig.enabled && claudeBrowserExposure.exposeForLaunch
|
|
? await resolveOptionalBrowserAttachRuntime(browserAttachConfig)
|
|
: undefined;
|
|
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
|
|
if (browserAttachRuntime?.warning) {
|
|
process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`);
|
|
}
|
|
if (browserRuntimeEnv) {
|
|
ensureBrowserMcpOrThrow();
|
|
}
|
|
displayWebSearchStatus();
|
|
|
|
const providerConfig = getProviderConfig(provider);
|
|
log(`Provider: ${providerConfig.displayName}`);
|
|
warnOAuthBanRisk(provider);
|
|
|
|
// Check remote proxy if configured
|
|
let useRemoteProxy = false;
|
|
let localBackend: CLIProxyBackend = 'original';
|
|
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<string>((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');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!useRemoteProxy) {
|
|
localBackend = getConfiguredBackend({ notifyOnPlus: true });
|
|
|
|
for (const p of allProviders) {
|
|
if (localBackend === 'original' && PLUS_ONLY_PROVIDERS.includes(p as CLIProxyProvider)) {
|
|
console.error('');
|
|
console.error(fail(getPlusBackendUnavailableMessage(p)));
|
|
console.error('');
|
|
throw new Error(`Provider ${p} requires local CLIProxy Plus backend`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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, { skipAutoUpdate: true });
|
|
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. Parse all CCS executor flags (extracted to arg-parser.ts)
|
|
const parsedFlags = parseExecutorFlags(argsWithoutProxy, {
|
|
provider,
|
|
compositeProviders,
|
|
unifiedConfig,
|
|
});
|
|
if (process.exitCode === 1) return;
|
|
|
|
// Validate cross-flag combinations (exits with code 1 on violation)
|
|
validateFlagCombinations(parsedFlags, { provider, compositeProviders }, argsWithoutProxy);
|
|
if (process.exitCode === 1) return;
|
|
|
|
const {
|
|
forceAuth,
|
|
pasteCallback,
|
|
portForward,
|
|
forceHeadless,
|
|
forceLogout,
|
|
forceConfig,
|
|
addAccount,
|
|
showAccounts,
|
|
forceImport,
|
|
gitlabTokenLogin,
|
|
acceptAgyRisk,
|
|
noIncognito,
|
|
useAccount,
|
|
setNickname,
|
|
kiroAuthMethod,
|
|
kiroIDCStartUrl,
|
|
kiroIDCRegion,
|
|
kiroIDCFlow,
|
|
gitlabBaseUrl,
|
|
extendedContextOverride,
|
|
thinkingParse,
|
|
} = parsedFlags;
|
|
|
|
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.'
|
|
)
|
|
);
|
|
}
|
|
|
|
// 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)} ${formatAccountDisplayName(acct)}${defaultMark}`);
|
|
}
|
|
console.log(`\n Use "ccs ${provider} --use <nickname-or-id>" 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) {
|
|
const displayName = formatAccountDisplayName(acct);
|
|
const label = acct.nickname ? `${acct.nickname} (${displayName})` : displayName;
|
|
console.error(` - ${label}`);
|
|
}
|
|
}
|
|
process.exit(1);
|
|
}
|
|
setDefaultAccount(provider, account.id);
|
|
touchAccount(provider, account.id);
|
|
const switchedLabel = account.nickname
|
|
? `${account.nickname} (${formatAccountDisplayName(account)})`
|
|
: formatAccountDisplayName(account);
|
|
console.log(ok(`Switched to account: ${switchedLabel}`));
|
|
}
|
|
|
|
// 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/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/auth-handler');
|
|
const authSuccess = await triggerOAuth(provider, {
|
|
verbose,
|
|
import: true,
|
|
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
|
|
...(kiroIDCStartUrl ? { kiroIDCStartUrl } : {}),
|
|
...(kiroIDCRegion ? { kiroIDCRegion } : {}),
|
|
...(kiroIDCFlow ? { kiroIDCFlow } : {}),
|
|
...(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/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 } : {}),
|
|
...(kiroIDCStartUrl && p === 'kiro' ? { kiroIDCStartUrl } : {}),
|
|
...(kiroIDCRegion && p === 'kiro' ? { kiroIDCRegion } : {}),
|
|
...(kiroIDCFlow && p === 'kiro' ? { kiroIDCFlow } : {}),
|
|
...(gitlabTokenLogin && p === 'gitlab' ? { gitlabAuthMode: 'pat' as const } : {}),
|
|
...(gitlabBaseUrl && p === 'gitlab' ? { gitlabBaseUrl } : {}),
|
|
...(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/auth-handler');
|
|
const authSuccess = await triggerOAuth(provider, {
|
|
verbose,
|
|
add: addAccount,
|
|
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
|
|
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
|
|
...(kiroIDCStartUrl ? { kiroIDCStartUrl } : {}),
|
|
...(kiroIDCRegion ? { kiroIDCRegion } : {}),
|
|
...(kiroIDCFlow ? { kiroIDCFlow } : {}),
|
|
...(gitlabTokenLogin ? { gitlabAuthMode: 'pat' as const } : {}),
|
|
...(gitlabBaseUrl ? { gitlabBaseUrl } : {}),
|
|
...(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) {
|
|
for (const managedProvider of MANAGED_QUOTA_PROVIDERS) {
|
|
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);
|
|
const replacementModel = getSuggestedReplacementModel(provider, currentModel);
|
|
console.error('');
|
|
console.error(warn(`${modelEntry?.name || currentModel} has known issues with Claude Code`));
|
|
if (replacementModel) {
|
|
console.error(` Tool calls will fail. Use "${replacementModel}" instead.`);
|
|
} else {
|
|
console.error(' Tool calls will fail. Consider changing the model in config.yaml.');
|
|
}
|
|
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);
|
|
|
|
if (provider === 'codex' && !cfg.isComposite && !skipLocalAuth) {
|
|
await reconcileCodexModelForActivePlan({
|
|
currentModel: getCurrentModel(provider, cfg.customSettingsPath),
|
|
verbose,
|
|
});
|
|
}
|
|
|
|
// 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,
|
|
localBackend,
|
|
configPath
|
|
);
|
|
|
|
// Register session
|
|
if (proxy.pid) {
|
|
sessionId = registerProxySession(cfg.port, proxy.pid, localBackend, verbose);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 8. Setup HTTPS tunnel if needed
|
|
let httpsTunnel: HttpsTunnelProxy | null = null;
|
|
let tunnelPort: number | null = null;
|
|
|
|
const useHttpsTunnel = shouldStartHttpsTunnel({
|
|
provider,
|
|
useRemoteProxy,
|
|
protocol: proxyConfig.protocol,
|
|
host: proxyConfig.host,
|
|
isComposite: cfg.isComposite,
|
|
});
|
|
|
|
if (useHttpsTunnel && 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}`);
|
|
}
|
|
} else if (useRemoteProxy && proxyConfig.protocol === 'https' && provider === 'codex') {
|
|
log('HTTPS tunnel skipped for Codex; local proxy chain will connect to remote HTTPS directly');
|
|
}
|
|
|
|
const imageAnalysisProxyTarget =
|
|
useRemoteProxy && proxyConfig.host
|
|
? {
|
|
host: proxyConfig.host,
|
|
port: proxyConfig.port,
|
|
protocol: proxyConfig.protocol,
|
|
authToken: proxyConfig.authToken,
|
|
managementKey: proxyConfig.managementKey,
|
|
allowSelfSigned: proxyConfig.allowSelfSigned,
|
|
isRemote: true as const,
|
|
}
|
|
: {
|
|
host: '127.0.0.1',
|
|
port: cfg.port,
|
|
protocol: 'http' as const,
|
|
isRemote: false as const,
|
|
};
|
|
const imageAnalysisResolution = await resolveCliproxyImageAnalysisEnv({
|
|
profileName: cfg.profileName || provider,
|
|
provider,
|
|
profileSettingsPath: cfg.customSettingsPath,
|
|
isComposite: cfg.isComposite,
|
|
proxyTarget: imageAnalysisProxyTarget,
|
|
tunnelPort,
|
|
proxyReachable: true,
|
|
});
|
|
const imageAnalysisProvisioningFailed =
|
|
!imageAnalysisMcpReady && imageAnalysisResolution.env.CCS_IMAGE_ANALYSIS_ENABLED === '1';
|
|
const imageAnalysisEnv = {
|
|
...imageAnalysisResolution.env,
|
|
CCS_IMAGE_ANALYSIS_SKIP_HOOK: imageAnalysisMcpReady ? '1' : '0',
|
|
};
|
|
const imageAnalysisWarning = imageAnalysisProvisioningFailed
|
|
? 'ImageAnalysis MCP provisioning failed. This session will use compatibility fallback when available.'
|
|
: imageAnalysisResolution.warning;
|
|
|
|
// 9. Setup tool sanitization proxy
|
|
let toolSanitizationProxy: ToolSanitizationProxy | null = null;
|
|
let toolSanitizationPort: number | null = null;
|
|
let inheritedClaudeConfigDir = cfg.claudeConfigDir;
|
|
|
|
if (!inheritedClaudeConfigDir && cfg.profileName) {
|
|
const continuityInheritance = await resolveProfileContinuityInheritance({
|
|
profileName: cfg.profileName,
|
|
profileType: 'cliproxy',
|
|
target: 'claude',
|
|
});
|
|
inheritedClaudeConfigDir = continuityInheritance.claudeConfigDir;
|
|
if (continuityInheritance.sourceAccount && process.env.CCS_DEBUG) {
|
|
log(
|
|
`Continuity inheritance active: profile "${cfg.profileName}" -> account "${continuityInheritance.sourceAccount}"`
|
|
);
|
|
}
|
|
}
|
|
|
|
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
|
|
if (
|
|
browserRuntimeEnv &&
|
|
inheritedClaudeConfigDir &&
|
|
!syncBrowserMcpToConfigDir(inheritedClaudeConfigDir)
|
|
) {
|
|
throw new Error(
|
|
'Browser MCP is enabled, but CCS could not sync the browser MCP config into the inherited Claude instance.'
|
|
);
|
|
}
|
|
|
|
// 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,
|
|
claudeConfigDir: inheritedClaudeConfigDir,
|
|
imageAnalysisEnv,
|
|
});
|
|
|
|
if (initialEnvVars.ANTHROPIC_BASE_URL) {
|
|
try {
|
|
toolSanitizationProxy = new ToolSanitizationProxy({
|
|
upstreamBaseUrl: initialEnvVars.ANTHROPIC_BASE_URL,
|
|
verbose,
|
|
warnOnSanitize: true,
|
|
allowSelfSigned: useRemoteProxy ? (proxyConfig.allowSelfSigned ?? false) : false,
|
|
});
|
|
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') : '',
|
|
allowSelfSigned: useRemoteProxy ? (proxyConfig.allowSelfSigned ?? false) : false,
|
|
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,
|
|
claudeConfigDir: inheritedClaudeConfigDir,
|
|
imageAnalysisEnv,
|
|
browserRuntimeEnv,
|
|
});
|
|
|
|
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();
|
|
if (process.env.CCS_DEBUG) {
|
|
console.error(
|
|
`[cliproxy-browser-debug] keys=${Object.keys(env)
|
|
.filter((key) => key.startsWith('CCS_BROWSER_'))
|
|
.sort()
|
|
.join(',')} ws=${env.CCS_BROWSER_DEVTOOLS_WS_URL || ''}`
|
|
);
|
|
}
|
|
logEnvironment(env, webSearchEnv, verbose);
|
|
if (imageAnalysisWarning) {
|
|
console.error(info(imageAnalysisWarning));
|
|
}
|
|
|
|
// 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 claudeArgs = filterCcsFlags(argsWithoutBrowserFlags);
|
|
|
|
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;
|
|
const imageAnalysisArgs = imageAnalysisMcpReady
|
|
? appendThirdPartyImageAnalysisToolArgs(claudeArgs)
|
|
: claudeArgs;
|
|
const browserArgs = browserRuntimeEnv
|
|
? appendBrowserToolArgs(imageAnalysisArgs)
|
|
: imageAnalysisArgs;
|
|
const launchArgs = [
|
|
'--settings',
|
|
settingsPath,
|
|
...appendThirdPartyWebSearchToolArgs(browserArgs),
|
|
];
|
|
const traceEnv = createWebSearchTraceContext({
|
|
launcher: 'cliproxy.executor',
|
|
args: launchArgs,
|
|
profile: cfg.profileName || provider,
|
|
profileType: 'cliproxy',
|
|
settingsPath,
|
|
claudeConfigDir: inheritedClaudeConfigDir,
|
|
});
|
|
const tracedEnv = { ...env, ...traceEnv };
|
|
if (needsShell) {
|
|
const cmdString = [claudeCli, ...launchArgs].map(escapeShellArg).join(' ');
|
|
claude = spawn(cmdString, {
|
|
stdio: 'inherit',
|
|
windowsHide: true,
|
|
shell: getWindowsEscapedCommandShell(),
|
|
env: tracedEnv,
|
|
});
|
|
} else {
|
|
claude = spawn(claudeCli, launchArgs, {
|
|
stdio: 'inherit',
|
|
windowsHide: true,
|
|
env: tracedEnv,
|
|
});
|
|
}
|
|
|
|
// 12b. Start runtime quota monitor (adaptive polling during session)
|
|
if (!skipLocalAuth) {
|
|
const { startQuotaMonitor } = await import('../quota/quota-manager');
|
|
for (const monitorProvider of resolveRuntimeQuotaMonitorProviders(
|
|
provider,
|
|
compositeProviders
|
|
)) {
|
|
const monitorAccount = getDefaultAccount(monitorProvider);
|
|
if (monitorAccount) {
|
|
startQuotaMonitor(monitorProvider, 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';
|
|
|
|
// Re-export arg-parser helpers (previously inlined here; external callers can
|
|
// import from index or directly from ./arg-parser)
|
|
export { readOptionValue, hasGitLabTokenLoginFlag, CCS_FLAGS, filterCcsFlags } from './arg-parser';
|
|
|
|
export const __testExports = {
|
|
resolveRuntimeQuotaMonitorProviders,
|
|
};
|
|
|
|
export default execClaudeWithCLIProxy;
|