diff --git a/.claude/skills/ccs-delegation/SKILL.md b/.claude/skills/ccs-delegation/SKILL.md index 89462939..0842d904 100644 --- a/.claude/skills/ccs-delegation/SKILL.md +++ b/.claude/skills/ccs-delegation/SKILL.md @@ -1,6 +1,12 @@ --- name: ccs-delegation -description: Auto-activate CCS CLI delegation for deterministic tasks. Parses user input, auto-selects optimal profile (glm/kimi/custom) from ~/.ccs/config.json, enhances prompts with context, executes via `ccs {profile} -p "task"` or `ccs {profile}:continue`, and reports results. Trigger: "use ccs [task]" patterns, typo/test/refactor keywords. Excludes: complex architecture, security-critical code, performance optimization, breaking changes. +description: >- + Auto-activate CCS CLI delegation for deterministic tasks. Parses user input, + auto-selects optimal profile (glm/kimi/custom) from ~/.ccs/config.json, + enhances prompts with context, executes via `ccs {profile} -p "task"` or + `ccs {profile}:continue`, and reports results. Triggers on "use ccs [task]" + patterns, typo/test/refactor keywords. Excludes complex architecture, + security-critical code, performance optimization, breaking changes. version: 3.0.0 --- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..eb61958a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + pull_request: + branches: [main, dev] + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build package + run: bun run build + + - name: Validate (typecheck + lint + tests) + run: bun run validate diff --git a/VERSION b/VERSION index 42cdd0b5..b54e6f23 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.7.0 +5.7.0-dev.8 diff --git a/package.json b/package.json index f0d36e37..5e0bb481 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "5.7.0", + "version": "5.7.0-dev.8", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", diff --git a/src/cliproxy/auth-handler.ts b/src/cliproxy/auth-handler.ts index 311ad428..512303ce 100644 --- a/src/cliproxy/auth-handler.ts +++ b/src/cliproxy/auth-handler.ts @@ -342,6 +342,8 @@ export function getAllAuthStatus(): AuthStatus[] { /** * Clear authentication for provider + * Only removes files belonging to the specified provider (by prefix or content) + * Does NOT remove the shared auth directory or other providers' files */ export function clearAuth(provider: CLIProxyProvider): boolean { const tokenDir = getProviderTokenDir(provider); @@ -350,16 +352,33 @@ export function clearAuth(provider: CLIProxyProvider): boolean { return false; } - // Remove all files in token directory + const validPrefixes = PROVIDER_AUTH_PREFIXES[provider] || []; const files = fs.readdirSync(tokenDir); + let removedCount = 0; + + // Only remove files that belong to this provider for (const file of files) { - fs.unlinkSync(path.join(tokenDir, file)); + const filePath = path.join(tokenDir, file); + const lowerFile = file.toLowerCase(); + + // Check by prefix first (fast path) + const matchesByPrefix = validPrefixes.some((prefix) => lowerFile.startsWith(prefix)); + + // If no prefix match, check by content (for Gemini tokens without prefix) + const matchesByContent = !matchesByPrefix && isTokenFileForProvider(filePath, provider); + + if (matchesByPrefix || matchesByContent) { + try { + fs.unlinkSync(filePath); + removedCount++; + } catch { + // Failed to remove - skip + } + } } - // Remove directory - fs.rmdirSync(tokenDir); - - return true; + // DO NOT remove the shared auth directory - other providers may still have tokens + return removedCount > 0; } /** diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index 81a5c8f6..bdc6df0e 100644 --- a/src/cliproxy/binary-manager.ts +++ b/src/cliproxy/binary-manager.ts @@ -383,6 +383,7 @@ export class BinaryManager { // Download archive const archivePath = path.join(this.config.binPath, `cliproxy-archive.${platform.extension}`); + // Use single spinner and update text as we progress (avoids UI jumping) const spinner = new ProgressIndicator(`Downloading CLIProxyAPI v${this.config.version}`); spinner.start(); @@ -394,11 +395,8 @@ export class BinaryManager { throw new Error(result.error || 'Download failed after retries'); } - spinner.succeed('Download complete'); - - // Verify checksum - const verifySpinner = new ProgressIndicator('Verifying checksum'); - verifySpinner.start(); + // Verify checksum (update spinner text instead of creating new one) + spinner.update('Verifying checksum'); const checksumResult = await this.verifyChecksum( archivePath, @@ -407,7 +405,7 @@ export class BinaryManager { ); if (!checksumResult.valid) { - verifySpinner.fail('Checksum mismatch'); + spinner.fail('Checksum mismatch'); fs.unlinkSync(archivePath); throw new Error( `Checksum mismatch for ${platform.binaryName}\n` + @@ -417,15 +415,12 @@ export class BinaryManager { ); } - verifySpinner.succeed('Checksum verified'); - - // Extract archive - const extractSpinner = new ProgressIndicator('Extracting binary'); - extractSpinner.start(); + // Extract archive (update spinner text) + spinner.update('Extracting binary'); await this.extractArchive(archivePath, platform.extension); - extractSpinner.succeed('Extraction complete'); + spinner.succeed('CLIProxyAPI ready'); // Cleanup archive fs.unlinkSync(archivePath); diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index 767fdfaf..f6b5af1c 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -50,22 +50,16 @@ export const MODEL_CATALOG: Partial> = id: 'gemini-claude-opus-4-5-thinking', name: 'Claude Opus 4.5 Thinking', description: 'Most capable, extended thinking', - broken: true, - issueUrl: 'https://github.com/router-for-me/CLIProxyAPI/issues/415', }, { id: 'gemini-claude-sonnet-4-5-thinking', name: 'Claude Sonnet 4.5 Thinking', description: 'Balanced with extended thinking', - broken: true, - issueUrl: 'https://github.com/router-for-me/CLIProxyAPI/issues/415', }, { id: 'gemini-claude-sonnet-4-5', name: 'Claude Sonnet 4.5', description: 'Fast and capable', - broken: true, - issueUrl: 'https://github.com/router-for-me/CLIProxyAPI/issues/415', }, { id: 'gemini-3-pro-preview', diff --git a/src/cliproxy/model-config.ts b/src/cliproxy/model-config.ts index 0e9c9190..9ae139fb 100644 --- a/src/cliproxy/model-config.ts +++ b/src/cliproxy/model-config.ts @@ -13,6 +13,20 @@ import { getProviderSettingsPath, getClaudeEnvVars } from './config-generator'; import { CLIProxyProvider } from './types'; import { initUI, color, bold, dim, ok, info, header } from '../utils/ui'; +/** + * Check if model is a Claude model routed via Antigravity + * Claude models require MAX_THINKING_TOKENS < 8192 for thinking to work + */ +function isClaudeModel(modelId: string): boolean { + return modelId.includes('claude'); +} + +/** + * Max thinking tokens for Claude models via Antigravity + * Must be < 8192 due to Google protocol conversion limitations + */ +const CLAUDE_MAX_THINKING_TOKENS = '8191'; + /** CCS directory */ const CCS_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.ccs'); @@ -117,18 +131,56 @@ export async function configureProviderModel( defaultIndex: safeDefaultIdx, }); - // Get base env vars to preserve haiku model and base URL + // Get base env vars for defaults const baseEnv = getClaudeEnvVars(provider); - // Build settings with selected model - const settings = { - env: { - ...baseEnv, - ANTHROPIC_MODEL: selectedModel, - ANTHROPIC_DEFAULT_OPUS_MODEL: selectedModel, - ANTHROPIC_DEFAULT_SONNET_MODEL: selectedModel, - // Keep haiku as-is from base config (usually flash model) - }, + // Read existing settings to preserve user customizations + let existingSettings: Record = {}; + let existingEnv: Record = {}; + if (fs.existsSync(settingsPath)) { + try { + existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + existingEnv = (existingSettings.env as Record) || {}; + } catch { + // Invalid JSON - start fresh + } + } + + // Build settings with selective merge: + // - Preserve ALL user settings (top-level and env vars) + // - Only update CCS-controlled fields (model selection + thinking toggle for Claude) + const isClaude = isClaudeModel(selectedModel); + + // CCS-controlled env vars (always override with our values) + const ccsControlledEnv: Record = { + ANTHROPIC_BASE_URL: baseEnv.ANTHROPIC_BASE_URL || '', + ANTHROPIC_AUTH_TOKEN: baseEnv.ANTHROPIC_AUTH_TOKEN || '', + ANTHROPIC_MODEL: selectedModel, + ANTHROPIC_DEFAULT_OPUS_MODEL: selectedModel, + ANTHROPIC_DEFAULT_SONNET_MODEL: selectedModel, + ANTHROPIC_DEFAULT_HAIKU_MODEL: baseEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL || '', + }; + + // Claude models require MAX_THINKING_TOKENS < 8192 for thinking to work + if (isClaude) { + ccsControlledEnv.MAX_THINKING_TOKENS = CLAUDE_MAX_THINKING_TOKENS; + } + + // Merge: user env vars (preserved) + CCS controlled (override) + const mergedEnv = { + ...existingEnv, + ...ccsControlledEnv, + }; + + // Remove MAX_THINKING_TOKENS when switching away from Claude model + if (!isClaude && mergedEnv.MAX_THINKING_TOKENS) { + delete mergedEnv.MAX_THINKING_TOKENS; + } + + // Build final settings: preserve user top-level settings + update env + const settings: Record = { + ...existingSettings, + env: mergedEnv, }; // Ensure CCS directory exists @@ -146,6 +198,15 @@ export async function configureProviderModel( console.error(''); console.error(ok(`Model set to: ${bold(displayName)}`)); console.error(dim(` Config saved: ${settingsPath}`)); + + // Show info for Claude models about thinking token limit + if (isClaude) { + console.error(''); + console.error( + info(`MAX_THINKING_TOKENS set to ${CLAUDE_MAX_THINKING_TOKENS} (required < 8192)`) + ); + console.error(dim(' Google protocol conversion requires this limit for thinking to work.')); + } console.error(''); return true; diff --git a/src/delegation/delegation-handler.ts b/src/delegation/delegation-handler.ts index da245f6a..868b39cf 100644 --- a/src/delegation/delegation-handler.ts +++ b/src/delegation/delegation-handler.ts @@ -16,6 +16,7 @@ interface ParsedArgs { timeout?: number; resumeSession?: boolean; sessionId?: string; + extraArgs?: string[]; // Passthrough args for Claude CLI }; } @@ -185,6 +186,39 @@ export class DelegationHandler { options.timeout = parseInt(args[timeoutIndex + 1], 10); } + // Collect extra args to pass through to Claude CLI + // CCS-handled flags with values (skip these and their values): + const ccsFlagsWithValue = new Set(['-p', '--prompt', '--timeout', '--permission-mode']); + const extraArgs: string[] = []; + const profile = this._extractProfile(args); + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + // Skip profile name (non-flag first arg) + if (arg === profile && !arg.startsWith('-')) continue; + + // Skip CCS-handled flags and their values + if (ccsFlagsWithValue.has(arg)) { + i++; // Skip next arg (the value) + continue; + } + + // Collect flags and their values as passthrough + if (arg.startsWith('-')) { + extraArgs.push(arg); + // If next arg exists and doesn't start with '-', it's likely a value + if (i + 1 < args.length && !args[i + 1].startsWith('-')) { + extraArgs.push(args[i + 1]); + i++; // Skip the value we just added + } + } + } + + if (extraArgs.length > 0) { + options.extraArgs = extraArgs; + } + return options; } diff --git a/src/delegation/headless-executor.ts b/src/delegation/headless-executor.ts index a9d723d2..0416f470 100644 --- a/src/delegation/headless-executor.ts +++ b/src/delegation/headless-executor.ts @@ -45,6 +45,7 @@ interface ExecutionOptions { resumeSession?: boolean; sessionId?: string; maxRetries?: number; + extraArgs?: string[]; // Passthrough args for Claude CLI } interface ExecutionResult { @@ -112,6 +113,7 @@ export class HeadlessExecutor { permissionMode = 'acceptEdits', resumeSession = false, sessionId = null, + extraArgs = [], } = options; // Validate permission mode @@ -210,6 +212,11 @@ export class HeadlessExecutor { // Note: No max-turns limit - using time-based limits instead (default 10min timeout) + // Passthrough extra args (from Claude CLI flags like --agent, --system-prompt-file, etc.) + if (extraArgs.length > 0) { + args.push(...extraArgs); + } + // Debug log args if (process.env.CCS_DEBUG) { console.error(`[i] Claude CLI args: ${args.join(' ')}`);