diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index c9ed7ba0..2f77ba4d 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -19,8 +19,9 @@ import { escapeShellArg } from '../utils/shell-executor'; import { ensureCLIProxyBinary } from './binary-manager'; import { generateConfig, - getClaudeEnvVars, + getEffectiveEnvVars, getProviderConfig, + ensureProviderSettings, CLIPROXY_DEFAULT_PORT, } from './config-generator'; import { isAuthenticated } from './auth-handler'; @@ -154,12 +155,15 @@ export async function execClaudeWithCLIProxy( } } - // 4. Generate config file + // 4. Ensure user settings file exists (creates from defaults if not) + ensureProviderSettings(provider); + + // 5. Generate config file log(`Generating config for ${provider}`); const configPath = generateConfig(provider, cfg.port); log(`Config written: ${configPath}`); - // 5. Spawn CLIProxyAPI binary + // 6. Spawn CLIProxyAPI binary const proxyArgs = ['--config', configPath]; log(`Spawning: ${binaryPath} ${proxyArgs.join(' ')}`); @@ -213,8 +217,9 @@ export async function execClaudeWithCLIProxy( throw new Error(`CLIProxy startup failed: ${err.message}`); } - // 6. Execute Claude CLI with proxied environment - const envVars = getClaudeEnvVars(provider, cfg.port); + // 7. Execute Claude CLI with proxied environment + // Uses user settings if ~/.ccs/{provider}.settings.json exists, else bundled defaults + const envVars = getEffectiveEnvVars(provider, cfg.port); const env = { ...process.env, ...envVars }; log(`Claude env: ANTHROPIC_BASE_URL=${envVars.ANTHROPIC_BASE_URL}`); @@ -244,7 +249,7 @@ export async function execClaudeWithCLIProxy( }); } - // 7. Cleanup: kill proxy when Claude exits + // 8. Cleanup: kill proxy when Claude exits claude.on('exit', (code, signal) => { log(`Claude exited: code=${code}, signal=${signal}`); proxy.kill('SIGTERM'); diff --git a/src/cliproxy/config-generator.ts b/src/cliproxy/config-generator.ts index c3aecf4f..d7a31d27 100644 --- a/src/cliproxy/config-generator.ts +++ b/src/cliproxy/config-generator.ts @@ -10,6 +10,11 @@ import * as path from 'path'; import { getCcsDir } from '../utils/config-manager'; import { CLIProxyProvider, ProviderConfig, ProviderModelMapping } from './types'; +/** Settings file structure for user overrides */ +interface ProviderSettings { + env: NodeJS.ProcessEnv; +} + /** Default CLIProxy port */ export const CLIPROXY_DEFAULT_PORT = 8317; @@ -193,7 +198,15 @@ export function deleteConfig(): void { } /** - * Get environment variables for Claude CLI + * Get path to user settings file for provider + * Example: ~/.ccs/gemini.settings.json + */ +export function getProviderSettingsPath(provider: CLIProxyProvider): string { + return path.join(getCcsDir(), `${provider}.settings.json`); +} + +/** + * Get environment variables for Claude CLI (bundled defaults) * Uses provider-specific endpoint (e.g., /api/provider/gemini) for explicit routing. * This enables concurrent gemini/codex usage - each session routes to its provider via URL path. */ @@ -213,3 +226,65 @@ export function getClaudeEnvVars( ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haikuModel || models.claudeModel, }; } + +/** + * Get effective environment variables for provider + * + * Priority order: + * 1. User settings file (~/.ccs/{provider}.settings.json) if exists + * 2. Bundled defaults from PROVIDER_CONFIGS + * + * This allows users to customize model mappings without code changes. + * User takes full responsibility for custom settings. + */ +export function getEffectiveEnvVars( + provider: CLIProxyProvider, + port: number = CLIPROXY_DEFAULT_PORT +): NodeJS.ProcessEnv { + const settingsPath = getProviderSettingsPath(provider); + + // Check for user override file + if (fs.existsSync(settingsPath)) { + try { + const content = fs.readFileSync(settingsPath, 'utf-8'); + const settings: ProviderSettings = JSON.parse(content); + + if (settings.env && typeof settings.env === 'object') { + // User override found - use their settings + // Note: User is responsible for correctness + return settings.env; + } + } catch { + // Invalid JSON or structure - fall through to defaults + // Silent fallback: don't spam errors for broken user files + } + } + + // No override or invalid - use bundled defaults + return getClaudeEnvVars(provider, port); +} + +/** + * Copy bundled settings template to user directory if not exists + * Called during installation/first run + */ +export function ensureProviderSettings(provider: CLIProxyProvider): void { + const settingsPath = getProviderSettingsPath(provider); + + // Only create if doesn't exist (preserve user edits) + if (fs.existsSync(settingsPath)) { + return; + } + + // Generate default settings from PROVIDER_CONFIGS + const envVars = getClaudeEnvVars(provider); + const settings: ProviderSettings = { env: envVars }; + + // Ensure directory exists + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); + + // Write with restricted permissions + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', { + mode: 0o600, + }); +} diff --git a/src/cliproxy/index.ts b/src/cliproxy/index.ts index 260b445b..339d3988 100644 --- a/src/cliproxy/index.ts +++ b/src/cliproxy/index.ts @@ -46,6 +46,9 @@ export { export { generateConfig, getClaudeEnvVars, + getEffectiveEnvVars, + getProviderSettingsPath, + ensureProviderSettings, getProviderConfig, getModelMapping, getCliproxyDir,