From ce9d60d0ca7d7fef76006af2c90c6774bff44919 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sun, 30 Nov 2025 04:02:43 -0500 Subject: [PATCH] feat(cliproxy): add user-defined CLIProxy variants Allow users to create custom profile names that route through CLIProxy providers with different model configurations. Config example: { "cliproxy": { "flash": { "provider": "gemini", "settings": "~/.ccs/gemini-flash.settings.json" }, "pro": { "provider": "gemini", "settings": "~/.ccs/gemini-pro.settings.json" } } } Usage: ccs flash "quick task" or ccs pro "complex analysis" Changes: - Add CLIProxyVariantConfig types to src/types/config.ts - Update profile-detector to check config.cliproxy section (Priority 1) - Pass customSettingsPath through executor chain - Update getEffectiveEnvVars to support custom settings path - Document feature in README.md and CLAUDE.md - Bump version to 5.0.2 --- CLAUDE.md | 2 ++ README.md | 25 ++++++++++++++-- VERSION | 2 +- installers/install.ps1 | 2 +- installers/install.sh | 2 +- package.json | 2 +- src/auth/profile-detector.ts | 48 +++++++++++++++++++++++++++---- src/ccs.ts | 6 ++-- src/cliproxy/cliproxy-executor.ts | 4 +-- src/cliproxy/config-generator.ts | 30 +++++++++++++++++-- src/cliproxy/types.ts | 2 ++ src/types/config.ts | 23 +++++++++++++++ 12 files changed, 129 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8c0a61ac..d5288510 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,6 +68,8 @@ bun run test # Build + run all tests ### Profile Mechanisms +**CLIProxy**: gemini, codex, agy → OAuth-based, zero config +**CLIProxy Variants**: user-defined profiles in `config.cliproxy` section → custom settings for CLIProxy providers **Settings-based**: `--settings` flag → GLM, GLMT, Kimi, default **Account-based**: `CLAUDE_CONFIG_DIR` → isolated Claude Sub instances diff --git a/README.md b/README.md index 9c006549..9b36cf36 100644 --- a/README.md +++ b/README.md @@ -376,9 +376,10 @@ graph LR CCS resolves profiles in priority order: 1. **CLIProxy profiles** (gemini, codex, agy) - OAuth-based, zero config -2. **Settings-based profiles** (glm, kimi) - API key required -3. **Account-based profiles** (work, personal) - Isolated Claude instances -4. **Default** - Claude CLI with subscription +2. **CLIProxy variants** (user-defined) - Custom model settings for CLIProxy providers +3. **Settings-based profiles** (glm, kimi) - API key required +4. **Account-based profiles** (work, personal) - Isolated Claude instances +5. **Default** - Claude CLI with subscription ### Profile Types @@ -392,6 +393,24 @@ CCS resolves profiles in priority order: - **Codex**: gpt-5.1-codex-max (opus), gpt-4o (sonnet), gpt-4o-mini (haiku) - **Antigravity**: agy-pro (sonnet), agy-turbo (haiku) +**CLIProxy Variants** (v5.0.2+) +- User-defined profile names that route through CLIProxy providers +- Allows multiple model configurations per provider (e.g., `flash` and `pro` for Gemini) +- Configure in `~/.ccs/config.json` under `cliproxy` section: + +```json +{ + "profiles": { ... }, + "cliproxy": { + "flash": { "provider": "gemini", "settings": "~/.ccs/gemini-flash.settings.json" }, + "pro": { "provider": "gemini", "settings": "~/.ccs/gemini-pro.settings.json" }, + "gpt": { "provider": "codex", "settings": "~/.ccs/codex-gpt.settings.json" } + } +} +``` + +Usage: `ccs flash "quick task"` or `ccs pro "complex analysis"` + **Settings-based**: GLM, GLMT, Kimi, default - Uses `--settings` flag pointing to config files - GLMT: Embedded proxy for thinking mode support diff --git a/VERSION b/VERSION index 6b244dcd..a1ef0cae 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.0.1 +5.0.2 diff --git a/installers/install.ps1 b/installers/install.ps1 index f64a847e..46f2019c 100644 --- a/installers/install.ps1 +++ b/installers/install.ps1 @@ -83,7 +83,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or ( # IMPORTANT: Update this version when releasing new versions! # This hardcoded version is used for standalone installations (irm | iex) # For git installations, VERSION file is read if available -$CcsVersion = "5.0.1" +$CcsVersion = "5.0.2" # Try to read VERSION file for git installations if ($ScriptDir) { diff --git a/installers/install.sh b/installers/install.sh index cf7f94ea..8d4fc7a4 100755 --- a/installers/install.sh +++ b/installers/install.sh @@ -84,7 +84,7 @@ fi # IMPORTANT: Update this version when releasing new versions! # This hardcoded version is used for standalone installations (curl | bash) # For git installations, VERSION file is read if available -CCS_VERSION="5.0.1" +CCS_VERSION="5.0.2" # Try to read VERSION file for git installations if [[ -f "$SCRIPT_DIR/VERSION" ]]; then diff --git a/package.json b/package.json index 880e795c..059cd3d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "5.0.1", + "version": "5.0.2", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", diff --git a/src/auth/profile-detector.ts b/src/auth/profile-detector.ts index dee4fff3..70880391 100644 --- a/src/auth/profile-detector.ts +++ b/src/auth/profile-detector.ts @@ -23,6 +23,8 @@ export interface ProfileDetectionResult { settingsPath?: string; profile?: Settings | ProfileMetadata; message?: string; + /** For cliproxy variants: the underlying provider (gemini, codex, agy) */ + provider?: CLIProxyProfileName; } export interface AllProfiles { @@ -85,6 +87,12 @@ class ProfileDetector { /** * Detect profile type and return routing information + * + * Priority order: + * 0. Hardcoded CLIProxy profiles (gemini, codex, agy) + * 1. User-defined CLIProxy variants (config.cliproxy section) + * 2. Settings-based profiles (config.profiles section) + * 3. Account-based profiles (profiles.json) */ detectProfileType(profileName: string | null | undefined): ProfileDetectionResult { // Special case: 'default' means use default profile @@ -97,12 +105,24 @@ class ProfileDetector { return { type: 'cliproxy', name: profileName, + provider: profileName as CLIProxyProfileName, }; } - // Priority 1: Check settings-based profiles (glm, kimi) - BACKWARD COMPATIBILITY + // Priority 1: Check user-defined CLIProxy variants (config.cliproxy section) const config = this.readConfig(); + if (config.cliproxy && config.cliproxy[profileName]) { + const variant = config.cliproxy[profileName]; + return { + type: 'cliproxy', + name: profileName, + provider: variant.provider as CLIProxyProfileName, + settingsPath: variant.settings, + }; + } + + // Priority 2: Check settings-based profiles (glm, kimi) - BACKWARD COMPATIBILITY if (config.profiles && config.profiles[profileName]) { return { type: 'settings', @@ -111,7 +131,7 @@ class ProfileDetector { }; } - // Priority 2: Check account-based profiles (work, personal) + // Priority 3: Check account-based profiles (work, personal) const profiles = this.readProfiles(); if (profiles.profiles && profiles.profiles[profileName]) { @@ -124,7 +144,12 @@ class ProfileDetector { // Not found - generate suggestions const allProfiles = this.getAllProfiles(); - const allProfileNames = [...allProfiles.settings, ...allProfiles.accounts]; + const allProfileNames = [ + ...allProfiles.cliproxy, + ...allProfiles.cliproxyVariants, + ...allProfiles.settings, + ...allProfiles.accounts, + ]; const suggestions = findSimilarStrings(profileName, allProfileNames); const error = new Error(`Profile not found: ${profileName}`) as ProfileNotFoundError; @@ -180,8 +205,20 @@ class ProfileDetector { lines.push(` - ${name}`); }); - // Settings-based profiles + // CLIProxy variants (user-defined) const config = this.readConfig(); + const cliproxyVariants = Object.keys(config.cliproxy || {}); + + const cliproxyConfig = config.cliproxy; + if (cliproxyVariants.length > 0 && cliproxyConfig) { + lines.push('CLIProxy variants (user-defined):'); + cliproxyVariants.forEach((name) => { + const variant = cliproxyConfig[name]; + lines.push(` - ${name} (${variant.provider})`); + }); + } + + // Settings-based profiles const settingsProfiles = Object.keys(config.profiles || {}); if (settingsProfiles.length > 0) { @@ -221,7 +258,7 @@ class ProfileDetector { /** * Get all available profile names */ - getAllProfiles(): AllProfiles & { cliproxy: string[] } { + getAllProfiles(): AllProfiles & { cliproxy: string[]; cliproxyVariants: string[] } { const config = this.readConfig(); const profiles = this.readProfiles(); @@ -229,6 +266,7 @@ class ProfileDetector { settings: Object.keys(config.profiles || {}), accounts: Object.keys(profiles.profiles || {}), cliproxy: [...CLIPROXY_PROFILES], + cliproxyVariants: Object.keys(config.cliproxy || {}), default: profiles.default, }; } diff --git a/src/ccs.ts b/src/ccs.ts index 95b0f610..231e5741 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -295,8 +295,10 @@ async function main(): Promise { const profileInfo = detector.detectProfileType(profile); if (profileInfo.type === 'cliproxy') { - // CLIPROXY FLOW: OAuth-based profiles (gemini, codex, agy) - await execClaudeWithCLIProxy(claudeCli, profileInfo.name as CLIProxyProvider, remainingArgs); + // CLIPROXY FLOW: OAuth-based profiles (gemini, codex, agy) or user-defined variants + const provider = profileInfo.provider || (profileInfo.name as CLIProxyProvider); + const customSettingsPath = profileInfo.settingsPath; // undefined for hardcoded profiles + await execClaudeWithCLIProxy(claudeCli, provider, remainingArgs, { customSettingsPath }); } else if (profileInfo.type === 'settings') { // Check if this is GLMT profile (requires proxy) if (profileInfo.name === 'glmt') { diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index 986df860..d95b839a 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -218,8 +218,8 @@ export async function execClaudeWithCLIProxy( } // 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); + // Uses custom settings path (for variants), user settings, or bundled defaults + const envVars = getEffectiveEnvVars(provider, cfg.port, cfg.customSettingsPath); const env = { ...process.env, ...envVars }; log(`Claude env: ANTHROPIC_BASE_URL=${envVars.ANTHROPIC_BASE_URL}`); diff --git a/src/cliproxy/config-generator.ts b/src/cliproxy/config-generator.ts index 6f39b34e..e7e4ed64 100644 --- a/src/cliproxy/config-generator.ts +++ b/src/cliproxy/config-generator.ts @@ -211,16 +211,40 @@ export function getClaudeEnvVars( * Get effective environment variables for provider * * Priority order: - * 1. User settings file (~/.ccs/{provider}.settings.json) if exists - * 2. Bundled defaults from PROVIDER_CONFIGS + * 1. Custom settings path (for user-defined CLIProxy variants) + * 2. User settings file (~/.ccs/{provider}.settings.json) if exists + * 3. 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 + port: number = CLIPROXY_DEFAULT_PORT, + customSettingsPath?: string ): NodeJS.ProcessEnv { + // Priority 1: Custom settings path (for user-defined variants) + if (customSettingsPath) { + const expandedPath = customSettingsPath.replace(/^~/, require('os').homedir()); + if (fs.existsSync(expandedPath)) { + try { + const content = fs.readFileSync(expandedPath, 'utf-8'); + const settings: ProviderSettings = JSON.parse(content); + + if (settings.env && typeof settings.env === 'object') { + // Custom variant settings found - use them + return settings.env; + } + } catch { + // Invalid JSON - fall through to provider defaults + console.warn(`[!] Warning: Invalid settings file: ${customSettingsPath}`); + } + } else { + console.warn(`[!] Warning: Settings file not found: ${customSettingsPath}`); + } + } + + // Priority 2: Default provider settings file const settingsPath = getProviderSettingsPath(provider); // Check for user override file diff --git a/src/cliproxy/types.ts b/src/cliproxy/types.ts index 926ade98..d83abf2f 100644 --- a/src/cliproxy/types.ts +++ b/src/cliproxy/types.ts @@ -150,6 +150,8 @@ export interface ExecutorConfig { verbose: boolean; /** Poll interval for port check in ms (default: 100) */ pollInterval: number; + /** Custom settings path for user-defined CLIProxy variants */ + customSettingsPath?: string; } /** diff --git a/src/types/config.ts b/src/types/config.ts index fe6a3282..5b0689c6 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -11,12 +11,35 @@ export interface ProfilesConfig { [profileName: string]: string; // Path to settings.json } +/** + * CLIProxy variant configuration + * Allows user-defined CLIProxy profile variants with custom settings + * Example: "flash" → gemini provider with gemini-2.5-flash model + */ +export interface CLIProxyVariantConfig { + /** CLIProxy provider to use (gemini, codex, agy) */ + provider: 'gemini' | 'codex' | 'agy'; + /** Path to settings.json with custom model configuration */ + settings: string; +} + +/** + * CLIProxy variants section in config.json + * Maps custom profile names to CLIProxy provider + settings + */ +export interface CLIProxyVariantsConfig { + [profileName: string]: CLIProxyVariantConfig; +} + /** * Main CCS configuration * Located at: ~/.ccs/config.json */ export interface Config { + /** Settings-based profiles (GLM, Kimi, etc.) */ profiles: ProfilesConfig; + /** User-defined CLIProxy profile variants (optional) */ + cliproxy?: CLIProxyVariantsConfig; } /**