Skip to content

Commit b8d98a6

Browse files
authored
Merge pull request #59 from kaitranntt/dev
feat(release): v5.7.0 - claude thinking support via antigravity
2 parents ff3599d + 165c43a commit b8d98a6

File tree

10 files changed

+185
-37
lines changed

10 files changed

+185
-37
lines changed

.claude/skills/ccs-delegation/SKILL.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
---
22
name: ccs-delegation
3-
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.
3+
description: >-
4+
Auto-activate CCS CLI delegation for deterministic tasks. Parses user input,
5+
auto-selects optimal profile (glm/kimi/custom) from ~/.ccs/config.json,
6+
enhances prompts with context, executes via `ccs {profile} -p "task"` or
7+
`ccs {profile}:continue`, and reports results. Triggers on "use ccs [task]"
8+
patterns, typo/test/refactor keywords. Excludes complex architecture,
9+
security-critical code, performance optimization, breaking changes.
410
version: 3.0.0
511
---
612

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches: [main, dev]
6+
7+
jobs:
8+
validate:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Checkout code
13+
uses: actions/checkout@v4
14+
15+
- name: Setup Bun
16+
uses: oven-sh/setup-bun@v2
17+
with:
18+
bun-version: latest
19+
20+
- name: Setup Node.js
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: '22'
24+
25+
- name: Install dependencies
26+
run: bun install --frozen-lockfile
27+
28+
- name: Build package
29+
run: bun run build
30+
31+
- name: Validate (typecheck + lint + tests)
32+
run: bun run validate

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5.7.0
1+
5.7.0-dev.8

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kaitranntt/ccs",
3-
"version": "5.7.0",
3+
"version": "5.7.0-dev.8",
44
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
55
"keywords": [
66
"cli",

src/cliproxy/auth-handler.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,8 @@ export function getAllAuthStatus(): AuthStatus[] {
342342

343343
/**
344344
* Clear authentication for provider
345+
* Only removes files belonging to the specified provider (by prefix or content)
346+
* Does NOT remove the shared auth directory or other providers' files
345347
*/
346348
export function clearAuth(provider: CLIProxyProvider): boolean {
347349
const tokenDir = getProviderTokenDir(provider);
@@ -350,16 +352,33 @@ export function clearAuth(provider: CLIProxyProvider): boolean {
350352
return false;
351353
}
352354

353-
// Remove all files in token directory
355+
const validPrefixes = PROVIDER_AUTH_PREFIXES[provider] || [];
354356
const files = fs.readdirSync(tokenDir);
357+
let removedCount = 0;
358+
359+
// Only remove files that belong to this provider
355360
for (const file of files) {
356-
fs.unlinkSync(path.join(tokenDir, file));
357-
}
361+
const filePath = path.join(tokenDir, file);
362+
const lowerFile = file.toLowerCase();
363+
364+
// Check by prefix first (fast path)
365+
const matchesByPrefix = validPrefixes.some((prefix) => lowerFile.startsWith(prefix));
366+
367+
// If no prefix match, check by content (for Gemini tokens without prefix)
368+
const matchesByContent = !matchesByPrefix && isTokenFileForProvider(filePath, provider);
358369

359-
// Remove directory
360-
fs.rmdirSync(tokenDir);
370+
if (matchesByPrefix || matchesByContent) {
371+
try {
372+
fs.unlinkSync(filePath);
373+
removedCount++;
374+
} catch {
375+
// Failed to remove - skip
376+
}
377+
}
378+
}
361379

362-
return true;
380+
// DO NOT remove the shared auth directory - other providers may still have tokens
381+
return removedCount > 0;
363382
}
364383

365384
/**

src/cliproxy/binary-manager.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ export class BinaryManager {
383383
// Download archive
384384
const archivePath = path.join(this.config.binPath, `cliproxy-archive.${platform.extension}`);
385385

386+
// Use single spinner and update text as we progress (avoids UI jumping)
386387
const spinner = new ProgressIndicator(`Downloading CLIProxyAPI v${this.config.version}`);
387388
spinner.start();
388389

@@ -394,11 +395,8 @@ export class BinaryManager {
394395
throw new Error(result.error || 'Download failed after retries');
395396
}
396397

397-
spinner.succeed('Download complete');
398-
399-
// Verify checksum
400-
const verifySpinner = new ProgressIndicator('Verifying checksum');
401-
verifySpinner.start();
398+
// Verify checksum (update spinner text instead of creating new one)
399+
spinner.update('Verifying checksum');
402400

403401
const checksumResult = await this.verifyChecksum(
404402
archivePath,
@@ -407,7 +405,7 @@ export class BinaryManager {
407405
);
408406

409407
if (!checksumResult.valid) {
410-
verifySpinner.fail('Checksum mismatch');
408+
spinner.fail('Checksum mismatch');
411409
fs.unlinkSync(archivePath);
412410
throw new Error(
413411
`Checksum mismatch for ${platform.binaryName}\n` +
@@ -417,15 +415,12 @@ export class BinaryManager {
417415
);
418416
}
419417

420-
verifySpinner.succeed('Checksum verified');
421-
422-
// Extract archive
423-
const extractSpinner = new ProgressIndicator('Extracting binary');
424-
extractSpinner.start();
418+
// Extract archive (update spinner text)
419+
spinner.update('Extracting binary');
425420

426421
await this.extractArchive(archivePath, platform.extension);
427422

428-
extractSpinner.succeed('Extraction complete');
423+
spinner.succeed('CLIProxyAPI ready');
429424

430425
// Cleanup archive
431426
fs.unlinkSync(archivePath);

src/cliproxy/model-catalog.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,16 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
5050
id: 'gemini-claude-opus-4-5-thinking',
5151
name: 'Claude Opus 4.5 Thinking',
5252
description: 'Most capable, extended thinking',
53-
broken: true,
54-
issueUrl: 'https://github.com/router-for-me/CLIProxyAPI/issues/415',
5553
},
5654
{
5755
id: 'gemini-claude-sonnet-4-5-thinking',
5856
name: 'Claude Sonnet 4.5 Thinking',
5957
description: 'Balanced with extended thinking',
60-
broken: true,
61-
issueUrl: 'https://github.com/router-for-me/CLIProxyAPI/issues/415',
6258
},
6359
{
6460
id: 'gemini-claude-sonnet-4-5',
6561
name: 'Claude Sonnet 4.5',
6662
description: 'Fast and capable',
67-
broken: true,
68-
issueUrl: 'https://github.com/router-for-me/CLIProxyAPI/issues/415',
6963
},
7064
{
7165
id: 'gemini-3-pro-preview',

src/cliproxy/model-config.ts

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ import { getProviderSettingsPath, getClaudeEnvVars } from './config-generator';
1313
import { CLIProxyProvider } from './types';
1414
import { initUI, color, bold, dim, ok, info, header } from '../utils/ui';
1515

16+
/**
17+
* Check if model is a Claude model routed via Antigravity
18+
* Claude models require MAX_THINKING_TOKENS < 8192 for thinking to work
19+
*/
20+
function isClaudeModel(modelId: string): boolean {
21+
return modelId.includes('claude');
22+
}
23+
24+
/**
25+
* Max thinking tokens for Claude models via Antigravity
26+
* Must be < 8192 due to Google protocol conversion limitations
27+
*/
28+
const CLAUDE_MAX_THINKING_TOKENS = '8191';
29+
1630
/** CCS directory */
1731
const CCS_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.ccs');
1832

@@ -117,18 +131,56 @@ export async function configureProviderModel(
117131
defaultIndex: safeDefaultIdx,
118132
});
119133

120-
// Get base env vars to preserve haiku model and base URL
134+
// Get base env vars for defaults
121135
const baseEnv = getClaudeEnvVars(provider);
122136

123-
// Build settings with selected model
124-
const settings = {
125-
env: {
126-
...baseEnv,
127-
ANTHROPIC_MODEL: selectedModel,
128-
ANTHROPIC_DEFAULT_OPUS_MODEL: selectedModel,
129-
ANTHROPIC_DEFAULT_SONNET_MODEL: selectedModel,
130-
// Keep haiku as-is from base config (usually flash model)
131-
},
137+
// Read existing settings to preserve user customizations
138+
let existingSettings: Record<string, unknown> = {};
139+
let existingEnv: Record<string, string> = {};
140+
if (fs.existsSync(settingsPath)) {
141+
try {
142+
existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
143+
existingEnv = (existingSettings.env as Record<string, string>) || {};
144+
} catch {
145+
// Invalid JSON - start fresh
146+
}
147+
}
148+
149+
// Build settings with selective merge:
150+
// - Preserve ALL user settings (top-level and env vars)
151+
// - Only update CCS-controlled fields (model selection + thinking toggle for Claude)
152+
const isClaude = isClaudeModel(selectedModel);
153+
154+
// CCS-controlled env vars (always override with our values)
155+
const ccsControlledEnv: Record<string, string> = {
156+
ANTHROPIC_BASE_URL: baseEnv.ANTHROPIC_BASE_URL || '',
157+
ANTHROPIC_AUTH_TOKEN: baseEnv.ANTHROPIC_AUTH_TOKEN || '',
158+
ANTHROPIC_MODEL: selectedModel,
159+
ANTHROPIC_DEFAULT_OPUS_MODEL: selectedModel,
160+
ANTHROPIC_DEFAULT_SONNET_MODEL: selectedModel,
161+
ANTHROPIC_DEFAULT_HAIKU_MODEL: baseEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL || '',
162+
};
163+
164+
// Claude models require MAX_THINKING_TOKENS < 8192 for thinking to work
165+
if (isClaude) {
166+
ccsControlledEnv.MAX_THINKING_TOKENS = CLAUDE_MAX_THINKING_TOKENS;
167+
}
168+
169+
// Merge: user env vars (preserved) + CCS controlled (override)
170+
const mergedEnv = {
171+
...existingEnv,
172+
...ccsControlledEnv,
173+
};
174+
175+
// Remove MAX_THINKING_TOKENS when switching away from Claude model
176+
if (!isClaude && mergedEnv.MAX_THINKING_TOKENS) {
177+
delete mergedEnv.MAX_THINKING_TOKENS;
178+
}
179+
180+
// Build final settings: preserve user top-level settings + update env
181+
const settings: Record<string, unknown> = {
182+
...existingSettings,
183+
env: mergedEnv,
132184
};
133185

134186
// Ensure CCS directory exists
@@ -146,6 +198,15 @@ export async function configureProviderModel(
146198
console.error('');
147199
console.error(ok(`Model set to: ${bold(displayName)}`));
148200
console.error(dim(` Config saved: ${settingsPath}`));
201+
202+
// Show info for Claude models about thinking token limit
203+
if (isClaude) {
204+
console.error('');
205+
console.error(
206+
info(`MAX_THINKING_TOKENS set to ${CLAUDE_MAX_THINKING_TOKENS} (required < 8192)`)
207+
);
208+
console.error(dim(' Google protocol conversion requires this limit for thinking to work.'));
209+
}
149210
console.error('');
150211

151212
return true;

src/delegation/delegation-handler.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface ParsedArgs {
1616
timeout?: number;
1717
resumeSession?: boolean;
1818
sessionId?: string;
19+
extraArgs?: string[]; // Passthrough args for Claude CLI
1920
};
2021
}
2122

@@ -185,6 +186,39 @@ export class DelegationHandler {
185186
options.timeout = parseInt(args[timeoutIndex + 1], 10);
186187
}
187188

189+
// Collect extra args to pass through to Claude CLI
190+
// CCS-handled flags with values (skip these and their values):
191+
const ccsFlagsWithValue = new Set(['-p', '--prompt', '--timeout', '--permission-mode']);
192+
const extraArgs: string[] = [];
193+
const profile = this._extractProfile(args);
194+
195+
for (let i = 0; i < args.length; i++) {
196+
const arg = args[i];
197+
198+
// Skip profile name (non-flag first arg)
199+
if (arg === profile && !arg.startsWith('-')) continue;
200+
201+
// Skip CCS-handled flags and their values
202+
if (ccsFlagsWithValue.has(arg)) {
203+
i++; // Skip next arg (the value)
204+
continue;
205+
}
206+
207+
// Collect flags and their values as passthrough
208+
if (arg.startsWith('-')) {
209+
extraArgs.push(arg);
210+
// If next arg exists and doesn't start with '-', it's likely a value
211+
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
212+
extraArgs.push(args[i + 1]);
213+
i++; // Skip the value we just added
214+
}
215+
}
216+
}
217+
218+
if (extraArgs.length > 0) {
219+
options.extraArgs = extraArgs;
220+
}
221+
188222
return options;
189223
}
190224

src/delegation/headless-executor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ interface ExecutionOptions {
4545
resumeSession?: boolean;
4646
sessionId?: string;
4747
maxRetries?: number;
48+
extraArgs?: string[]; // Passthrough args for Claude CLI
4849
}
4950

5051
interface ExecutionResult {
@@ -112,6 +113,7 @@ export class HeadlessExecutor {
112113
permissionMode = 'acceptEdits',
113114
resumeSession = false,
114115
sessionId = null,
116+
extraArgs = [],
115117
} = options;
116118

117119
// Validate permission mode
@@ -210,6 +212,11 @@ export class HeadlessExecutor {
210212

211213
// Note: No max-turns limit - using time-based limits instead (default 10min timeout)
212214

215+
// Passthrough extra args (from Claude CLI flags like --agent, --system-prompt-file, etc.)
216+
if (extraArgs.length > 0) {
217+
args.push(...extraArgs);
218+
}
219+
213220
// Debug log args
214221
if (process.env.CCS_DEBUG) {
215222
console.error(`[i] Claude CLI args: ${args.join(' ')}`);

0 commit comments

Comments
 (0)