From 1f5d11930ee19c0f00b46d7994ea99c7be8e55c6 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Mon, 26 Jan 2026 16:19:35 +0900 Subject: [PATCH 01/21] feat: skip local OAuth when using remote proxy with auth token When --proxy-host and --proxy-auth-token are provided, the remote proxy handles authentication via its own OAuth sessions. This change skips: - Local OAuth check/trigger - Preflight quota check (managed by remote server) - Model configuration prompts (configured on remote server) - Broken model warnings (model selection is remote) This enables headless CI/CD usage without requiring pre-cached CCS_SESSIONS when a remote proxy with auth token is available. --- src/cliproxy/cliproxy-executor.ts | 40 ++-- tests/unit/cliproxy/skip-local-auth.test.ts | 213 ++++++++++++++++++++ 2 files changed, 239 insertions(+), 14 deletions(-) create mode 100644 tests/unit/cliproxy/skip-local-auth.test.ts diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index bea75a25..c74453f5 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -498,7 +498,14 @@ export async function execClaudeWithCLIProxy( } // 3. Ensure OAuth completed (if provider requires it) - if (providerConfig.requiresOAuth) { + // Skip local OAuth check when using remote proxy with auth token + // The remote proxy has its own OAuth sessions and handles authentication + const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + if (skipLocalAuth) { + log(`Using remote proxy authentication (skipping local OAuth)`); + } + + if (providerConfig.requiresOAuth && !skipLocalAuth) { log(`Checking authentication for ${provider}`); if (forceAuth || !isAuthenticated(provider)) { @@ -549,7 +556,8 @@ export async function execClaudeWithCLIProxy( // 3b. Preflight quota check - auto-switch to account with quota before launch // Uses quota-manager for caching, tier priority, and cooldown support - if (provider === 'agy') { + // Skip for remote proxy - quota is managed on the remote server + if (provider === 'agy' && !skipLocalAuth) { const preflight = await preflightCheck(provider); if (!preflight.proceed) { @@ -571,23 +579,27 @@ export async function execClaudeWithCLIProxy( // 4. First-run model configuration (interactive) // For supported providers, prompt user to select model on first run // Pass customSettingsPath for CLIProxy variants - if (supportsModelConfig(provider)) { + // Skip for remote proxy - model is configured on the remote server + if (supportsModelConfig(provider) && !skipLocalAuth) { await configureProviderModel(provider, false, cfg.customSettingsPath); // false = only if not configured } // 5. Check for known broken models and warn user - 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}`); + // Skip for remote proxy - model selection is on the remote server + if (!skipLocalAuth) { + 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}`); + } + console.error(` Run "ccs ${provider} --config" to change model.`); + console.error(''); } - console.error(` Run "ccs ${provider} --config" to change model.`); - console.error(''); } // 6. Ensure user settings file exists (creates from defaults if not) diff --git a/tests/unit/cliproxy/skip-local-auth.test.ts b/tests/unit/cliproxy/skip-local-auth.test.ts new file mode 100644 index 00000000..e840e032 --- /dev/null +++ b/tests/unit/cliproxy/skip-local-auth.test.ts @@ -0,0 +1,213 @@ +/** + * Unit tests for skip-local-auth functionality when using remote proxy with auth token + * + * When --proxy-host and --proxy-auth-token are provided together, the system should + * skip local OAuth checks because the remote proxy handles authentication. + */ +import { describe, it, expect } from 'bun:test'; + +describe('skip-local-auth logic', () => { + describe('skipLocalAuth flag determination', () => { + it('should skip local auth when both useRemoteProxy and authToken are truthy', () => { + const useRemoteProxy = true; + const proxyConfig = { authToken: 'test-token-123' }; + + const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + + expect(skipLocalAuth).toBeTruthy(); + }); + + it('should NOT skip local auth when useRemoteProxy is false', () => { + const useRemoteProxy = false; + const proxyConfig = { authToken: 'test-token-123' }; + + const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + + expect(skipLocalAuth).toBeFalsy(); + }); + + it('should NOT skip local auth when authToken is undefined', () => { + const useRemoteProxy = true; + const proxyConfig = { authToken: undefined }; + + const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + + expect(skipLocalAuth).toBeFalsy(); + }); + + it('should NOT skip local auth when authToken is empty string', () => { + const useRemoteProxy = true; + const proxyConfig = { authToken: '' }; + + const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + + expect(skipLocalAuth).toBeFalsy(); + }); + + it('should NOT skip local auth when both are falsy', () => { + const useRemoteProxy = false; + const proxyConfig = { authToken: undefined }; + + const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + + expect(skipLocalAuth).toBeFalsy(); + }); + }); + + describe('OAuth check bypass scenarios', () => { + it('should document that OAuth is skipped for remote proxy with auth', () => { + // This test documents the expected behavior: + // When using remote proxy with auth token, the remote server + // already has its own OAuth sessions, so local OAuth is unnecessary + const scenario = { + useRemoteProxy: true, + authToken: 'bearer-token', + providerRequiresOAuth: true, + }; + + const skipLocalAuth = scenario.useRemoteProxy && scenario.authToken; + const shouldTriggerLocalOAuth = scenario.providerRequiresOAuth && !skipLocalAuth; + + expect(skipLocalAuth).toBeTruthy(); + expect(shouldTriggerLocalOAuth).toBe(false); + }); + + it('should document that OAuth runs when no remote proxy', () => { + const scenario = { + useRemoteProxy: false, + authToken: undefined, + providerRequiresOAuth: true, + }; + + const skipLocalAuth = scenario.useRemoteProxy && scenario.authToken; + const shouldTriggerLocalOAuth = scenario.providerRequiresOAuth && !skipLocalAuth; + + expect(skipLocalAuth).toBeFalsy(); + expect(shouldTriggerLocalOAuth).toBe(true); + }); + + it('should document that OAuth runs when remote proxy has no auth token', () => { + // Edge case: remote proxy configured but no auth token + // This should fall back to local OAuth + const scenario = { + useRemoteProxy: true, + authToken: undefined, + providerRequiresOAuth: true, + }; + + const skipLocalAuth = scenario.useRemoteProxy && scenario.authToken; + const shouldTriggerLocalOAuth = scenario.providerRequiresOAuth && !skipLocalAuth; + + expect(skipLocalAuth).toBeFalsy(); + expect(shouldTriggerLocalOAuth).toBe(true); + }); + }); + + describe('preflight quota check bypass', () => { + it('should skip preflight for agy provider when using remote proxy with auth', () => { + const provider = 'agy'; + const skipLocalAuth = true; + + const shouldRunPreflight = provider === 'agy' && !skipLocalAuth; + + expect(shouldRunPreflight).toBe(false); + }); + + it('should run preflight for agy provider when using local mode', () => { + const provider = 'agy'; + const skipLocalAuth = false; + + const shouldRunPreflight = provider === 'agy' && !skipLocalAuth; + + expect(shouldRunPreflight).toBe(true); + }); + + it('should not run preflight for non-agy providers regardless of mode', () => { + const providers = ['gemini', 'codex', 'copilot', 'kiro']; + + for (const provider of providers) { + const shouldRunPreflight = provider === 'agy' && !false; + expect(shouldRunPreflight).toBe(false); + } + }); + }); + + describe('model configuration bypass', () => { + it('should skip model config when using remote proxy with auth', () => { + const supportsModelConfig = true; + const skipLocalAuth = true; + + const shouldConfigureModel = supportsModelConfig && !skipLocalAuth; + + expect(shouldConfigureModel).toBe(false); + }); + + it('should run model config when using local mode', () => { + const supportsModelConfig = true; + const skipLocalAuth = false; + + const shouldConfigureModel = supportsModelConfig && !skipLocalAuth; + + expect(shouldConfigureModel).toBe(true); + }); + }); + + describe('broken model warning bypass', () => { + it('should skip broken model warning when using remote proxy with auth', () => { + const skipLocalAuth = true; + const currentModel = 'some-broken-model'; + const isModelBroken = true; + + // Logic: only warn if NOT skipping local auth + const shouldWarn = !skipLocalAuth && currentModel && isModelBroken; + + expect(shouldWarn).toBe(false); + }); + + it('should show broken model warning when using local mode', () => { + const skipLocalAuth = false; + const currentModel = 'some-broken-model'; + const isModelBroken = true; + + const shouldWarn = !skipLocalAuth && currentModel && isModelBroken; + + expect(shouldWarn).toBe(true); + }); + }); + + describe('CI/CD workflow scenarios', () => { + it('should enable headless CI workflow with remote proxy', () => { + // Simulate GitHub Actions workflow configuration + const workflowConfig = { + headless: true, + proxyHost: 'proxy.example.com', + proxyPort: 443, + proxyProtocol: 'https', + proxyAuthToken: 'github-secret-token', + remoteOnly: true, + }; + + // Determine if this configuration should skip local OAuth + const useRemoteProxy = !!workflowConfig.proxyHost; + const skipLocalAuth = useRemoteProxy && !!workflowConfig.proxyAuthToken; + + expect(useRemoteProxy).toBe(true); + expect(skipLocalAuth).toBe(true); + }); + + it('should require local OAuth when no proxy configured', () => { + // Simulate local development without proxy + const localConfig = { + headless: false, + proxyHost: undefined, + proxyAuthToken: undefined, + }; + + const useRemoteProxy = !!localConfig.proxyHost; + const skipLocalAuth = useRemoteProxy && !!localConfig.proxyAuthToken; + + expect(useRemoteProxy).toBe(false); + expect(skipLocalAuth).toBe(false); + }); + }); +}); From 21e819b59062b77c2686ecc5f24e9c3436e42f84 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 26 Jan 2026 16:16:23 -0500 Subject: [PATCH 02/21] fix(cliproxy): improve skip-local-auth edge case handling - Validate authToken with trim() to reject whitespace-only values - Show broken model warning for both remote and local modes - Add context-aware messaging for remote proxy users - Add comprehensive test coverage for authToken edge cases: - Whitespace-only strings - Null values - Tabs/newlines - Tokens with leading/trailing whitespace Co-authored-by: Shun Kakinoki --- src/cliproxy/cliproxy-executor.ts | 34 +++--- tests/unit/cliproxy/skip-local-auth.test.ts | 119 ++++++++++++++++---- 2 files changed, 113 insertions(+), 40 deletions(-) diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index c74453f5..96b1e14b 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -500,7 +500,9 @@ export async function execClaudeWithCLIProxy( // 3. Ensure OAuth completed (if provider requires it) // Skip local OAuth check when using remote proxy with auth token // The remote proxy has its own OAuth sessions and handles authentication - const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + // Note: Trim authToken to reject whitespace-only values + const remoteAuthToken = proxyConfig.authToken?.trim(); + const skipLocalAuth = useRemoteProxy && !!remoteAuthToken; if (skipLocalAuth) { log(`Using remote proxy authentication (skipping local OAuth)`); } @@ -585,21 +587,23 @@ export async function execClaudeWithCLIProxy( } // 5. Check for known broken models and warn user - // Skip for remote proxy - model selection is on the remote server - if (!skipLocalAuth) { - 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}`); - } - console.error(` Run "ccs ${provider} --config" to change model.`); - console.error(''); + // Show warning for both local and remote modes - user should be aware of model issues + 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 (creates from defaults if not) diff --git a/tests/unit/cliproxy/skip-local-auth.test.ts b/tests/unit/cliproxy/skip-local-auth.test.ts index e840e032..b20ff061 100644 --- a/tests/unit/cliproxy/skip-local-auth.test.ts +++ b/tests/unit/cliproxy/skip-local-auth.test.ts @@ -3,54 +3,116 @@ * * When --proxy-host and --proxy-auth-token are provided together, the system should * skip local OAuth checks because the remote proxy handles authentication. + * + * Implementation uses: const remoteAuthToken = proxyConfig.authToken?.trim(); + * const skipLocalAuth = useRemoteProxy && !!remoteAuthToken; */ import { describe, it, expect } from 'bun:test'; +/** + * Helper to compute skipLocalAuth exactly as the implementation does + * Mirrors logic from cliproxy-executor.ts lines 503-505 + */ +function computeSkipLocalAuth( + useRemoteProxy: boolean, + proxyConfig: { authToken?: string | null } +): boolean { + const remoteAuthToken = proxyConfig.authToken?.trim(); + return useRemoteProxy && !!remoteAuthToken; +} + describe('skip-local-auth logic', () => { describe('skipLocalAuth flag determination', () => { it('should skip local auth when both useRemoteProxy and authToken are truthy', () => { const useRemoteProxy = true; const proxyConfig = { authToken: 'test-token-123' }; - const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig); - expect(skipLocalAuth).toBeTruthy(); + expect(skipLocalAuth).toBe(true); }); it('should NOT skip local auth when useRemoteProxy is false', () => { const useRemoteProxy = false; const proxyConfig = { authToken: 'test-token-123' }; - const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig); - expect(skipLocalAuth).toBeFalsy(); + expect(skipLocalAuth).toBe(false); }); it('should NOT skip local auth when authToken is undefined', () => { const useRemoteProxy = true; const proxyConfig = { authToken: undefined }; - const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig); - expect(skipLocalAuth).toBeFalsy(); + expect(skipLocalAuth).toBe(false); }); it('should NOT skip local auth when authToken is empty string', () => { const useRemoteProxy = true; const proxyConfig = { authToken: '' }; - const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig); - expect(skipLocalAuth).toBeFalsy(); + expect(skipLocalAuth).toBe(false); }); it('should NOT skip local auth when both are falsy', () => { const useRemoteProxy = false; const proxyConfig = { authToken: undefined }; - const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig); - expect(skipLocalAuth).toBeFalsy(); + expect(skipLocalAuth).toBe(false); + }); + }); + + describe('authToken edge cases', () => { + it('should NOT skip local auth when authToken is whitespace-only', () => { + const useRemoteProxy = true; + const proxyConfig = { authToken: ' ' }; + + const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig); + + expect(skipLocalAuth).toBe(false); + }); + + it('should NOT skip local auth when authToken is tabs and newlines', () => { + const useRemoteProxy = true; + const proxyConfig = { authToken: '\t\n\r' }; + + const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig); + + expect(skipLocalAuth).toBe(false); + }); + + it('should NOT skip local auth when authToken is null', () => { + const useRemoteProxy = true; + const proxyConfig = { authToken: null }; + + const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig); + + expect(skipLocalAuth).toBe(false); + }); + + it('should skip local auth when authToken has leading/trailing whitespace but valid content', () => { + const useRemoteProxy = true; + const proxyConfig = { authToken: ' valid-token-123 ' }; + + const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig); + + expect(skipLocalAuth).toBe(true); + }); + + it('should skip local auth when authToken contains special characters', () => { + const useRemoteProxy = true; + const proxyConfig = { authToken: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test' }; + + const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig); + + expect(skipLocalAuth).toBe(true); }); }); @@ -152,26 +214,33 @@ describe('skip-local-auth logic', () => { }); }); - describe('broken model warning bypass', () => { - it('should skip broken model warning when using remote proxy with auth', () => { + describe('broken model warning behavior', () => { + it('should show broken model warning in BOTH remote and local modes', () => { + // Updated behavior: warnings always shown (with different messaging for remote) + // Remote users need to know about broken models too + const currentModel = 'some-broken-model'; + const isModelBroken = true; + + // Warning should show regardless of skipLocalAuth + const shouldWarnRemote = currentModel && isModelBroken; // skipLocalAuth=true + const shouldWarnLocal = currentModel && isModelBroken; // skipLocalAuth=false + + expect(shouldWarnRemote).toBe(true); + expect(shouldWarnLocal).toBe(true); + }); + + it('should show different message for remote vs local mode', () => { const skipLocalAuth = true; const currentModel = 'some-broken-model'; const isModelBroken = true; - // Logic: only warn if NOT skipping local auth - const shouldWarn = !skipLocalAuth && currentModel && isModelBroken; + // When remote: "Note: Model may be overridden by remote proxy configuration." + // When local: "Run ccs --config to change model." + const remoteMessage = skipLocalAuth + ? 'Note: Model may be overridden by remote proxy configuration.' + : 'Run "ccs provider --config" to change model.'; - expect(shouldWarn).toBe(false); - }); - - it('should show broken model warning when using local mode', () => { - const skipLocalAuth = false; - const currentModel = 'some-broken-model'; - const isModelBroken = true; - - const shouldWarn = !skipLocalAuth && currentModel && isModelBroken; - - expect(shouldWarn).toBe(true); + expect(remoteMessage).toContain('remote proxy'); }); }); From 838cd1d460de68acb571bb44bc12f91bd0636ff7 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 26 Jan 2026 16:19:56 -0500 Subject: [PATCH 03/21] fix(test): use correct provider name 'ghcp' instead of 'copilot' --- tests/unit/cliproxy/skip-local-auth.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/cliproxy/skip-local-auth.test.ts b/tests/unit/cliproxy/skip-local-auth.test.ts index b20ff061..f507f40f 100644 --- a/tests/unit/cliproxy/skip-local-auth.test.ts +++ b/tests/unit/cliproxy/skip-local-auth.test.ts @@ -185,7 +185,7 @@ describe('skip-local-auth logic', () => { }); it('should not run preflight for non-agy providers regardless of mode', () => { - const providers = ['gemini', 'codex', 'copilot', 'kiro']; + const providers = ['gemini', 'codex', 'ghcp', 'kiro']; for (const provider of providers) { const shouldRunPreflight = provider === 'agy' && !false; From 5e398ff2f9659d8f7b491efae5ee1295d5fd20b9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 26 Jan 2026 21:27:29 +0000 Subject: [PATCH 04/21] chore(release): 7.28.1-dev.1 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e407d00..3505e3b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.28.1", + "version": "7.28.1-dev.1", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", From 28d8bd84a5ac912b79416aeced95f74fd71876bb Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 15:43:21 -0500 Subject: [PATCH 05/21] feat(cliproxy): add Claude (Anthropic) OAuth provider support Add Claude as a first-class OAuth provider in CCS CLI and dashboard. Backend support already exists in CLIProxyAPIPlus (port 54545). Changes: - Add 'claude' to CLIProxyProvider type and profile detector - Add Claude OAuth config (port 54545, --anthropic-login flag) - Add Claude to oauth-port-diagnostics flow types - Add Claude token discovery prefixes (claude-, anthropic-) - Add Claude model catalog (Opus 4.5, Sonnet 4.5/4, Haiku 4.5) - Add Claude logo and provider display name - Update variant config types and adapters Closes #380 --- src/auth/profile-detector.ts | 1 + src/cliproxy/auth/auth-types.ts | 10 ++++ src/cliproxy/config-generator.ts | 1 + src/cliproxy/model-catalog.ts | 49 +++++++++++++++++++ .../services/variant-config-adapter.ts | 2 +- src/cliproxy/types.ts | 11 ++++- src/config/unified-config-types.ts | 2 +- src/management/oauth-port-diagnostics.ts | 15 +++++- src/types/config.ts | 2 +- ui/public/assets/providers/claude.svg | 1 + ui/src/components/cliproxy/provider-logo.tsx | 1 + ui/src/lib/model-catalogs.ts | 45 +++++++++++++++++ 12 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 ui/public/assets/providers/claude.svg diff --git a/src/auth/profile-detector.ts b/src/auth/profile-detector.ts index bde28265..d5db7ae1 100644 --- a/src/auth/profile-detector.ts +++ b/src/auth/profile-detector.ts @@ -28,6 +28,7 @@ export const CLIPROXY_PROFILES = [ 'iflow', 'kiro', 'ghcp', + 'claude', ] as const; export type CLIProxyProfileName = (typeof CLIPROXY_PROFILES)[number]; diff --git a/src/cliproxy/auth/auth-types.ts b/src/cliproxy/auth/auth-types.ts index 42e3cc9e..d3af8c76 100644 --- a/src/cliproxy/auth/auth-types.ts +++ b/src/cliproxy/auth/auth-types.ts @@ -27,6 +27,7 @@ export const OAUTH_CALLBACK_PORTS: Partial> = { codex: 1455, agy: 51121, iflow: 11451, + claude: 54545, // qwen: Device Code Flow - no callback port // ghcp: Device Code Flow - no callback port }; @@ -121,6 +122,13 @@ export const OAUTH_CONFIGS: Record = { scopes: ['copilot'], authFlag: '--github-copilot-login', }, + claude: { + provider: 'claude', + displayName: 'Claude (Anthropic)', + authUrl: 'https://console.anthropic.com/oauth/authorize', + scopes: ['user:inference', 'user:profile'], + authFlag: '--anthropic-login', + }, }; /** @@ -136,6 +144,7 @@ export const PROVIDER_AUTH_PREFIXES: Record = { iflow: ['iflow-'], kiro: ['kiro-', 'aws-', 'codewhisperer-'], ghcp: ['github-copilot-', 'copilot-', 'gh-'], + claude: ['claude-', 'anthropic-'], }; /** @@ -150,6 +159,7 @@ export const PROVIDER_TYPE_VALUES: Record = { iflow: ['iflow'], kiro: ['kiro', 'codewhisperer'], ghcp: ['github-copilot', 'copilot'], + claude: ['claude', 'anthropic'], }; /** diff --git a/src/cliproxy/config-generator.ts b/src/cliproxy/config-generator.ts index a4652417..a5772bf1 100644 --- a/src/cliproxy/config-generator.ts +++ b/src/cliproxy/config-generator.ts @@ -299,6 +299,7 @@ const PROVIDER_DISPLAY_NAMES: Record = { iflow: 'iFlow', kiro: 'Kiro (AWS)', ghcp: 'GitHub Copilot (OAuth)', + claude: 'Claude (Anthropic)', }; /** diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index 78c86d38..7f035350 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -166,6 +166,55 @@ export const MODEL_CATALOG: Partial> = }, ], }, + claude: { + provider: 'claude', + displayName: 'Claude (Anthropic)', + defaultModel: 'claude-sonnet-4-5-20250514', + models: [ + { + id: 'claude-opus-4-5-20250220', + name: 'Claude Opus 4.5', + description: 'Most capable Claude model', + thinking: { + type: 'budget', + min: 1024, + max: 128000, + zeroAllowed: false, + dynamicAllowed: true, + }, + }, + { + id: 'claude-sonnet-4-5-20250514', + name: 'Claude Sonnet 4.5', + description: 'Balanced performance and speed', + thinking: { + type: 'budget', + min: 1024, + max: 128000, + zeroAllowed: false, + dynamicAllowed: true, + }, + }, + { + id: 'claude-sonnet-4-20250514', + name: 'Claude Sonnet 4', + description: 'Previous generation Sonnet', + thinking: { + type: 'budget', + min: 1024, + max: 128000, + zeroAllowed: false, + dynamicAllowed: true, + }, + }, + { + id: 'claude-haiku-4-5-20250514', + name: 'Claude Haiku 4.5', + description: 'Fast and efficient', + thinking: { type: 'none' }, + }, + ], + }, }; /** diff --git a/src/cliproxy/services/variant-config-adapter.ts b/src/cliproxy/services/variant-config-adapter.ts index 4294b956..1fb0ec65 100644 --- a/src/cliproxy/services/variant-config-adapter.ts +++ b/src/cliproxy/services/variant-config-adapter.ts @@ -132,7 +132,7 @@ export function saveVariantUnified( if (!config.cliproxy) { config.cliproxy = { oauth_accounts: {}, - providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'], + providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', 'claude'], variants: {}, }; } diff --git a/src/cliproxy/types.ts b/src/cliproxy/types.ts index 9759bba2..3ecc56bb 100644 --- a/src/cliproxy/types.ts +++ b/src/cliproxy/types.ts @@ -118,8 +118,17 @@ export interface DownloadResult { * - iflow: iFlow via OAuth * - kiro: Kiro (AWS CodeWhisperer) via OAuth * - ghcp: GitHub Copilot via Device Code (OAuth through CLIProxyAPIPlus) + * - claude: Claude (Anthropic) via OAuth */ -export type CLIProxyProvider = 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp'; +export type CLIProxyProvider = + | 'gemini' + | 'codex' + | 'agy' + | 'qwen' + | 'iflow' + | 'kiro' + | 'ghcp' + | 'claude'; /** * CLIProxy backend selection diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index f2d3618e..0df03c27 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -61,7 +61,7 @@ export type OAuthAccounts = Record; */ export interface CLIProxyVariantConfig { /** Base provider to use */ - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp'; + provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; /** Account nickname (references oauth_accounts) */ account?: string; /** Path to settings file (e.g., "~/.ccs/gemini-custom.settings.json") */ diff --git a/src/management/oauth-port-diagnostics.ts b/src/management/oauth-port-diagnostics.ts index 262d963d..48db2348 100644 --- a/src/management/oauth-port-diagnostics.ts +++ b/src/management/oauth-port-diagnostics.ts @@ -34,6 +34,7 @@ export const OAUTH_CALLBACK_PORTS: Record = { iflow: 11451, // Authorization Code Flow kiro: 9876, // Authorization Code Flow ghcp: null, // Device Code Flow - no callback port + claude: 54545, // Authorization Code Flow (Anthropic OAuth) }; /** @@ -52,6 +53,7 @@ export const OAUTH_FLOW_TYPES: Record = { iflow: 'authorization_code', kiro: 'authorization_code', ghcp: 'device_code', + claude: 'authorization_code', }; /** @@ -138,7 +140,16 @@ export async function checkOAuthPort(provider: CLIProxyProvider): Promise { - const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp']; + const providers: CLIProxyProvider[] = [ + 'gemini', + 'codex', + 'agy', + 'qwen', + 'iflow', + 'kiro', + 'ghcp', + 'claude', + ]; const results: OAuthPortDiagnostic[] = []; for (const provider of providers) { @@ -153,7 +164,7 @@ export async function checkAllOAuthPorts(): Promise { * Check OAuth ports for providers that use Authorization Code flow only */ export async function checkAuthCodePorts(): Promise { - const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'kiro']; + const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'kiro', 'claude']; const results: OAuthPortDiagnostic[] = []; for (const provider of providers) { diff --git a/src/types/config.ts b/src/types/config.ts index a00195a3..db910441 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -18,7 +18,7 @@ export interface ProfilesConfig { */ export interface CLIProxyVariantConfig { /** CLIProxy provider to use */ - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp'; + provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; /** Path to settings.json with custom model configuration (optional) */ settings?: string; /** Account identifier for multi-account support (optional, defaults to 'default') */ diff --git a/ui/public/assets/providers/claude.svg b/ui/public/assets/providers/claude.svg new file mode 100644 index 00000000..62dc0db1 --- /dev/null +++ b/ui/public/assets/providers/claude.svg @@ -0,0 +1 @@ +Claude \ No newline at end of file diff --git a/ui/src/components/cliproxy/provider-logo.tsx b/ui/src/components/cliproxy/provider-logo.tsx index 66ad0420..ee762285 100644 --- a/ui/src/components/cliproxy/provider-logo.tsx +++ b/ui/src/components/cliproxy/provider-logo.tsx @@ -20,6 +20,7 @@ const PROVIDER_IMAGES: Record = { iflow: '/assets/providers/iflow.png', kiro: '/assets/providers/kiro.png', ghcp: '/assets/providers/copilot.svg', + claude: '/assets/providers/claude.svg', }; /** Provider color configuration (for fallback only - no background for image logos) */ diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index 09c056ba..9c6065a1 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -311,4 +311,49 @@ export const MODEL_CATALOGS: Record = { }, ], }, + claude: { + provider: 'claude', + displayName: 'Claude (Anthropic)', + defaultModel: 'claude-sonnet-4-5-20250514', + models: [ + { + id: 'claude-opus-4-5-20250220', + name: 'Claude Opus 4.5', + description: 'Most capable Claude model', + presetMapping: { + default: 'claude-opus-4-5-20250220', + opus: 'claude-opus-4-5-20250220', + sonnet: 'claude-sonnet-4-5-20250514', + haiku: 'claude-haiku-4-5-20250514', + }, + }, + { + id: 'claude-sonnet-4-5-20250514', + name: 'Claude Sonnet 4.5', + description: 'Balanced performance and speed', + presetMapping: { + default: 'claude-sonnet-4-5-20250514', + opus: 'claude-opus-4-5-20250220', + sonnet: 'claude-sonnet-4-5-20250514', + haiku: 'claude-haiku-4-5-20250514', + }, + }, + { + id: 'claude-sonnet-4-20250514', + name: 'Claude Sonnet 4', + description: 'Previous generation Sonnet', + presetMapping: { + default: 'claude-sonnet-4-20250514', + opus: 'claude-opus-4-5-20250220', + sonnet: 'claude-sonnet-4-20250514', + haiku: 'claude-haiku-4-5-20250514', + }, + }, + { + id: 'claude-haiku-4-5-20250514', + name: 'Claude Haiku 4.5', + description: 'Fast and efficient', + }, + ], + }, }; From d2129957d7e954701be973725545f475711d0468 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 20:14:22 -0500 Subject: [PATCH 06/21] fix(cliproxy): add Claude to all provider lists for sidebar display Claude was missing from: - getAllAuthStatus() providers array in token-manager.ts - PROVIDER_ASSETS, PROVIDER_COLORS, PROVIDER_NAMES in provider-config.ts - PROVIDERS array in setup wizard constants.ts - PLUS_ONLY_PROVIDERS in proxy settings --- src/cliproxy/auth/token-manager.ts | 11 ++++++++++- ui/src/components/setup/wizard/constants.ts | 1 + ui/src/lib/provider-config.ts | 3 +++ ui/src/pages/settings/sections/proxy/index.tsx | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/cliproxy/auth/token-manager.ts b/src/cliproxy/auth/token-manager.ts index 1e31273c..fb1fa6fb 100644 --- a/src/cliproxy/auth/token-manager.ts +++ b/src/cliproxy/auth/token-manager.ts @@ -145,7 +145,16 @@ export function getAuthStatus(provider: CLIProxyProvider): AuthStatus { * Get auth status for all providers */ export function getAllAuthStatus(): AuthStatus[] { - const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp']; + const providers: CLIProxyProvider[] = [ + 'gemini', + 'codex', + 'agy', + 'qwen', + 'iflow', + 'kiro', + 'ghcp', + 'claude', + ]; return providers.map(getAuthStatus); } diff --git a/ui/src/components/setup/wizard/constants.ts b/ui/src/components/setup/wizard/constants.ts index 4c530fe7..abed372c 100644 --- a/ui/src/components/setup/wizard/constants.ts +++ b/ui/src/components/setup/wizard/constants.ts @@ -12,6 +12,7 @@ export const PROVIDERS: ProviderOption[] = [ { id: 'iflow', name: 'iFlow', description: 'iFlow AI models' }, { id: 'kiro', name: 'Kiro (AWS)', description: 'AWS CodeWhisperer models' }, { id: 'ghcp', name: 'GitHub Copilot (OAuth)', description: 'GitHub Copilot via OAuth' }, + { id: 'claude', name: 'Claude (Anthropic)', description: 'Claude Opus/Sonnet models' }, ]; export const ALL_STEPS = ['provider', 'auth', 'variant', 'success']; diff --git a/ui/src/lib/provider-config.ts b/ui/src/lib/provider-config.ts index 989479a2..764adf51 100644 --- a/ui/src/lib/provider-config.ts +++ b/ui/src/lib/provider-config.ts @@ -11,6 +11,7 @@ export const PROVIDER_ASSETS: Record = { qwen: '/assets/providers/qwen-color.svg', kiro: '/assets/providers/kiro.png', ghcp: '/assets/providers/copilot.svg', + claude: '/assets/providers/claude.svg', }; // Provider brand colors @@ -23,6 +24,7 @@ export const PROVIDER_COLORS: Record = { qwen: '#6236FF', kiro: '#4d908e', // Dark Cyan (AWS-inspired) ghcp: '#43aa8b', // Seaweed (GitHub-inspired) + claude: '#D97706', // Anthropic orange }; // Provider display names @@ -35,6 +37,7 @@ const PROVIDER_NAMES: Record = { qwen: 'Qwen', kiro: 'Kiro (AWS)', ghcp: 'GitHub Copilot (OAuth)', + claude: 'Claude (Anthropic)', }; // Map provider to display name diff --git a/ui/src/pages/settings/sections/proxy/index.tsx b/ui/src/pages/settings/sections/proxy/index.tsx index 38879cc2..0009b164 100644 --- a/ui/src/pages/settings/sections/proxy/index.tsx +++ b/ui/src/pages/settings/sections/proxy/index.tsx @@ -29,7 +29,7 @@ import { api } from '@/lib/api-client'; const DEBUG_MODE_KEY = 'ccs_debug_mode'; /** Providers only available on CLIProxyAPIPlus */ -const PLUS_ONLY_PROVIDERS = ['kiro', 'ghcp']; +const PLUS_ONLY_PROVIDERS = ['kiro', 'ghcp', 'claude']; export default function ProxySection() { const { From b385ab131d2b179c7b7bd014859f9118afd6ce5c Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 20:17:10 -0500 Subject: [PATCH 07/21] refactor(cliproxy): reorder providers - Antigravity first, then Claude --- src/cliproxy/auth/token-manager.ts | 4 ++-- ui/src/components/setup/wizard/constants.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cliproxy/auth/token-manager.ts b/src/cliproxy/auth/token-manager.ts index fb1fa6fb..665e821d 100644 --- a/src/cliproxy/auth/token-manager.ts +++ b/src/cliproxy/auth/token-manager.ts @@ -146,14 +146,14 @@ export function getAuthStatus(provider: CLIProxyProvider): AuthStatus { */ export function getAllAuthStatus(): AuthStatus[] { const providers: CLIProxyProvider[] = [ + 'agy', + 'claude', 'gemini', 'codex', - 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', - 'claude', ]; return providers.map(getAuthStatus); } diff --git a/ui/src/components/setup/wizard/constants.ts b/ui/src/components/setup/wizard/constants.ts index abed372c..ba0fda30 100644 --- a/ui/src/components/setup/wizard/constants.ts +++ b/ui/src/components/setup/wizard/constants.ts @@ -5,14 +5,14 @@ import type { ProviderOption } from './types'; export const PROVIDERS: ProviderOption[] = [ + { id: 'agy', name: 'Antigravity', description: 'Antigravity AI models' }, + { id: 'claude', name: 'Claude (Anthropic)', description: 'Claude Opus/Sonnet models' }, { id: 'gemini', name: 'Google Gemini', description: 'Gemini Pro/Flash models' }, { id: 'codex', name: 'OpenAI Codex', description: 'GPT-4 and codex models' }, - { id: 'agy', name: 'Antigravity', description: 'Antigravity AI models' }, { id: 'qwen', name: 'Alibaba Qwen', description: 'Qwen Code models' }, { id: 'iflow', name: 'iFlow', description: 'iFlow AI models' }, { id: 'kiro', name: 'Kiro (AWS)', description: 'AWS CodeWhisperer models' }, { id: 'ghcp', name: 'GitHub Copilot (OAuth)', description: 'GitHub Copilot via OAuth' }, - { id: 'claude', name: 'Claude (Anthropic)', description: 'Claude Opus/Sonnet models' }, ]; export const ALL_STEPS = ['provider', 'auth', 'variant', 'success']; From 2091a90b7710e7cb0b565577a5e659473126a541 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 20:24:01 -0500 Subject: [PATCH 08/21] fix(cliproxy): address PR review feedback - Remove claude from PLUS_ONLY_PROVIDERS (Claude works with normal CLIProxy too) - Fix color inconsistency: use #D97757 to match SVG brand color - Add iflow to checkAuthCodePorts (pre-existing issue, fixed opportunistically) --- src/management/oauth-port-diagnostics.ts | 2 +- ui/src/lib/provider-config.ts | 2 +- ui/src/pages/settings/sections/proxy/index.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/management/oauth-port-diagnostics.ts b/src/management/oauth-port-diagnostics.ts index 48db2348..806a97cc 100644 --- a/src/management/oauth-port-diagnostics.ts +++ b/src/management/oauth-port-diagnostics.ts @@ -164,7 +164,7 @@ export async function checkAllOAuthPorts(): Promise { * Check OAuth ports for providers that use Authorization Code flow only */ export async function checkAuthCodePorts(): Promise { - const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'kiro', 'claude']; + const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'iflow', 'kiro', 'claude']; const results: OAuthPortDiagnostic[] = []; for (const provider of providers) { diff --git a/ui/src/lib/provider-config.ts b/ui/src/lib/provider-config.ts index 764adf51..72cb9808 100644 --- a/ui/src/lib/provider-config.ts +++ b/ui/src/lib/provider-config.ts @@ -24,7 +24,7 @@ export const PROVIDER_COLORS: Record = { qwen: '#6236FF', kiro: '#4d908e', // Dark Cyan (AWS-inspired) ghcp: '#43aa8b', // Seaweed (GitHub-inspired) - claude: '#D97706', // Anthropic orange + claude: '#D97757', // Anthropic brand color (matches SVG) }; // Provider display names diff --git a/ui/src/pages/settings/sections/proxy/index.tsx b/ui/src/pages/settings/sections/proxy/index.tsx index 0009b164..38879cc2 100644 --- a/ui/src/pages/settings/sections/proxy/index.tsx +++ b/ui/src/pages/settings/sections/proxy/index.tsx @@ -29,7 +29,7 @@ import { api } from '@/lib/api-client'; const DEBUG_MODE_KEY = 'ccs_debug_mode'; /** Providers only available on CLIProxyAPIPlus */ -const PLUS_ONLY_PROVIDERS = ['kiro', 'ghcp', 'claude']; +const PLUS_ONLY_PROVIDERS = ['kiro', 'ghcp']; export default function ProxySection() { const { From c4ec326530d3baf5847d264c5601ded52d07bfb6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Jan 2026 01:29:08 +0000 Subject: [PATCH 09/21] chore(release): 7.28.2-dev.1 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce1cfac0..d5a0bccd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.28.2", + "version": "7.28.2-dev.1", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", From 9cd9c423e929579c86da3f409d74927c3c7dedc1 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 20:50:24 -0500 Subject: [PATCH 10/21] fix: replace hardcoded provider validation arrays with CLIPROXY_PROFILES import Closes #382 Replace duplicate hardcoded provider validation arrays across three route files with a single import from CLIPROXY_PROFILES constant. This DRY fix eliminates code duplication and ensures provider validation is consistent across all routes. Files updated: - account-routes.ts - cliproxy-auth-routes.ts - cliproxy-stats-routes.ts Reduces code by 21 lines while improving maintainability. --- src/web-server/routes/account-routes.ts | 13 +++---------- src/web-server/routes/cliproxy-auth-routes.ts | 13 +++---------- src/web-server/routes/cliproxy-stats-routes.ts | 13 +++---------- 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/src/web-server/routes/account-routes.ts b/src/web-server/routes/account-routes.ts index d87d6592..cfc3ef34 100644 --- a/src/web-server/routes/account-routes.ts +++ b/src/web-server/routes/account-routes.ts @@ -17,20 +17,13 @@ import { soloAccount, } from '../../cliproxy/account-manager'; import type { CLIProxyProvider } from '../../cliproxy/types'; +import { CLIPROXY_PROFILES } from '../../auth/profile-detector'; const router = Router(); const registry = new ProfileRegistry(); -/** Valid CLIProxy providers */ -const VALID_PROVIDERS: CLIProxyProvider[] = [ - 'gemini', - 'codex', - 'agy', - 'qwen', - 'iflow', - 'kiro', - 'ghcp', -]; +/** Valid CLIProxy providers - derived from canonical CLIPROXY_PROFILES */ +const VALID_PROVIDERS: CLIProxyProvider[] = [...CLIPROXY_PROFILES]; /** Check if provider is valid */ function isValidProvider(provider: string): provider is CLIProxyProvider { diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts index 5485b178..b72418d3 100644 --- a/src/web-server/routes/cliproxy-auth-routes.ts +++ b/src/web-server/routes/cliproxy-auth-routes.ts @@ -35,19 +35,12 @@ import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader'; import { tryKiroImport } from '../../cliproxy/auth/kiro-import'; import { getProviderTokenDir } from '../../cliproxy/auth/token-manager'; import type { CLIProxyProvider } from '../../cliproxy/types'; +import { CLIPROXY_PROFILES } from '../../auth/profile-detector'; const router = Router(); -// Valid providers list -const validProviders: CLIProxyProvider[] = [ - 'gemini', - 'codex', - 'agy', - 'qwen', - 'iflow', - 'kiro', - 'ghcp', -]; +// Valid providers list - derived from canonical CLIPROXY_PROFILES +const validProviders: CLIProxyProvider[] = [...CLIPROXY_PROFILES]; /** * GET /api/cliproxy/auth - Get auth status for built-in CLIProxy profiles diff --git a/src/web-server/routes/cliproxy-stats-routes.ts b/src/web-server/routes/cliproxy-stats-routes.ts index ebd77966..032a86a0 100644 --- a/src/web-server/routes/cliproxy-stats-routes.ts +++ b/src/web-server/routes/cliproxy-stats-routes.ts @@ -14,6 +14,7 @@ import { } from '../../cliproxy/stats-fetcher'; import { fetchAccountQuota } from '../../cliproxy/quota-fetcher'; import type { CLIProxyProvider } from '../../cliproxy/types'; +import { CLIPROXY_PROFILES } from '../../auth/profile-detector'; import { getCliproxyWritablePath, getCliproxyConfigPath, @@ -517,16 +518,8 @@ router.put('/models/:provider', async (req: Request, res: Response): Promise => { const { provider, accountId } = req.params; - // Validate provider - const validProviders: CLIProxyProvider[] = [ - 'agy', - 'gemini', - 'codex', - 'qwen', - 'iflow', - 'kiro', - 'ghcp', - ]; + // Validate provider - use canonical CLIPROXY_PROFILES + const validProviders: CLIProxyProvider[] = [...CLIPROXY_PROFILES]; if (!validProviders.includes(provider as CLIProxyProvider)) { res.status(400).json({ error: 'Invalid provider', From 4a2abc74cac93e17ee12fab3fcf8fc0693552347 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 21:02:34 -0500 Subject: [PATCH 11/21] fix: add claude provider to statsProviderMap, UI types, and provider arrays Fixes remaining edge cases from #382: - Add anthropic/claude mapping to statsProviderMap in cliproxy-auth-routes - Add 'claude' case to provider-refreshers switch statement - Add 'claude' to 4 UI type definitions in api-client.ts - Add 'claude' to cliproxy-dialog providers array and options - Add 'claude' to use-cliproxy-auth-flow VALID_PROVIDERS - Fix wizard index.tsx type cast to include all 8 providers --- src/cliproxy/auth/provider-refreshers/index.ts | 1 + src/web-server/routes/cliproxy-auth-routes.ts | 2 ++ ui/src/components/cliproxy/cliproxy-dialog.tsx | 3 ++- ui/src/components/setup/wizard/index.tsx | 10 +++++++++- ui/src/hooks/use-cliproxy-auth-flow.ts | 2 +- ui/src/lib/api-client.ts | 8 ++++---- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/cliproxy/auth/provider-refreshers/index.ts b/src/cliproxy/auth/provider-refreshers/index.ts index 232fe8c9..89df2ab2 100644 --- a/src/cliproxy/auth/provider-refreshers/index.ts +++ b/src/cliproxy/auth/provider-refreshers/index.ts @@ -35,6 +35,7 @@ export async function refreshToken( case 'iflow': case 'kiro': case 'ghcp': + case 'claude': return { success: false, error: `Token refresh not yet implemented for ${provider}`, diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts index b72418d3..295d7070 100644 --- a/src/web-server/routes/cliproxy-auth-routes.ts +++ b/src/web-server/routes/cliproxy-auth-routes.ts @@ -71,6 +71,8 @@ router.get('/', async (_req: Request, res: Response): Promise => { iflow: 'iflow', kiro: 'kiro', copilot: 'ghcp', // CLIProxyAPI returns 'copilot', we map to 'ghcp' + anthropic: 'claude', // CLIProxyAPI returns 'anthropic', we map to 'claude' + claude: 'claude', }; // Update lastUsedAt for providers with recent activity diff --git a/ui/src/components/cliproxy/cliproxy-dialog.tsx b/ui/src/components/cliproxy/cliproxy-dialog.tsx index fa8fc3fb..3a848e0b 100644 --- a/ui/src/components/cliproxy/cliproxy-dialog.tsx +++ b/ui/src/components/cliproxy/cliproxy-dialog.tsx @@ -14,7 +14,7 @@ import { Label } from '@/components/ui/label'; import { useCreateVariant, useCliproxyAuth } from '@/hooks/use-cliproxy'; import { usePrivacy } from '@/contexts/privacy-context'; -const providers = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'] as const; +const providers = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', 'claude'] as const; const schema = z.object({ name: z @@ -41,6 +41,7 @@ const providerOptions = [ { value: 'iflow', label: 'iFlow' }, { value: 'kiro', label: 'Kiro (AWS)' }, { value: 'ghcp', label: 'GitHub Copilot (OAuth)' }, + { value: 'claude', label: 'Claude (Anthropic)' }, ]; export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) { diff --git a/ui/src/components/setup/wizard/index.tsx b/ui/src/components/setup/wizard/index.tsx index 29e98e76..6c21af05 100644 --- a/ui/src/components/setup/wizard/index.tsx +++ b/ui/src/components/setup/wizard/index.tsx @@ -136,7 +136,15 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { try { await createMutation.mutateAsync({ name: variantName, - provider: selectedProvider as 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow', + provider: selectedProvider as + | 'gemini' + | 'codex' + | 'agy' + | 'qwen' + | 'iflow' + | 'kiro' + | 'ghcp' + | 'claude', model: modelName || undefined, account: selectedAccount?.id, }); diff --git a/ui/src/hooks/use-cliproxy-auth-flow.ts b/ui/src/hooks/use-cliproxy-auth-flow.ts index a91a05c3..4867c3a8 100644 --- a/ui/src/hooks/use-cliproxy-auth-flow.ts +++ b/ui/src/hooks/use-cliproxy-auth-flow.ts @@ -14,7 +14,7 @@ interface AuthFlowState { error: string | null; } -const VALID_PROVIDERS = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp']; +const VALID_PROVIDERS = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', 'claude']; export function useCliproxyAuthFlow() { const [state, setState] = useState({ diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 6f052997..db85b48e 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -47,7 +47,7 @@ export interface UpdateProfile { export interface Variant { name: string; - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp'; + provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; settings: string; account?: string; port?: number; @@ -56,13 +56,13 @@ export interface Variant { export interface CreateVariant { name: string; - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp'; + provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; model?: string; account?: string; } export interface UpdateVariant { - provider?: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp'; + provider?: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; model?: string; account?: string; } @@ -72,7 +72,7 @@ export interface OAuthAccount { id: string; email?: string; nickname?: string; - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp'; + provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; isDefault: boolean; tokenFile: string; createdAt: string; From 5a4c8e009ce1cc355d6fa2f05001cab6c9b684c4 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 21:07:26 -0500 Subject: [PATCH 12/21] refactor(ui): centralize provider list in provider-config.ts (DRY) - Add CLIPROXY_PROVIDERS array as single source of truth for UI - Add CLIProxyProvider type and isValidProvider() helper - Update api-client.ts to use CLIProxyProvider type - Update cliproxy-dialog.tsx to import from provider-config - Update cliproxy-header.tsx to use CLIPROXY_PROVIDERS - Update use-cliproxy-auth-flow.ts to use isValidProvider() - Update wizard constants to use typed provider info Adding a new provider now only requires updating: - Backend: src/auth/profile-detector.ts (CLIPROXY_PROFILES) - UI: ui/src/lib/provider-config.ts (CLIPROXY_PROVIDERS) - UI wizard: ui/src/components/setup/wizard/constants.ts (PROVIDER_INFO) --- .../components/cliproxy/cliproxy-dialog.tsx | 19 +++------ .../components/cliproxy/cliproxy-header.tsx | 15 +++---- ui/src/components/setup/wizard/constants.ts | 40 ++++++++++++++----- ui/src/components/setup/wizard/index.tsx | 11 +---- ui/src/hooks/use-cliproxy-auth-flow.ts | 5 +-- ui/src/lib/api-client.ts | 10 +++-- ui/src/lib/provider-config.ts | 28 ++++++++++++- 7 files changed, 79 insertions(+), 49 deletions(-) diff --git a/ui/src/components/cliproxy/cliproxy-dialog.tsx b/ui/src/components/cliproxy/cliproxy-dialog.tsx index 3a848e0b..6ad1cce9 100644 --- a/ui/src/components/cliproxy/cliproxy-dialog.tsx +++ b/ui/src/components/cliproxy/cliproxy-dialog.tsx @@ -13,15 +13,14 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useCreateVariant, useCliproxyAuth } from '@/hooks/use-cliproxy'; import { usePrivacy } from '@/contexts/privacy-context'; - -const providers = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', 'claude'] as const; +import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config'; const schema = z.object({ name: z .string() .min(1, 'Name is required') .regex(/^[a-zA-Z][a-zA-Z0-9._-]*$/, 'Invalid variant name'), - provider: z.enum(providers, { message: 'Provider is required' }), + provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }), model: z.string().optional(), account: z.string().optional(), }); @@ -33,16 +32,10 @@ interface CliproxyDialogProps { onClose: () => void; } -const providerOptions = [ - { value: 'gemini', label: 'Google Gemini' }, - { value: 'codex', label: 'OpenAI Codex' }, - { value: 'agy', label: 'Antigravity' }, - { value: 'qwen', label: 'Alibaba Qwen' }, - { value: 'iflow', label: 'iFlow' }, - { value: 'kiro', label: 'Kiro (AWS)' }, - { value: 'ghcp', label: 'GitHub Copilot (OAuth)' }, - { value: 'claude', label: 'Claude (Anthropic)' }, -]; +const providerOptions = CLIPROXY_PROVIDERS.map((id) => ({ + value: id, + label: getProviderDisplayName(id), +})); export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) { const createMutation = useCreateVariant(); diff --git a/ui/src/components/cliproxy/cliproxy-header.tsx b/ui/src/components/cliproxy/cliproxy-header.tsx index 6b5bb51a..a2bcf5e5 100644 --- a/ui/src/components/cliproxy/cliproxy-header.tsx +++ b/ui/src/components/cliproxy/cliproxy-header.tsx @@ -10,6 +10,7 @@ import { RefreshCw, Loader2, AlertTriangle } from 'lucide-react'; import { useCliproxyAuth } from '@/hooks/use-cliproxy'; import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow'; import { cn } from '@/lib/utils'; +import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config'; interface VersionInfo { currentVersion: string; @@ -135,16 +136,10 @@ export function CliproxyHeader({ .catch(() => {}); // Silently fail }, []); - const providers = [ - { id: 'claude', displayName: 'Claude' }, - { id: 'gemini', displayName: 'Gemini' }, - { id: 'codex', displayName: 'Codex' }, - { id: 'agy', displayName: 'Agy' }, - { id: 'qwen', displayName: 'Qwen' }, - { id: 'iflow', displayName: 'iFlow' }, - { id: 'kiro', displayName: 'Kiro' }, - { id: 'ghcp', displayName: 'GitHub Copilot' }, - ]; + const providers = CLIPROXY_PROVIDERS.map((id) => ({ + id, + displayName: getProviderDisplayName(id), + })); const getProviderStatus = (providerId: string) => { const status = authData?.authStatus.find((s) => s.provider === providerId); diff --git a/ui/src/components/setup/wizard/constants.ts b/ui/src/components/setup/wizard/constants.ts index ba0fda30..96c9d633 100644 --- a/ui/src/components/setup/wizard/constants.ts +++ b/ui/src/components/setup/wizard/constants.ts @@ -1,20 +1,42 @@ /** * Constants for Quick Setup Wizard + * Provider display info with custom ordering for wizard UI. + * Provider IDs must match CLIPROXY_PROVIDERS from provider-config.ts */ import type { ProviderOption } from './types'; +import type { CLIProxyProvider } from '@/lib/provider-config'; -export const PROVIDERS: ProviderOption[] = [ - { id: 'agy', name: 'Antigravity', description: 'Antigravity AI models' }, - { id: 'claude', name: 'Claude (Anthropic)', description: 'Claude Opus/Sonnet models' }, - { id: 'gemini', name: 'Google Gemini', description: 'Gemini Pro/Flash models' }, - { id: 'codex', name: 'OpenAI Codex', description: 'GPT-4 and codex models' }, - { id: 'qwen', name: 'Alibaba Qwen', description: 'Qwen Code models' }, - { id: 'iflow', name: 'iFlow', description: 'iFlow AI models' }, - { id: 'kiro', name: 'Kiro (AWS)', description: 'AWS CodeWhisperer models' }, - { id: 'ghcp', name: 'GitHub Copilot (OAuth)', description: 'GitHub Copilot via OAuth' }, +/** Provider display info for wizard - ordered by recommendation */ +const PROVIDER_INFO: Record = { + agy: { name: 'Antigravity', description: 'Antigravity AI models' }, + claude: { name: 'Claude (Anthropic)', description: 'Claude Opus/Sonnet models' }, + gemini: { name: 'Google Gemini', description: 'Gemini Pro/Flash models' }, + codex: { name: 'OpenAI Codex', description: 'GPT-4 and codex models' }, + qwen: { name: 'Alibaba Qwen', description: 'Qwen Code models' }, + iflow: { name: 'iFlow', description: 'iFlow AI models' }, + kiro: { name: 'Kiro (AWS)', description: 'AWS CodeWhisperer models' }, + ghcp: { name: 'GitHub Copilot (OAuth)', description: 'GitHub Copilot via OAuth' }, +}; + +/** Wizard display order - most recommended first */ +const WIZARD_PROVIDER_ORDER: CLIProxyProvider[] = [ + 'agy', + 'claude', + 'gemini', + 'codex', + 'qwen', + 'iflow', + 'kiro', + 'ghcp', ]; +export const PROVIDERS: ProviderOption[] = WIZARD_PROVIDER_ORDER.map((id) => ({ + id, + name: PROVIDER_INFO[id].name, + description: PROVIDER_INFO[id].description, +})); + export const ALL_STEPS = ['provider', 'auth', 'variant', 'success']; export function getStepProgress(step: string): number { diff --git a/ui/src/components/setup/wizard/index.tsx b/ui/src/components/setup/wizard/index.tsx index 6c21af05..0b894eff 100644 --- a/ui/src/components/setup/wizard/index.tsx +++ b/ui/src/components/setup/wizard/index.tsx @@ -22,6 +22,7 @@ import { useCancelAuth, } from '@/hooks/use-cliproxy'; import type { AuthStatus, OAuthAccount } from '@/lib/api-client'; +import type { CLIProxyProvider } from '@/lib/provider-config'; import { applyDefaultPreset } from '@/lib/preset-utils'; import { usePrivacy } from '@/contexts/privacy-context'; import { toast } from 'sonner'; @@ -136,15 +137,7 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { try { await createMutation.mutateAsync({ name: variantName, - provider: selectedProvider as - | 'gemini' - | 'codex' - | 'agy' - | 'qwen' - | 'iflow' - | 'kiro' - | 'ghcp' - | 'claude', + provider: selectedProvider as CLIProxyProvider, model: modelName || undefined, account: selectedAccount?.id, }); diff --git a/ui/src/hooks/use-cliproxy-auth-flow.ts b/ui/src/hooks/use-cliproxy-auth-flow.ts index 4867c3a8..bc9c46af 100644 --- a/ui/src/hooks/use-cliproxy-auth-flow.ts +++ b/ui/src/hooks/use-cliproxy-auth-flow.ts @@ -7,6 +7,7 @@ import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { api } from '@/lib/api-client'; +import { isValidProvider } from '@/lib/provider-config'; interface AuthFlowState { provider: string | null; @@ -14,8 +15,6 @@ interface AuthFlowState { error: string | null; } -const VALID_PROVIDERS = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', 'claude']; - export function useCliproxyAuthFlow() { const [state, setState] = useState({ provider: null, @@ -35,7 +34,7 @@ export function useCliproxyAuthFlow() { const startAuth = useCallback( async (provider: string) => { - if (!VALID_PROVIDERS.includes(provider)) { + if (!isValidProvider(provider)) { setState({ provider: null, isAuthenticating: false, diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index db85b48e..77b73065 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -3,6 +3,8 @@ * Phase 03: REST API Routes & CRUD */ +import type { CLIProxyProvider } from './provider-config'; + const BASE_URL = '/api'; async function request(url: string, options?: RequestInit): Promise { @@ -47,7 +49,7 @@ export interface UpdateProfile { export interface Variant { name: string; - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; + provider: CLIProxyProvider; settings: string; account?: string; port?: number; @@ -56,13 +58,13 @@ export interface Variant { export interface CreateVariant { name: string; - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; + provider: CLIProxyProvider; model?: string; account?: string; } export interface UpdateVariant { - provider?: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; + provider?: CLIProxyProvider; model?: string; account?: string; } @@ -72,7 +74,7 @@ export interface OAuthAccount { id: string; email?: string; nickname?: string; - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; + provider: CLIProxyProvider; isDefault: boolean; tokenFile: string; createdAt: string; diff --git a/ui/src/lib/provider-config.ts b/ui/src/lib/provider-config.ts index 72cb9808..0486b973 100644 --- a/ui/src/lib/provider-config.ts +++ b/ui/src/lib/provider-config.ts @@ -1,8 +1,34 @@ /** * Provider Configuration - * Shared constants for provider branding and assets + * Shared constants for CLIProxy providers - SINGLE SOURCE OF TRUTH for UI + * + * When adding a new provider, update CLIPROXY_PROVIDERS array and related mappings. */ +/** + * Canonical list of CLIProxy provider IDs + * This is the UI's single source of truth for valid providers. + * Must stay in sync with backend's CLIPROXY_PROFILES in src/auth/profile-detector.ts + */ +export const CLIPROXY_PROVIDERS = [ + 'gemini', + 'codex', + 'agy', + 'qwen', + 'iflow', + 'kiro', + 'ghcp', + 'claude', +] as const; + +/** Union type for CLIProxy provider IDs */ +export type CLIProxyProvider = (typeof CLIPROXY_PROVIDERS)[number]; + +/** Check if a string is a valid CLIProxy provider */ +export function isValidProvider(provider: string): provider is CLIProxyProvider { + return CLIPROXY_PROVIDERS.includes(provider as CLIProxyProvider); +} + // Map provider names to asset filenames (only providers with actual logos) export const PROVIDER_ASSETS: Record = { gemini: '/assets/providers/gemini-color.svg', From 409ad67afb38bdae4b862f38f8b5b6fe1c44c509 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Jan 2026 02:27:20 +0000 Subject: [PATCH 13/21] chore(release): 7.28.2-dev.2 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d5a0bccd..91249d66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.28.2-dev.1", + "version": "7.28.2-dev.2", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", From 8017ce8f8639ffc282203d6809091df83e0c8f18 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 21:39:19 -0500 Subject: [PATCH 14/21] fix(cliproxy): use correct --claude-login flag for Claude OAuth - CLIProxyAPI expects --claude-login, not --anthropic-login - Fixes 'CLIProxy auth exited with code 2' error Closes #382 --- src/cliproxy/auth/auth-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cliproxy/auth/auth-types.ts b/src/cliproxy/auth/auth-types.ts index d3af8c76..3ebdff0a 100644 --- a/src/cliproxy/auth/auth-types.ts +++ b/src/cliproxy/auth/auth-types.ts @@ -127,7 +127,7 @@ export const OAUTH_CONFIGS: Record = { displayName: 'Claude (Anthropic)', authUrl: 'https://console.anthropic.com/oauth/authorize', scopes: ['user:inference', 'user:profile'], - authFlag: '--anthropic-login', + authFlag: '--claude-login', }, }; From a9c5520b8b4b7d49d1afe0e63b4facab3142db1b Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 21:40:29 -0500 Subject: [PATCH 15/21] fix(ui): truncate long account emails in provider editor - Add min-w-0 flex-1 for proper text overflow - Add truncate class to prevent layout break --- ui/src/components/cliproxy/provider-editor/account-item.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/src/components/cliproxy/provider-editor/account-item.tsx b/ui/src/components/cliproxy/provider-editor/account-item.tsx index ec71ce8a..0fa29685 100644 --- a/ui/src/components/cliproxy/provider-editor/account-item.tsx +++ b/ui/src/components/cliproxy/provider-editor/account-item.tsx @@ -187,9 +187,11 @@ export function AccountItem({ > -
+
- + {account.email || account.id} {account.isDefault && ( From b3cde80d0e5211f3b01def28a6f97c3f91373ccd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Jan 2026 02:43:55 +0000 Subject: [PATCH 16/21] chore(release): 7.28.2-dev.3 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 91249d66..72add80f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.28.2-dev.2", + "version": "7.28.2-dev.3", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", From 9fd93955880fd1b90d15f45d9738d413d04769ca Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 22:04:46 -0500 Subject: [PATCH 17/21] refactor(cliproxy): use CLIPROXY_PROFILES for provider arrays (DRY) - Replace hardcoded arrays in token-manager.ts, token-expiry-checker.ts - Replace hardcoded arrays in account-manager.ts, oauth-port-diagnostics.ts - Addresses PR #384 code review feedback --- src/cliproxy/account-manager.ts | 3 ++- src/cliproxy/auth/token-expiry-checker.ts | 3 ++- src/cliproxy/auth/token-manager.ts | 12 ++---------- src/management/oauth-port-diagnostics.ts | 12 ++---------- 4 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/cliproxy/account-manager.ts b/src/cliproxy/account-manager.ts index 069a93f7..57495035 100644 --- a/src/cliproxy/account-manager.ts +++ b/src/cliproxy/account-manager.ts @@ -12,6 +12,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { CLIProxyProvider } from './types'; +import { CLIPROXY_PROFILES } from '../auth/profile-detector'; import { getCliproxyDir, getAuthDir } from './config-generator'; import { PROVIDER_TYPE_VALUES } from './auth/auth-types'; @@ -946,7 +947,7 @@ export async function soloAccount( * Get summary of all accounts across providers */ export function getAllAccountsSummary(): Record { - const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp']; + const providers: CLIProxyProvider[] = [...CLIPROXY_PROFILES]; const summary: Record = {} as Record< CLIProxyProvider, AccountInfo[] diff --git a/src/cliproxy/auth/token-expiry-checker.ts b/src/cliproxy/auth/token-expiry-checker.ts index d3833173..9a6665ee 100644 --- a/src/cliproxy/auth/token-expiry-checker.ts +++ b/src/cliproxy/auth/token-expiry-checker.ts @@ -8,6 +8,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { CLIProxyProvider } from '../types'; +import { CLIPROXY_PROFILES } from '../../auth/profile-detector'; import { getProviderAccounts, getAccountTokenPath } from '../account-manager'; /** Preemptive refresh time: refresh tokens 45 minutes before expiry */ @@ -113,7 +114,7 @@ export function getTokenExpiryInfo( * @returns Array of token expiry info, excluding invalid tokens */ export function getAllTokenExpiryInfo(): TokenExpiryInfo[] { - const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp']; + const providers: CLIProxyProvider[] = [...CLIPROXY_PROFILES]; const results: TokenExpiryInfo[] = []; for (const provider of providers) { diff --git a/src/cliproxy/auth/token-manager.ts b/src/cliproxy/auth/token-manager.ts index 665e821d..41ef2092 100644 --- a/src/cliproxy/auth/token-manager.ts +++ b/src/cliproxy/auth/token-manager.ts @@ -8,6 +8,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { CLIProxyProvider } from '../types'; +import { CLIPROXY_PROFILES } from '../../auth/profile-detector'; import { getProviderAuthDir } from '../config-generator'; import { getProviderAccounts, getDefaultAccount } from '../account-manager'; import { @@ -145,16 +146,7 @@ export function getAuthStatus(provider: CLIProxyProvider): AuthStatus { * Get auth status for all providers */ export function getAllAuthStatus(): AuthStatus[] { - const providers: CLIProxyProvider[] = [ - 'agy', - 'claude', - 'gemini', - 'codex', - 'qwen', - 'iflow', - 'kiro', - 'ghcp', - ]; + const providers: CLIProxyProvider[] = [...CLIPROXY_PROFILES]; return providers.map(getAuthStatus); } diff --git a/src/management/oauth-port-diagnostics.ts b/src/management/oauth-port-diagnostics.ts index 806a97cc..8c261457 100644 --- a/src/management/oauth-port-diagnostics.ts +++ b/src/management/oauth-port-diagnostics.ts @@ -21,6 +21,7 @@ import { BindingTestResult, } from '../utils/port-utils'; import { CLIProxyProvider } from '../cliproxy/types'; +import { CLIPROXY_PROFILES } from '../auth/profile-detector'; /** * OAuth callback ports for each provider @@ -140,16 +141,7 @@ export async function checkOAuthPort(provider: CLIProxyProvider): Promise { - const providers: CLIProxyProvider[] = [ - 'gemini', - 'codex', - 'agy', - 'qwen', - 'iflow', - 'kiro', - 'ghcp', - 'claude', - ]; + const providers: CLIProxyProvider[] = [...CLIPROXY_PROFILES]; const results: OAuthPortDiagnostic[] = []; for (const provider of providers) { From ebc42c9457eea229bbab3691a2936dd77c961b87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Jan 2026 03:06:09 +0000 Subject: [PATCH 18/21] chore(release): 7.28.2-dev.4 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 72add80f..a98c9be4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.28.2-dev.3", + "version": "7.28.2-dev.4", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", From 5c62e06d0236b5080ccfb3ca2ff55407cbb414e1 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 22:19:19 -0500 Subject: [PATCH 19/21] fix(ui): add iFlow to PROVIDER_ASSETS + sync validation test - Add iflow.png to PROVIDER_ASSETS map - Add backend-ui-provider-arrays-sync.test.ts to catch mismatches - Addresses PR #384 review: dual source of truth + missing iFlow --- .../backend-ui-provider-arrays-sync.test.ts | 40 +++++++++++++++++++ ui/src/lib/provider-config.ts | 1 + 2 files changed, 41 insertions(+) create mode 100644 tests/unit/cliproxy/backend-ui-provider-arrays-sync.test.ts diff --git a/tests/unit/cliproxy/backend-ui-provider-arrays-sync.test.ts b/tests/unit/cliproxy/backend-ui-provider-arrays-sync.test.ts new file mode 100644 index 00000000..50f48dea --- /dev/null +++ b/tests/unit/cliproxy/backend-ui-provider-arrays-sync.test.ts @@ -0,0 +1,40 @@ +/** + * Provider Sync Test + * + * Validates that backend CLIPROXY_PROFILES and UI CLIPROXY_PROVIDERS stay in sync. + * This test catches mismatches when adding new providers. + */ + +import { describe, expect, test } from 'bun:test'; +import { CLIPROXY_PROFILES } from '../../../src/auth/profile-detector'; + +// UI providers (must manually sync - this test validates the sync) +const UI_CLIPROXY_PROVIDERS = [ + 'gemini', + 'codex', + 'agy', + 'qwen', + 'iflow', + 'kiro', + 'ghcp', + 'claude', +] as const; + +describe('Provider Sync', () => { + test('backend CLIPROXY_PROFILES matches UI CLIPROXY_PROVIDERS', () => { + const backend = [...CLIPROXY_PROFILES].sort(); + const ui = [...UI_CLIPROXY_PROVIDERS].sort(); + + expect(backend).toEqual(ui); + }); + + test('both arrays have same length', () => { + expect(CLIPROXY_PROFILES.length).toBe(UI_CLIPROXY_PROVIDERS.length); + }); + + test('UI array contains all backend providers', () => { + for (const provider of CLIPROXY_PROFILES) { + expect(UI_CLIPROXY_PROVIDERS).toContain(provider); + } + }); +}); diff --git a/ui/src/lib/provider-config.ts b/ui/src/lib/provider-config.ts index 0486b973..f0b8743e 100644 --- a/ui/src/lib/provider-config.ts +++ b/ui/src/lib/provider-config.ts @@ -35,6 +35,7 @@ export const PROVIDER_ASSETS: Record = { agy: '/assets/providers/agy.png', codex: '/assets/providers/openai.svg', qwen: '/assets/providers/qwen-color.svg', + iflow: '/assets/providers/iflow.png', kiro: '/assets/providers/kiro.png', ghcp: '/assets/providers/copilot.svg', claude: '/assets/providers/claude.svg', From c713d48d08af5044cb0fab4505365fd98e31b9d6 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 22:25:31 -0500 Subject: [PATCH 20/21] refactor(oauth): derive auth-code providers from OAUTH_FLOW_TYPES (DRY) - Update checkAuthCodePorts() to filter from CLIPROXY_PROFILES - Update header comment to list all OAuth ports including Claude - Addresses PR #384 review round 3 --- src/management/oauth-port-diagnostics.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/management/oauth-port-diagnostics.ts b/src/management/oauth-port-diagnostics.ts index 8c261457..d50c16cf 100644 --- a/src/management/oauth-port-diagnostics.ts +++ b/src/management/oauth-port-diagnostics.ts @@ -8,7 +8,11 @@ * - Gemini: 8085 * - Codex: 1455 * - Agy: 51121 + * - iFlow: 11451 + * - Kiro: 9876 + * - Claude: 54545 * - Qwen: Device Code Flow (no port needed) + * - GHCP: Device Code Flow (no port needed) */ import { @@ -156,7 +160,8 @@ export async function checkAllOAuthPorts(): Promise { * Check OAuth ports for providers that use Authorization Code flow only */ export async function checkAuthCodePorts(): Promise { - const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'iflow', 'kiro', 'claude']; + // Filter providers that use authorization_code flow (DRY: derive from OAUTH_FLOW_TYPES) + const providers = CLIPROXY_PROFILES.filter((p) => OAUTH_FLOW_TYPES[p] === 'authorization_code'); const results: OAuthPortDiagnostic[] = []; for (const provider of providers) { From 6e560b78c403c81de88fe37b3773b4147a022e1a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Jan 2026 03:26:46 +0000 Subject: [PATCH 21/21] chore(release): 7.28.2-dev.5 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a98c9be4..b4451619 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.28.2-dev.4", + "version": "7.28.2-dev.5", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli",