From d089ab06c2c007e4a46173efcac9ea9ff23f0bb5 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 13:46:33 -0400 Subject: [PATCH 01/12] refactor(errors): add RetryableError and retry-strategy utility Extract retryable error class and reusable withRetry wrapper from scattered retry logic in glmt-proxy and binary/downloader. --- src/errors/__tests__/error-types.test.ts | 63 ++++++++ src/errors/error-types.ts | 15 ++ src/errors/index.ts | 1 + src/utils/__tests__/retry-strategy.test.ts | 161 +++++++++++++++++++++ src/utils/retry-strategy.ts | 117 +++++++++++++++ 5 files changed, 357 insertions(+) create mode 100644 src/errors/__tests__/error-types.test.ts create mode 100644 src/utils/__tests__/retry-strategy.test.ts create mode 100644 src/utils/retry-strategy.ts diff --git a/src/errors/__tests__/error-types.test.ts b/src/errors/__tests__/error-types.test.ts new file mode 100644 index 00000000..0a28b240 --- /dev/null +++ b/src/errors/__tests__/error-types.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from 'bun:test'; +import { CCSError, RetryableError, isCCSError, isRecoverableError } from '../error-types'; +import { ExitCode } from '../exit-codes'; + +describe('RetryableError', () => { + it('extends CCSError', () => { + const err = new RetryableError('test'); + expect(err).toBeInstanceOf(CCSError); + expect(err).toBeInstanceOf(RetryableError); + }); + + it('sets name to RetryableError', () => { + const err = new RetryableError('test'); + expect(err.name).toBe('RetryableError'); + }); + + it('sets recoverable to true', () => { + const err = new RetryableError('test'); + expect(err.recoverable).toBe(true); + }); + + it('defaults exit code to GENERAL_ERROR', () => { + const err = new RetryableError('test'); + expect(err.code).toBe(ExitCode.GENERAL_ERROR); + }); + + it('passes message through', () => { + const err = new RetryableError('something went wrong'); + expect(err.message).toBe('something went wrong'); + }); + + it('accepts an optional cause', () => { + const cause = new Error('original'); + const err = new RetryableError('wrapped', cause); + expect(err.cause).toBe(cause); + }); + + it('accepts an optional retryAfter (ms)', () => { + const err = new RetryableError('rate limited', undefined, 5000); + expect(err.retryAfter).toBe(5000); + }); + + it('defaults retryAfter to undefined', () => { + const err = new RetryableError('test'); + expect(err.retryAfter).toBeUndefined(); + }); + + it('is identified by isCCSError', () => { + const err = new RetryableError('test'); + expect(isCCSError(err)).toBe(true); + }); + + it('is identified as recoverable by isRecoverableError', () => { + const err = new RetryableError('test'); + expect(isRecoverableError(err)).toBe(true); + }); + + it('has a proper stack trace', () => { + const err = new RetryableError('test'); + expect(err.stack).toBeDefined(); + expect(err.stack).toContain('RetryableError'); + }); +}); diff --git a/src/errors/error-types.ts b/src/errors/error-types.ts index 08424dfe..c0b8a9b2 100644 --- a/src/errors/error-types.ts +++ b/src/errors/error-types.ts @@ -169,6 +169,21 @@ export class ValidationError extends CCSError { } } +/** + * Retryable/transient error + * Signals that the operation may succeed on retry (e.g. rate limits, timeouts) + */ +export class RetryableError extends CCSError { + constructor( + message: string, + public readonly cause?: Error, + public readonly retryAfter?: number // ms until next attempt + ) { + super(message, ExitCode.GENERAL_ERROR, true); + this.name = 'RetryableError'; + } +} + /** * Type guard to check if an error is a CCSError */ diff --git a/src/errors/index.ts b/src/errors/index.ts index 40d2cd4b..45e89b7d 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -37,6 +37,7 @@ export { ProxyError, MigrationError, UserAbortError, + RetryableError, isCCSError, isRecoverableError, } from './error-types'; diff --git a/src/utils/__tests__/retry-strategy.test.ts b/src/utils/__tests__/retry-strategy.test.ts new file mode 100644 index 00000000..e22693ef --- /dev/null +++ b/src/utils/__tests__/retry-strategy.test.ts @@ -0,0 +1,161 @@ +import { describe, it, expect, mock, spyOn } from 'bun:test'; +import { withRetry, type RetryOptions } from '../retry-strategy'; +import { RetryableError } from '../../errors/error-types'; + +describe('withRetry', () => { + it('returns the result on first success', async () => { + const fn = mock(() => Promise.resolve(42)); + const result = await withRetry(fn, { maxRetries: 3, baseDelayMs: 10 }); + expect(result).toBe(42); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('retries on RetryableError and succeeds', async () => { + let attempt = 0; + const fn = mock(() => { + attempt++; + if (attempt < 3) { + return Promise.reject(new RetryableError('transient failure')); + } + return Promise.resolve('ok'); + }); + + const result = await withRetry(fn, { maxRetries: 5, baseDelayMs: 1 }); + expect(result).toBe('ok'); + expect(fn).toHaveBeenCalledTimes(3); + }); + + it('throws after max retries exhausted', async () => { + const fn = mock(() => Promise.reject(new RetryableError('always fails'))); + await expect(withRetry(fn, { maxRetries: 2, baseDelayMs: 1 })).rejects.toThrow('always fails'); + // 1 initial + 2 retries = 3 total calls + expect(fn).toHaveBeenCalledTimes(3); + }); + + it('does not retry non-retryable errors', async () => { + const fn = mock(() => Promise.reject(new Error('fatal'))); + await expect(withRetry(fn, { maxRetries: 3, baseDelayMs: 1 })).rejects.toThrow('fatal'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('does not retry errors with recoverable=false', async () => { + const fn = mock(() => Promise.reject(new Error('non-retryable'))); + await expect(withRetry(fn, { maxRetries: 3, baseDelayMs: 1 })).rejects.toThrow('non-retryable'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('uses custom retryableCheck when provided', async () => { + let attempt = 0; + const fn = mock(() => { + attempt++; + if (attempt < 2) { + return Promise.reject(new Error('custom-retry')); + } + return Promise.resolve('recovered'); + }); + + const customCheck = (error: unknown) => + error instanceof Error && error.message === 'custom-retry'; + + const result = await withRetry(fn, { + maxRetries: 5, + baseDelayMs: 1, + retryableCheck: customCheck, + }); + expect(result).toBe('recovered'); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('calls onRetry callback on each retry attempt', async () => { + const onRetry = mock(() => {}); + const fn = mock(() => Promise.reject(new RetryableError('fail'))); + + await expect(withRetry(fn, { maxRetries: 3, baseDelayMs: 1, onRetry })).rejects.toThrow('fail'); + + // 1 initial + 3 retries = 3 onRetry calls (not called for initial) + expect(onRetry).toHaveBeenCalledTimes(3); + // First retry call + expect(onRetry.mock.calls[0][1]).toBe(1); + }); + + it('respects maxDelayMs cap', async () => { + const sleepSpy = spyOn(globalThis, 'setTimeout'); + let attempt = 0; + const fn = mock(() => { + attempt++; + if (attempt <= 2) { + return Promise.reject(new RetryableError('fail')); + } + return Promise.resolve('ok'); + }); + + await withRetry(fn, { + maxRetries: 5, + baseDelayMs: 1000, + maxDelayMs: 200, + }); + + // Verify setTimeout was called with delay <= maxDelayMs (200ms) + jitter buffer + // Jitter adds 0-20% of delay, so max possible is 240ms + for (const call of sleepSpy.mock.calls) { + const delay = call[1] as number; + expect(delay).toBeLessThanOrEqual(250); // allow small jitter overhead + } + sleepSpy.mockRestore(); + }); + + it('uses default backoffMultiplier when not specified', async () => { + let attempt = 0; + const fn = mock(() => { + attempt++; + if (attempt < 2) { + return Promise.reject(new RetryableError('fail')); + } + return Promise.resolve('ok'); + }); + + // Should not throw - defaults to multiplier of 2 + await expect(withRetry(fn, { maxRetries: 3, baseDelayMs: 1 })).resolves.toBe('ok'); + }); + + it('applies exponential backoff with custom multiplier', async () => { + const sleepSpy = spyOn(globalThis, 'setTimeout'); + let attempt = 0; + const fn = mock(() => { + attempt++; + if (attempt <= 3) { + return Promise.reject(new RetryableError('fail')); + } + return Promise.resolve('ok'); + }); + + await withRetry(fn, { + maxRetries: 5, + baseDelayMs: 10, + backoffMultiplier: 3, + }); + + // Delays should grow: ~10, ~30, ~90 (with jitter) + const delays = sleepSpy.mock.calls.map((call) => call[1] as number); + // First delay should be close to base * multiplier^0 = 10 + expect(delays[0]).toBeGreaterThan(5); + expect(delays[0]).toBeLessThan(25); // 10 + jitter + // Second delay should be close to base * multiplier^1 = 30 + expect(delays[1]).toBeGreaterThan(20); + expect(delays[1]).toBeLessThan(50); // 30 + jitter + + sleepSpy.mockRestore(); + }); + + it('passes through errors that are not Error instances', async () => { + const fn = mock(() => Promise.reject('string error')); + await expect(withRetry(fn, { maxRetries: 3, baseDelayMs: 1 })).rejects.toBe('string error'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('works with maxRetries of 0 (no retries)', async () => { + const fn = mock(() => Promise.reject(new RetryableError('fail'))); + await expect(withRetry(fn, { maxRetries: 0, baseDelayMs: 1 })).rejects.toThrow('fail'); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/utils/retry-strategy.ts b/src/utils/retry-strategy.ts new file mode 100644 index 00000000..988f9216 --- /dev/null +++ b/src/utils/retry-strategy.ts @@ -0,0 +1,117 @@ +/** + * Retry Strategy Utility + * + * Reusable exponential-backoff retry wrapper extracted from + * scattered retry logic in glmt-proxy and binary/downloader. + * + * Usage: + * const data = await withRetry(() => fetch(url), { maxRetries: 3, baseDelayMs: 100 }); + */ + +import { RetryableError, isRecoverableError } from '../errors/error-types'; + +/** Configuration options for retry behavior */ +export interface RetryOptions { + /** Maximum number of retry attempts (default: 3) */ + maxRetries: number; + /** Base delay in ms for the first retry (default: 1000) */ + baseDelayMs: number; + /** Upper bound for the computed delay (default: 30000) */ + maxDelayMs?: number; + /** Multiplier applied per attempt (default: 2) */ + backoffMultiplier?: number; + /** Override the default retryability check */ + retryableCheck?: (error: unknown) => boolean; + /** Callback fired before each retry (not fired on initial call) */ + onRetry?: (error: Error, attempt: number) => void; +} + +const DEFAULT_MAX_DELAY_MS = 30_000; +const DEFAULT_MULTIPLIER = 2; +const JITTER_RATIO = 0.2; // 20% of delay as random jitter + +/** + * Check whether an unknown thrown value is retryable. + * Uses CCSError.recoverable flag and RetryableError instance check. + */ +function defaultRetryableCheck(error: unknown): boolean { + if (error instanceof RetryableError) { + return true; + } + if (isRecoverableError(error)) { + return true; + } + return false; +} + +/** + * Compute backoff delay: base * multiplier^attempt + jitter, capped at maxDelay. + */ +function computeDelay( + attempt: number, + baseDelayMs: number, + maxDelayMs: number, + multiplier: number +): number { + const exponentialDelay = Math.min(baseDelayMs * Math.pow(multiplier, attempt), maxDelayMs); + const jitter = exponentialDelay * JITTER_RATIO * Math.random(); + return exponentialDelay + jitter; +} + +/** + * Sleep for the specified number of milliseconds. + */ +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Execute `fn` with automatic retries on retryable failures. + * + * Retryability defaults to checking for `RetryableError` instances + * and CCSError with `recoverable === true`. Override with `retryableCheck`. + * + * @param fn - The async function to execute + * @param options - Retry configuration + * @returns The resolved value from `fn` + * @throws The last error encountered after exhausting retries + */ +export async function withRetry(fn: () => Promise, options: RetryOptions): Promise { + const { + maxRetries, + baseDelayMs, + maxDelayMs = DEFAULT_MAX_DELAY_MS, + backoffMultiplier = DEFAULT_MULTIPLIER, + retryableCheck = defaultRetryableCheck, + onRetry, + } = options; + + const isRetryable = retryableCheck ?? defaultRetryableCheck; + let lastError: unknown; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error; + + // No more retries left + if (attempt >= maxRetries) { + break; + } + + // Check retryability + if (!isRetryable(error)) { + break; + } + + const err = error instanceof Error ? error : new Error(String(error)); + onRetry?.(err, attempt + 1); + + const delay = computeDelay(attempt, baseDelayMs, maxDelayMs, backoffMultiplier); + await sleep(delay); + } + } + + throw lastError; +} From 51df0ee55b52ecc858544ec8d8ff7e7932db0f11 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 13:54:40 -0400 Subject: [PATCH 02/12] refactor(config): reorganize unified-config-types into schemas directory Split the 1,128-line unified-config-types.ts into focused schema modules under src/config/schemas/ for maintainability. Each file is under 200 LOC. New schema files: - version.ts: UNIFIED_CONFIG_VERSION constant - auth.ts: AccountConfig, ProfileConfig, OAuthAccounts, CLIProxyAuthConfig, etc. - cliproxy.ts: CLIProxyConfig, CompositeTierConfig, routing/safety types - copilot-cursor.ts: CopilotConfig, CursorConfig + defaults - proxy-server.ts: CliproxyServerConfig, GlobalEnvConfig, ImageAnalysisConfig - quota.ts: QuotaManagementConfig + all quota types and defaults - thinking.ts: ThinkingConfig + tier defaults - channels.ts: OfficialChannelsConfig (Telegram, Discord, iMessage) - websearch.ts: All WebSearch backend types (DuckDuckGo, Brave, Exa, etc.) - browser.ts: BrowserConfig, BrowserClaudeConfig, BrowserCodexConfig - logging.ts: LoggingConfig, PreferencesConfig - unified-config.ts: UnifiedConfig interface, factory, type guard - index.ts: Barrel re-export of all schema modules unified-config-types.ts is now a thin backward-compatible barrel that re-exports everything from schemas/index. All 67 existing imports across the codebase continue to resolve unchanged. --- .../__tests__/schemas-reexport.test.ts | 198 +++ src/config/schemas/auth.ts | 109 ++ src/config/schemas/browser.ts | 71 ++ src/config/schemas/channels.ts | 32 + src/config/schemas/cliproxy.ts | 151 +++ src/config/schemas/copilot-cursor.ts | 93 ++ src/config/schemas/index.ts | 112 ++ src/config/schemas/logging.ts | 53 + src/config/schemas/providers.ts | 30 + src/config/schemas/proxy-server.ts | 193 +++ src/config/schemas/quota.ts | 121 ++ src/config/schemas/thinking.ts | 66 + src/config/schemas/unified-config.ts | 200 +++ src/config/schemas/version.ts | 23 + src/config/schemas/websearch.ts | 148 +++ src/config/unified-config-types.ts | 1120 +---------------- 16 files changed, 1603 insertions(+), 1117 deletions(-) create mode 100644 src/config/schemas/__tests__/schemas-reexport.test.ts create mode 100644 src/config/schemas/auth.ts create mode 100644 src/config/schemas/browser.ts create mode 100644 src/config/schemas/channels.ts create mode 100644 src/config/schemas/cliproxy.ts create mode 100644 src/config/schemas/copilot-cursor.ts create mode 100644 src/config/schemas/index.ts create mode 100644 src/config/schemas/logging.ts create mode 100644 src/config/schemas/providers.ts create mode 100644 src/config/schemas/proxy-server.ts create mode 100644 src/config/schemas/quota.ts create mode 100644 src/config/schemas/thinking.ts create mode 100644 src/config/schemas/unified-config.ts create mode 100644 src/config/schemas/version.ts create mode 100644 src/config/schemas/websearch.ts diff --git a/src/config/schemas/__tests__/schemas-reexport.test.ts b/src/config/schemas/__tests__/schemas-reexport.test.ts new file mode 100644 index 00000000..5bdda5d9 --- /dev/null +++ b/src/config/schemas/__tests__/schemas-reexport.test.ts @@ -0,0 +1,198 @@ +/** + * Tests: config schemas re-export backward compatibility. + * + * Verifies that every type, interface, constant, and function originally + * exported from unified-config-types.ts is still accessible via both + * the barrel file and the schemas/index barrel. + */ + +import { describe, it, expect } from 'bun:test'; + +// Import from the backward-compatible barrel (this is what all existing code uses) +import * as barrel from '../../unified-config-types'; + +// Import from the new schemas barrel (this is what the barrel delegates to) +import * as schemas from '../index'; + +// --------------------------------------------------------------------------- +// Type-level checks (compile-time, not runtime) +// --------------------------------------------------------------------------- + +// Verify key interfaces are accessible as types +import type { + UnifiedConfig, + AccountConfig, + ProfileConfig, + OAuthAccounts, + CLIProxyAuthConfig, + TokenRefreshSettings, + DashboardAuthConfig, + CLIProxyVariantConfig, + CompositeTierConfig, + CompositeVariantConfig, + CLIProxyLoggingConfig, + CLIProxySafetyConfig, + CLIProxyRoutingConfig, + CLIProxyConfig, + AutoQuotaConfig, + RuntimeMonitorConfig, + ManualQuotaConfig, + QuotaManagementMode, + QuotaManagementConfig, + ThinkingMode, + ThinkingTierDefaults, + ThinkingConfig, + OfficialChannelId, + OfficialChannelsConfig, + DuckDuckGoWebSearchConfig, + BraveWebSearchConfig, + ExaWebSearchConfig, + TavilyWebSearchConfig, + SearxngWebSearchConfig, + GeminiWebSearchConfig, + GrokWebSearchConfig, + OpenCodeWebSearchConfig, + WebSearchProvidersConfig, + WebSearchConfig, + BrowserToolPolicy, + BrowserEvalMode, + BrowserClaudeConfig, + BrowserCodexConfig, + BrowserConfig, + LoggingLevel, + LoggingConfig, + PreferencesConfig, + CopilotAccountType, + CopilotConfig, + CursorConfig, + ProxyRemoteConfig, + ProxyFallbackConfig, + ProxyLocalConfig, + OpenAICompatProxyRoutingConfig, + OpenAICompatProxyConfig, + CliproxyServerConfig, + GlobalEnvConfig, + ContinuityConfig, + ImageAnalysisConfig, +} from '../../unified-config-types'; + +describe('config schemas backward compatibility', () => { + // ------------------------------------------------------------------------- + // Constants + // ------------------------------------------------------------------------- + it('re-exports UNIFIED_CONFIG_VERSION', () => { + expect(barrel.UNIFIED_CONFIG_VERSION).toBe(13); + expect(schemas.UNIFIED_CONFIG_VERSION).toBe(13); + }); + + it('re-exports CLIPROXY_SUPPORTED_PROVIDERS', () => { + expect(Array.isArray(barrel.CLIPROXY_SUPPORTED_PROVIDERS)).toBe(true); + expect(barrel.CLIPROXY_SUPPORTED_PROVIDERS.length).toBeGreaterThan(0); + expect(barrel.CLIPROXY_SUPPORTED_PROVIDERS).toEqual(schemas.CLIPROXY_SUPPORTED_PROVIDERS); + }); + + // ------------------------------------------------------------------------- + // Default constants + // ------------------------------------------------------------------------- + const defaultConstants = [ + 'DEFAULT_CLIPROXY_SAFETY_CONFIG', + 'DEFAULT_LOGGING_CONFIG', + 'DEFAULT_OFFICIAL_CHANNELS_CONFIG', + 'DEFAULT_BROWSER_CONFIG', + 'DEFAULT_DASHBOARD_AUTH_CONFIG', + 'DEFAULT_AUTO_QUOTA_CONFIG', + 'DEFAULT_MANUAL_QUOTA_CONFIG', + 'DEFAULT_RUNTIME_MONITOR_CONFIG', + 'DEFAULT_QUOTA_MANAGEMENT_CONFIG', + 'DEFAULT_THINKING_TIER_DEFAULTS', + 'DEFAULT_THINKING_CONFIG', + 'DEFAULT_COPILOT_CONFIG', + 'DEFAULT_CURSOR_CONFIG', + 'DEFAULT_CLIPROXY_SERVER_CONFIG', + 'DEFAULT_OPENAI_COMPAT_PROXY_CONFIG', + 'DEFAULT_GLOBAL_ENV', + 'DEFAULT_IMAGE_ANALYSIS_CONFIG', + ] as const; + + for (const name of defaultConstants) { + it(`re-exports ${name}`, () => { + expect(barrel[name]).toBeDefined(); + expect(barrel[name]).toEqual(schemas[name]); + }); + } + + // ------------------------------------------------------------------------- + // Functions + // ------------------------------------------------------------------------- + it('re-exports createEmptyUnifiedConfig', () => { + expect(typeof barrel.createEmptyUnifiedConfig).toBe('function'); + expect(typeof schemas.createEmptyUnifiedConfig).toBe('function'); + + const config = barrel.createEmptyUnifiedConfig(); + expect(config.version).toBe(13); + expect(config.accounts).toEqual({}); + expect(config.profiles).toEqual({}); + expect(config.cliproxy).toBeDefined(); + expect(config.cliproxy.oauth_accounts).toEqual({}); + expect(config.cliproxy.variants).toEqual({}); + expect(config.logging).toBeDefined(); + expect(config.preferences).toBeDefined(); + expect(config.browser).toBeDefined(); + expect(config.image_analysis).toBeDefined(); + expect(config.quota_management).toBeDefined(); + expect(config.thinking).toBeDefined(); + expect(config.channels).toBeDefined(); + expect(config.dashboard_auth).toBeDefined(); + expect(config.copilot).toBeDefined(); + expect(config.cursor).toBeDefined(); + expect(config.cliproxy_server).toBeDefined(); + expect(config.websearch).toBeDefined(); + }); + + it('re-exports isUnifiedConfig', () => { + expect(typeof barrel.isUnifiedConfig).toBe('function'); + expect(typeof schemas.isUnifiedConfig).toBe('function'); + + expect(barrel.isUnifiedConfig({ version: 13 })).toBe(true); + expect(barrel.isUnifiedConfig(null)).toBe(false); + expect(barrel.isUnifiedConfig({})).toBe(false); + expect(barrel.isUnifiedConfig({ version: 0 })).toBe(false); + expect(barrel.isUnifiedConfig({ version: 1 })).toBe(true); + expect(barrel.isUnifiedConfig('not an object')).toBe(false); + }); + + // ------------------------------------------------------------------------- + // Barrel has all expected runtime exports (type-only exports are verified + // at compile time via the import type block above — they are erased at + // runtime and cannot be checked with the `in` operator). + // ------------------------------------------------------------------------- + const expectedRuntimeExports = [ + 'UNIFIED_CONFIG_VERSION', + 'CLIPROXY_SUPPORTED_PROVIDERS', + 'createEmptyUnifiedConfig', + 'isUnifiedConfig', + 'DEFAULT_CLIPROXY_SAFETY_CONFIG', + 'DEFAULT_LOGGING_CONFIG', + 'DEFAULT_OFFICIAL_CHANNELS_CONFIG', + 'DEFAULT_BROWSER_CONFIG', + 'DEFAULT_DASHBOARD_AUTH_CONFIG', + 'DEFAULT_AUTO_QUOTA_CONFIG', + 'DEFAULT_MANUAL_QUOTA_CONFIG', + 'DEFAULT_RUNTIME_MONITOR_CONFIG', + 'DEFAULT_QUOTA_MANAGEMENT_CONFIG', + 'DEFAULT_THINKING_TIER_DEFAULTS', + 'DEFAULT_THINKING_CONFIG', + 'DEFAULT_COPILOT_CONFIG', + 'DEFAULT_CURSOR_CONFIG', + 'DEFAULT_CLIPROXY_SERVER_CONFIG', + 'DEFAULT_OPENAI_COMPAT_PROXY_CONFIG', + 'DEFAULT_GLOBAL_ENV', + 'DEFAULT_IMAGE_ANALYSIS_CONFIG', + ] as const; + + for (const name of expectedRuntimeExports) { + it(`barrel exports "${name}"`, () => { + expect(name in barrel).toBe(true); + }); + } +}); diff --git a/src/config/schemas/auth.ts b/src/config/schemas/auth.ts new file mode 100644 index 00000000..4056ecf8 --- /dev/null +++ b/src/config/schemas/auth.ts @@ -0,0 +1,109 @@ +/** + * Account, profile, and authentication config types. + * + * Covers: + * - AccountConfig: isolated Claude instances via CLAUDE_CONFIG_DIR + * - ProfileConfig: API-based profiles (env var injection) + * - OAuthAccounts: CLIProxy nickname-to-email mapping + * - CLIProxyAuthConfig: API key and management secret customization + * - TokenRefreshSettings: background token refresh worker config + * - DashboardAuthConfig: dashboard login protection + */ + +import type { TargetType } from '../../targets/target-adapter'; + +/** + * Account configuration (formerly in profiles.json). + * Represents an isolated Claude instance via CLAUDE_CONFIG_DIR. + */ +export interface AccountConfig { + /** ISO timestamp when account was created */ + created: string; + /** ISO timestamp of last usage, null if never used */ + last_used: string | null; + /** Context mode for project workspace data */ + context_mode?: 'isolated' | 'shared'; + /** Context-sharing group when context_mode='shared' */ + context_group?: string; + /** Shared continuity depth when context_mode='shared' */ + continuity_mode?: 'standard' | 'deeper'; + /** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */ + bare?: boolean; +} + +/** + * API-based profile configuration. + * Injects environment variables for alternative providers (GLM, Kimi, etc.). + * + * Settings are stored in separate *.settings.json files (matching Claude's pattern) + * to allow users to edit them directly without touching config.yaml. + */ +export interface ProfileConfig { + /** Profile type - currently only 'api' */ + type: 'api'; + /** Path to settings file (e.g., "~/.ccs/glm.settings.json") */ + settings: string; + /** Target CLI to use for this profile (default: 'claude') */ + target?: TargetType; +} + +/** + * CLIProxy OAuth account nickname mapping. + * Maps user-friendly nicknames to email addresses. + */ +export type OAuthAccounts = Record; + +/** + * CLIProxy authentication configuration. + * Allows customization of API key and management secret for CLIProxyAPI. + */ +export interface CLIProxyAuthConfig { + /** API key for CCS-managed requests (default: 'ccs-internal-managed') */ + api_key?: string; + /** Management secret for Control Panel login (default: 'ccs') */ + management_secret?: string; +} + +/** + * Token refresh configuration. + * Manages background token refresh worker settings. + */ +export interface TokenRefreshSettings { + /** Enable background token refresh (default: false) */ + enabled?: boolean; + /** Refresh check interval in minutes (default: 30) */ + interval_minutes?: number; + /** Preemptive refresh time in minutes (default: 45) */ + preemptive_minutes?: number; + /** Maximum retry attempts per token (default: 3) */ + max_retries?: number; + /** Enable verbose logging (default: false) */ + verbose?: boolean; +} + +/** + * Dashboard authentication configuration. + * Optional login protection for CCS dashboard. + * Disabled by default for backward compatibility. + */ +export interface DashboardAuthConfig { + /** Enable dashboard authentication (default: false) */ + enabled: boolean; + /** Username for dashboard login */ + username: string; + /** Bcrypt-hashed password (use: npx bcrypt-cli hash 'password') */ + password_hash: string; + /** Session timeout in hours (default: 24) */ + session_timeout_hours?: number; +} + +/** + * Default dashboard auth configuration. + * Disabled by default - must be explicitly enabled. + */ +export const DEFAULT_DASHBOARD_AUTH_CONFIG: DashboardAuthConfig = { + enabled: false, + username: '', + password_hash: '', + session_timeout_hours: 24, +}; diff --git a/src/config/schemas/browser.ts b/src/config/schemas/browser.ts new file mode 100644 index 00000000..f4facb4b --- /dev/null +++ b/src/config/schemas/browser.ts @@ -0,0 +1,71 @@ +/** + * Browser automation configuration types and defaults. + * + * Controls Claude browser attach and Codex browser tooling. + * Version 13+ feature. + */ + +/** + * Browser tool exposure policy. + */ +export type BrowserToolPolicy = 'auto' | 'manual'; + +/** + * Browser eval access mode. + */ +export type BrowserEvalMode = 'disabled' | 'readonly' | 'readwrite'; + +/** + * Claude browser attach configuration. + */ +export interface BrowserClaudeConfig { + /** Enable Claude browser attach (default: false) */ + enabled: boolean; + /** Control whether Claude browser attach is exposed automatically or only via --browser */ + policy: BrowserToolPolicy; + /** Chrome user-data directory used for attach mode */ + user_data_dir: string; + /** DevTools port used for attach mode (default: 9222) */ + devtools_port: number; + /** Eval access mode exposed through browser settings/status surfaces */ + eval_mode?: BrowserEvalMode; +} + +/** + * Codex browser tooling configuration. + */ +export interface BrowserCodexConfig { + /** Enable Codex browser tooling injection (default: false) */ + enabled: boolean; + /** Control whether Codex browser tooling is exposed automatically or only via --browser */ + policy: BrowserToolPolicy; + /** Eval access mode exposed through browser settings/status surfaces */ + eval_mode?: BrowserEvalMode; +} + +/** + * Browser automation configuration. + * Controls Claude browser attach and Codex browser tooling. + */ +export interface BrowserConfig { + claude: BrowserClaudeConfig; + codex: BrowserCodexConfig; +} + +/** + * Default browser configuration. + */ +export const DEFAULT_BROWSER_CONFIG: BrowserConfig = { + claude: { + enabled: false, + policy: 'manual', + user_data_dir: '', + devtools_port: 9222, + eval_mode: 'readonly', + }, + codex: { + enabled: false, + policy: 'manual', + eval_mode: 'readonly', + }, +}; diff --git a/src/config/schemas/channels.ts b/src/config/schemas/channels.ts new file mode 100644 index 00000000..f8d5274c --- /dev/null +++ b/src/config/schemas/channels.ts @@ -0,0 +1,32 @@ +/** + * Official Channels configuration types and defaults. + * + * Controls runtime-only injection of Anthropic's official channel plugins + * (Telegram, Discord, iMessage). + * Version 12+ feature. + */ + +/** + * Supported Anthropic official channel IDs. + */ +export type OfficialChannelId = 'telegram' | 'discord' | 'imessage'; + +/** + * Official Channels configuration. + * Controls runtime-only injection of Anthropic's official channel plugins. + */ +export interface OfficialChannelsConfig { + /** Selected official channels to auto-enable for compatible sessions */ + selected: OfficialChannelId[]; + /** Also add --dangerously-skip-permissions when auto-enable is active */ + unattended: boolean; +} + +/** + * Default Official Channels configuration. + * Disabled by default because the feature requires explicit user setup. + */ +export const DEFAULT_OFFICIAL_CHANNELS_CONFIG: OfficialChannelsConfig = { + selected: [], + unattended: false, +}; diff --git a/src/config/schemas/cliproxy.ts b/src/config/schemas/cliproxy.ts new file mode 100644 index 00000000..1765335f --- /dev/null +++ b/src/config/schemas/cliproxy.ts @@ -0,0 +1,151 @@ +/** + * CLIProxy configuration types and defaults. + * + * Covers provider/variant/routing/safety/logging configuration + * for the CLIProxy integration layer. + */ + +import type { TargetType } from '../../targets/target-adapter'; +import type { CLIProxyProvider, CliproxyRoutingStrategy } from '../../cliproxy/types'; +import { CLIPROXY_PROVIDER_IDS } from '../../cliproxy/provider-capabilities'; +import type { OAuthAccounts, CLIProxyAuthConfig, TokenRefreshSettings } from './auth'; + +/** + * Supported CLIProxy providers. + * Derived from CLIPROXY_PROVIDER_IDS — single source of truth in provider-capabilities.ts. + */ +export { CLIPROXY_PROVIDER_IDS as CLIPROXY_SUPPORTED_PROVIDERS }; + +/** + * CLIProxy variant configuration. + * User-defined variants of built-in OAuth providers. + * + * Settings are stored in separate *.settings.json files (matching Claude's pattern) + * to allow users to edit them directly without touching config.yaml. + */ +export interface CLIProxyVariantConfig { + /** Base provider to use */ + provider: CLIProxyProvider; + /** Account nickname (references oauth_accounts) */ + account?: string; + /** Path to settings file (e.g., "~/.ccs/gemini-custom.settings.json") */ + settings?: string; + /** Unique port for variant isolation (8318-8417) */ + port?: number; + /** Per-variant auth override (optional) */ + auth?: CLIProxyAuthConfig; + /** Target CLI to use for this variant (default: 'claude') */ + target?: TargetType; +} + +/** + * Per-tier provider+model mapping for composite variants. + */ +export interface CompositeTierConfig { + /** Provider for this tier */ + provider: CLIProxyProvider; + /** Model ID to use for this tier */ + model: string; + /** Account nickname (optional, references oauth_accounts) */ + account?: string; + /** Fallback provider+model if primary fails */ + fallback?: { + provider: CLIProxyProvider; + model: string; + account?: string; + }; + /** Per-tier thinking budget override (e.g. 'xhigh', 'medium', 'off') */ + thinking?: string; +} + +/** + * Composite variant configuration. + * Mixes different providers per Claude tier (opus, sonnet, haiku) in a single profile. + * Uses CLIProxyAPI root endpoints (/v1/messages) for model-based routing + * instead of provider-specific endpoints (/api/provider/{provider}). + */ +export interface CompositeVariantConfig { + /** Discriminator for composite type */ + type: 'composite'; + /** Which tier ANTHROPIC_MODEL equals (default must be one of the three) */ + default_tier: 'opus' | 'sonnet' | 'haiku'; + /** Per-tier provider+model mapping */ + tiers: { + opus: CompositeTierConfig; + sonnet: CompositeTierConfig; + haiku: CompositeTierConfig; + }; + /** Path to settings file */ + settings?: string; + /** Shared port for the composite profile */ + port?: number; + /** Per-variant auth override (optional) */ + auth?: CLIProxyAuthConfig; + /** Target CLI to use for this composite variant (default: 'claude') */ + target?: TargetType; +} + +/** + * CLIProxy logging configuration. + * Controls whether CLIProxyAPI writes logs to disk. + * Logs can grow to several GB if left enabled. + */ +export interface CLIProxyLoggingConfig { + /** Enable logging to file (default: false to prevent disk bloat) */ + enabled?: boolean; + /** Enable request logging for debugging (default: false) */ + request_log?: boolean; +} + +/** + * CLIProxy safety configuration. + * Controls high-risk flow safeguards for supported providers. + */ +export interface CLIProxySafetyConfig { + /** Allow skipping AGY responsibility checks and Gemini dashboard typed acknowledgement */ + antigravity_ack_bypass?: boolean; +} + +/** + * Default CLIProxy safety configuration. + */ +export const DEFAULT_CLIPROXY_SAFETY_CONFIG: CLIProxySafetyConfig = { + antigravity_ack_bypass: false, +}; + +export interface CLIProxyRoutingConfig { + /** Credential selection strategy when multiple accounts match */ + strategy?: CliproxyRoutingStrategy; + /** Keep one conversation pinned to the same account when possible */ + session_affinity?: boolean; + /** Go-style duration for session-affinity binding retention */ + session_affinity_ttl?: string; +} + +/** + * CLIProxy configuration section. + */ +export interface CLIProxyConfig { + /** Backend selection: 'original' or 'plus' (default: 'original') */ + backend?: 'original' | 'plus'; + /** Nickname to email mapping for OAuth accounts */ + oauth_accounts: OAuthAccounts; + /** Built-in providers (read-only, for reference) */ + providers: readonly string[]; + /** User-defined provider variants (single-provider or composite) */ + variants: Record; + /** Logging configuration (disabled by default) */ + logging?: CLIProxyLoggingConfig; + /** Safety controls for high-risk provider flows */ + safety?: CLIProxySafetyConfig; + /** Kiro: disable incognito browser mode (use normal browser to save credentials) */ + kiro_no_incognito?: boolean; + /** Global auth configuration for CLIProxyAPI */ + auth?: CLIProxyAuthConfig; + /** Background token refresh worker settings */ + token_refresh?: TokenRefreshSettings; + /** Auto-sync API profiles to local CLIProxy config on settings change (default: true) */ + auto_sync?: boolean; + /** Routing strategy for multi-account CLIProxy selection */ + routing?: CLIProxyRoutingConfig; +} diff --git a/src/config/schemas/copilot-cursor.ts b/src/config/schemas/copilot-cursor.ts new file mode 100644 index 00000000..e0b33fbb --- /dev/null +++ b/src/config/schemas/copilot-cursor.ts @@ -0,0 +1,93 @@ +/** + * Copilot and Cursor IDE integration configuration types and defaults. + * + * Covers: + * - CopilotConfig: GitHub Copilot proxy integration (strictly opt-in) + * - CursorConfig: Cursor IDE proxy daemon + */ + +/** + * Copilot API account type. + */ +export type CopilotAccountType = 'individual' | 'business' | 'enterprise'; + +/** + * Copilot API configuration. + * Enables GitHub Copilot subscription usage via copilot-api proxy. + * Strictly opt-in - disabled by default. + * + * !! DISCLAIMER - USE AT YOUR OWN RISK !! + * This uses an UNOFFICIAL reverse-engineered API. + * Excessive usage may trigger GitHub account restrictions. + * CCS provides NO WARRANTY and accepts NO RESPONSIBILITY for any consequences. + */ +export interface CopilotConfig { + /** Enable Copilot integration (default: false) - must be explicitly enabled */ + enabled: boolean; + /** Auto-start copilot-api daemon when using profile (default: false) */ + auto_start: boolean; + /** Port for copilot-api proxy (default: 4141) */ + port: number; + /** GitHub Copilot account type (default: individual) */ + account_type: CopilotAccountType; + /** Rate limit in seconds between requests (null = no limit) */ + rate_limit: number | null; + /** Wait instead of error when rate limit is hit (default: true) */ + wait_on_limit: boolean; + /** Default model ID (e.g., claude-sonnet-4.5) */ + model: string; + /** Model mapping for Claude tiers - maps opus/sonnet/haiku to specific models */ + opus_model?: string; + sonnet_model?: string; + haiku_model?: string; +} + +/** + * Cursor IDE integration configuration. + * Enables Cursor IDE usage via cursor proxy daemon. + */ +export interface CursorConfig { + /** Enable Cursor integration (default: false) */ + enabled: boolean; + /** Port for cursor proxy daemon (default: 20129) */ + port: number; + /** Auto-start daemon when CCS starts (default: false) */ + auto_start: boolean; + /** Enable ghost mode to disable telemetry (default: true) */ + ghost_mode: boolean; + /** Default model ID used by Cursor integration */ + model: string; + /** Optional tier mapping for Claude-compatible model routing */ + opus_model?: string; + /** Optional tier mapping for Claude-compatible model routing */ + sonnet_model?: string; + /** Optional tier mapping for Claude-compatible model routing */ + haiku_model?: string; +} + +/** + * Default Copilot configuration. + * Strictly opt-in - disabled by default. + * Uses gpt-4.1 as default model (free tier compatible). + */ +export const DEFAULT_COPILOT_CONFIG: CopilotConfig = { + enabled: false, + auto_start: false, + port: 4141, + account_type: 'individual', + rate_limit: null, + wait_on_limit: true, + model: 'gpt-4.1', +}; + +/** + * Default Cursor configuration. + * Disabled by default, ghost mode enabled for privacy. + */ +export const DEFAULT_CURSOR_CONFIG: CursorConfig = { + enabled: false, + port: 20129, + auto_start: false, + ghost_mode: true, + model: 'gpt-5.3-codex', +}; diff --git a/src/config/schemas/index.ts b/src/config/schemas/index.ts new file mode 100644 index 00000000..ff0313f3 --- /dev/null +++ b/src/config/schemas/index.ts @@ -0,0 +1,112 @@ +/** + * Config schema barrel re-exports. + * + * All types, interfaces, constants, and functions originally in + * unified-config-types.ts are re-exported here for backward compatibility. + * Each module is responsible for a focused domain of the config schema. + */ + +// Version constant +export { UNIFIED_CONFIG_VERSION } from './version'; + +// Account, profile, OAuth, auth types +export type { + AccountConfig, + ProfileConfig, + OAuthAccounts, + CLIProxyAuthConfig, + TokenRefreshSettings, + DashboardAuthConfig, +} from './auth'; +export { DEFAULT_DASHBOARD_AUTH_CONFIG } from './auth'; + +// CLIProxy provider, variant, routing, safety, logging types +export { CLIPROXY_SUPPORTED_PROVIDERS, DEFAULT_CLIPROXY_SAFETY_CONFIG } from './cliproxy'; +export type { + CLIProxyVariantConfig, + CompositeTierConfig, + CompositeVariantConfig, + CLIProxyLoggingConfig, + CLIProxySafetyConfig, + CLIProxyRoutingConfig, + CLIProxyConfig, +} from './cliproxy'; + +// Quota management types and defaults +export { + DEFAULT_AUTO_QUOTA_CONFIG, + DEFAULT_MANUAL_QUOTA_CONFIG, + DEFAULT_RUNTIME_MONITOR_CONFIG, + DEFAULT_QUOTA_MANAGEMENT_CONFIG, +} from './quota'; +export type { + AutoQuotaConfig, + RuntimeMonitorConfig, + ManualQuotaConfig, + QuotaManagementMode, + QuotaManagementConfig, +} from './quota'; + +// Thinking/reasoning budget types and defaults +export { DEFAULT_THINKING_TIER_DEFAULTS, DEFAULT_THINKING_CONFIG } from './thinking'; +export type { ThinkingMode, ThinkingTierDefaults, ThinkingConfig } from './thinking'; + +// Official channels types and defaults +export { DEFAULT_OFFICIAL_CHANNELS_CONFIG } from './channels'; +export type { OfficialChannelId, OfficialChannelsConfig } from './channels'; + +// WebSearch backend types +export type { + DuckDuckGoWebSearchConfig, + BraveWebSearchConfig, + ExaWebSearchConfig, + TavilyWebSearchConfig, + SearxngWebSearchConfig, + GeminiWebSearchConfig, + GrokWebSearchConfig, + OpenCodeWebSearchConfig, + WebSearchProvidersConfig, + WebSearchConfig, +} from './websearch'; + +// Browser automation types and defaults +export { DEFAULT_BROWSER_CONFIG } from './browser'; +export type { + BrowserToolPolicy, + BrowserEvalMode, + BrowserClaudeConfig, + BrowserCodexConfig, + BrowserConfig, +} from './browser'; + +// Logging and preferences types and defaults +export { DEFAULT_LOGGING_CONFIG } from './logging'; +export type { LoggingLevel, LoggingConfig, PreferencesConfig } from './logging'; + +// Provider integration types and defaults +export { + DEFAULT_GLOBAL_ENV, + DEFAULT_COPILOT_CONFIG, + DEFAULT_CURSOR_CONFIG, + DEFAULT_CLIPROXY_SERVER_CONFIG, + DEFAULT_OPENAI_COMPAT_PROXY_CONFIG, + DEFAULT_IMAGE_ANALYSIS_CONFIG, +} from './providers'; +export type { + CopilotAccountType, + CopilotConfig, + CursorConfig, + ProxyRemoteConfig, + ProxyFallbackConfig, + ProxyLocalConfig, + OpenAICompatProxyRoutingConfig, + OpenAICompatProxyConfig, + CliproxyServerConfig, + GlobalEnvConfig, + ContinuityConfig, + ImageAnalysisConfig, +} from './providers'; + +// Main unified config interface, factory, and type guard +export { createEmptyUnifiedConfig, isUnifiedConfig } from './unified-config'; +export type { UnifiedConfig } from './unified-config'; diff --git a/src/config/schemas/logging.ts b/src/config/schemas/logging.ts new file mode 100644 index 00000000..05fe3061 --- /dev/null +++ b/src/config/schemas/logging.ts @@ -0,0 +1,53 @@ +/** + * Logging and preferences configuration types and defaults. + * + * Covers: + * - LoggingConfig: CCS-owned structured runtime logging + * - LoggingLevel: log severity levels + * - PreferencesConfig: user preferences (theme, telemetry, auto-update) + */ + +export type LoggingLevel = 'error' | 'warn' | 'info' | 'debug'; + +/** + * CCS-owned structured logging configuration. + * Separate from cliproxy.logging, which controls CLIProxy runtime files. + */ +export interface LoggingConfig { + /** Enable CCS-owned structured runtime logging */ + enabled: boolean; + /** Minimum level written to disk */ + level: LoggingLevel; + /** Rotate current log when it reaches this size in MB */ + rotate_mb: number; + /** Keep archived segments for this many days */ + retain_days: number; + /** Redact sensitive values before persistence */ + redact: boolean; + /** In-memory recent event buffer size for dashboard reads */ + live_buffer_size: number; +} + +/** + * Default logging configuration. + */ +export const DEFAULT_LOGGING_CONFIG: LoggingConfig = { + enabled: true, + level: 'info', + rotate_mb: 10, + retain_days: 7, + redact: true, + live_buffer_size: 250, +}; + +/** + * User preferences. + */ +export interface PreferencesConfig { + /** UI theme preference */ + theme?: 'light' | 'dark' | 'system'; + /** Enable anonymous telemetry */ + telemetry?: boolean; + /** Enable automatic update checks */ + auto_update?: boolean; +} diff --git a/src/config/schemas/providers.ts b/src/config/schemas/providers.ts new file mode 100644 index 00000000..78a6dc8f --- /dev/null +++ b/src/config/schemas/providers.ts @@ -0,0 +1,30 @@ +/** + * Provider integration configuration types and defaults. + * + * Re-exports from focused sub-modules for backward compatibility. + * Actual definitions live in: + * - copilot-cursor.ts: CopilotConfig, CursorConfig + defaults + * - proxy-server.ts: CliproxyServerConfig, OpenAICompatProxyConfig, + * GlobalEnvConfig, ContinuityConfig, ImageAnalysisConfig + defaults + */ + +export type { CopilotAccountType, CopilotConfig, CursorConfig } from './copilot-cursor'; +export { DEFAULT_COPILOT_CONFIG, DEFAULT_CURSOR_CONFIG } from './copilot-cursor'; + +export type { + ProxyRemoteConfig, + ProxyFallbackConfig, + ProxyLocalConfig, + OpenAICompatProxyRoutingConfig, + OpenAICompatProxyConfig, + CliproxyServerConfig, + GlobalEnvConfig, + ContinuityConfig, + ImageAnalysisConfig, +} from './proxy-server'; +export { + DEFAULT_CLIPROXY_SERVER_CONFIG, + DEFAULT_OPENAI_COMPAT_PROXY_CONFIG, + DEFAULT_GLOBAL_ENV, + DEFAULT_IMAGE_ANALYSIS_CONFIG, +} from './proxy-server'; diff --git a/src/config/schemas/proxy-server.ts b/src/config/schemas/proxy-server.ts new file mode 100644 index 00000000..745ebab3 --- /dev/null +++ b/src/config/schemas/proxy-server.ts @@ -0,0 +1,193 @@ +/** + * Proxy server, global env, continuity, and image analysis types and defaults. + * + * Covers: + * - CliproxyServerConfig: remote/local CLIProxy server mode + * - OpenAICompatProxyConfig: OpenAI-compatible local proxy + * - GlobalEnvConfig: global environment variable injection + * - ContinuityConfig: cross-profile continuity inheritance + * - ImageAnalysisConfig: vision analysis via CLIProxy + */ + +/** + * Remote proxy configuration. + * Connect to a remote CLIProxyAPI instance instead of spawning local binary. + */ +export interface ProxyRemoteConfig { + /** Enable remote proxy mode (default: false = local mode) */ + enabled: boolean; + /** Remote proxy hostname or IP (empty = not configured) */ + host: string; + /** + * Remote proxy port. + * Optional - defaults based on protocol: + * - HTTPS: 443 + * - HTTP: 8317 + * When empty/undefined, uses protocol default. + */ + port?: number; + /** Protocol for remote connection */ + protocol: 'http' | 'https'; + /** Auth token for remote proxy API endpoints (optional, sent as header) */ + auth_token: string; + /** + * Management key for remote proxy management API endpoints. + * CLIProxyAPI uses separate authentication for management endpoints + * (/v0/management/*) via 'secret-key' config. + * If not set, falls back to auth_token for backwards compatibility. + */ + management_key?: string; + /** Connection timeout in milliseconds (default: 2000) */ + timeout?: number; + /** Enable auto-sync profiles to remote on settings change (default: false) */ + auto_sync?: boolean; +} + +/** + * Fallback configuration when remote proxy is unreachable. + */ +export interface ProxyFallbackConfig { + /** Enable fallback to local proxy (default: true) */ + enabled: boolean; + /** Auto-start local proxy without prompting (default: false = prompt user) */ + auto_start: boolean; +} + +/** + * Local proxy configuration. + */ +export interface ProxyLocalConfig { + /** Local proxy port (default: 8317) */ + port: number; + /** Auto-start local binary (default: true) */ + auto_start: boolean; +} + +export interface OpenAICompatProxyRoutingConfig { + default?: string; + background?: string; + think?: string; + longContext?: string; + webSearch?: string; + longContextThreshold?: number; +} + +export interface OpenAICompatProxyConfig { + /** Default local port for OpenAI-compatible proxy instances */ + port?: number; + /** Optional profile-scoped local port overrides */ + profile_ports?: Record; + routing?: OpenAICompatProxyRoutingConfig; +} + +/** + * CLIProxy server configuration section. + * Controls whether CCS uses local or remote CLIProxyAPI instance. + */ +export interface CliproxyServerConfig { + /** Remote proxy settings */ + remote: ProxyRemoteConfig; + /** Fallback behavior when remote is unreachable */ + fallback: ProxyFallbackConfig; + /** Local proxy settings */ + local: ProxyLocalConfig; +} + +/** + * Global environment variables configuration. + * These env vars are injected into ALL non-Claude subscription profiles. + * Useful for disabling telemetry, bug commands, error reporting, etc. + */ +export interface GlobalEnvConfig { + /** Enable global env injection (default: true) */ + enabled: boolean; + /** Environment variables to inject */ + env: Record; +} + +/** + * Cross-profile continuity inheritance configuration. + * Maps execution profile names to source account profiles for CLAUDE_CONFIG_DIR reuse. + */ +export interface ContinuityConfig { + /** Profile name -> source account profile name */ + inherit_from_account?: Record; +} + +/** + * Default global env vars for third-party profiles. + * These disable Claude Code telemetry/reporting since we're using proxy. + */ +export const DEFAULT_GLOBAL_ENV: Record = { + DISABLE_BUG_COMMAND: '1', + DISABLE_ERROR_REPORTING: '1', + DISABLE_TELEMETRY: '1', +}; + +/** + * Default CLIProxy server configuration. + * Local mode by default - remote must be explicitly enabled. + * Port is optional for remote - defaults based on protocol. + */ +export const DEFAULT_CLIPROXY_SERVER_CONFIG: CliproxyServerConfig = { + remote: { + enabled: false, + host: '', + protocol: 'http', + auth_token: '', + }, + fallback: { + enabled: true, + auto_start: false, + }, + local: { + port: 8317, + auto_start: true, + }, +}; + +export const DEFAULT_OPENAI_COMPAT_PROXY_CONFIG: OpenAICompatProxyConfig = { + profile_ports: {}, + routing: { + longContextThreshold: 60_000, + }, +}; + +/** + * Image analysis configuration. + * Routes image/PDF files through CLIProxy for vision analysis. + */ +export interface ImageAnalysisConfig { + /** Enable image analysis via CLIProxy (default: true) */ + enabled: boolean; + /** Timeout in seconds (default: 60) */ + timeout: number; + /** Provider-to-model mapping for vision analysis */ + provider_models: Record; + /** Fallback backend used when a profile does not resolve to a provider-specific backend */ + fallback_backend?: string; + /** Explicit profile-name-to-backend overrides for settings/custom aliases */ + profile_backends?: Record; +} + +/** + * Default image analysis configuration. + * Enabled by default for CLIProxy providers with vision support. + */ +export const DEFAULT_IMAGE_ANALYSIS_CONFIG: ImageAnalysisConfig = { + enabled: true, + timeout: 60, + provider_models: { + agy: 'gemini-3-1-flash-preview', + gemini: 'gemini-3-flash-preview', + codex: 'gpt-5.1-codex-mini', + kiro: 'kiro-claude-haiku-4-5', + ghcp: 'claude-haiku-4.5', + claude: 'claude-haiku-4.5-20251001', + qwen: 'vision-model', + iflow: 'qwen3-vl-plus', + kimi: 'vision-model', + }, + fallback_backend: 'gemini', + profile_backends: {}, +}; diff --git a/src/config/schemas/quota.ts b/src/config/schemas/quota.ts new file mode 100644 index 00000000..0d380694 --- /dev/null +++ b/src/config/schemas/quota.ts @@ -0,0 +1,121 @@ +/** + * Quota management configuration types and defaults. + * + * Controls hybrid auto+manual account selection for multi-account setups. + * Version 7+ feature. + */ + +// ============================================================================ +// QUOTA MANAGEMENT CONFIGURATION (v7+) +// ============================================================================ + +/** + * Auto quota management configuration. + * Controls automatic failover behavior. + */ +export interface AutoQuotaConfig { + /** Enable pre-flight quota check before requests (default: true) */ + preflight_check: boolean; + /** Quota percentage below which account is "exhausted" (default: 5) */ + exhaustion_threshold: number; + /** Tier priority for failover, highest to lowest (default: ['paid']) */ + tier_priority: string[]; + /** Minutes to skip exhausted account before retry (default: 5) */ + cooldown_minutes: number; +} + +/** + * Runtime quota monitor configuration. + * Controls adaptive polling during active sessions. + */ +export interface RuntimeMonitorConfig { + /** Enable runtime monitoring during sessions (default: true) */ + enabled: boolean; + /** Poll interval in seconds when quota > warn_threshold (default: 300) */ + normal_interval_seconds: number; + /** Poll interval in seconds when quota <= warn_threshold (default: 60) */ + critical_interval_seconds: number; + /** Quota percentage that triggers fast polling + warning (default: 20) */ + warn_threshold: number; + /** Quota percentage that triggers cooldown + switch (default: 5) */ + exhaustion_threshold: number; + /** Minutes to cooldown exhausted account (default: 5) */ + cooldown_minutes: number; +} + +/** + * Manual quota management configuration. + * User-controlled overrides for account selection. + */ +export interface ManualQuotaConfig { + /** User-paused accounts (stored in accounts.json) */ + paused_accounts: string[]; + /** Force use of specific account (overrides auto-selection) */ + forced_default: string | null; + /** Lock to specific tier only */ + tier_lock: string | null; +} + +/** + * Quota management mode. + * - auto: Fully automatic failover based on quota + * - manual: User controls everything, no auto-switching + * - hybrid: Auto-failover with user overrides (default) + */ +export type QuotaManagementMode = 'auto' | 'manual' | 'hybrid'; + +/** + * Quota management configuration section. + * Controls hybrid auto+manual account selection for multi-account setups. + */ +export interface QuotaManagementConfig { + /** Management mode (default: hybrid) */ + mode: QuotaManagementMode; + /** Auto mode settings */ + auto: AutoQuotaConfig; + /** Manual mode settings */ + manual: ManualQuotaConfig; + /** Runtime monitor settings */ + runtime_monitor: RuntimeMonitorConfig; +} + +/** + * Default auto quota configuration. + */ +export const DEFAULT_AUTO_QUOTA_CONFIG: AutoQuotaConfig = { + preflight_check: true, + exhaustion_threshold: 5, + tier_priority: ['ultra', 'pro', 'free'], + cooldown_minutes: 5, +}; + +/** + * Default manual quota configuration. + */ +export const DEFAULT_MANUAL_QUOTA_CONFIG: ManualQuotaConfig = { + paused_accounts: [], + forced_default: null, + tier_lock: null, +}; + +/** + * Default runtime monitor configuration. + */ +export const DEFAULT_RUNTIME_MONITOR_CONFIG: RuntimeMonitorConfig = { + enabled: true, + normal_interval_seconds: 300, + critical_interval_seconds: 60, + warn_threshold: 20, + exhaustion_threshold: 5, + cooldown_minutes: 5, +}; + +/** + * Default quota management configuration. + */ +export const DEFAULT_QUOTA_MANAGEMENT_CONFIG: QuotaManagementConfig = { + mode: 'hybrid', + auto: { ...DEFAULT_AUTO_QUOTA_CONFIG }, + manual: { ...DEFAULT_MANUAL_QUOTA_CONFIG }, + runtime_monitor: { ...DEFAULT_RUNTIME_MONITOR_CONFIG }, +}; diff --git a/src/config/schemas/thinking.ts b/src/config/schemas/thinking.ts new file mode 100644 index 00000000..81951ad2 --- /dev/null +++ b/src/config/schemas/thinking.ts @@ -0,0 +1,66 @@ +/** + * Thinking/reasoning budget configuration types and defaults. + * + * Controls thinking budget injection for CLIProxy providers. + * Version 8+ feature. + */ + +// ============================================================================ +// THINKING CONFIGURATION (v8+) +// ============================================================================ + +/** + * Thinking mode for auto/manual/off control. + * - auto: Apply tier-based defaults (opus→high, sonnet→medium, haiku→low) + * - off: Disable thinking entirely + * - manual: Use explicit override value + */ +export type ThinkingMode = 'auto' | 'off' | 'manual'; + +/** + * Tier-to-thinking level defaults. + * Maps Claude tier names to thinking level names. + */ +export interface ThinkingTierDefaults { + /** Thinking level for opus tier (default: 'high') */ + opus: string; + /** Thinking level for sonnet tier (default: 'medium') */ + sonnet: string; + /** Thinking level for haiku tier (default: 'low') */ + haiku: string; +} + +/** + * Thinking configuration section. + * Controls thinking/reasoning budget injection for CLIProxy providers. + */ +export interface ThinkingConfig { + /** Thinking mode (default: 'auto') */ + mode: ThinkingMode; + /** Manual override value (level name or budget number) */ + override?: string | number; + /** Tier-to-level mapping */ + tier_defaults: ThinkingTierDefaults; + /** Per-provider overrides (e.g., { gemini: { opus: 'high' } }) */ + provider_overrides?: Record>; + /** Show warning when values are clamped (default: true) */ + show_warnings?: boolean; +} + +/** + * Default thinking tier defaults. + */ +export const DEFAULT_THINKING_TIER_DEFAULTS: ThinkingTierDefaults = { + opus: 'high', + sonnet: 'medium', + haiku: 'low', +}; + +/** + * Default thinking configuration. + */ +export const DEFAULT_THINKING_CONFIG: ThinkingConfig = { + mode: 'auto', + tier_defaults: { ...DEFAULT_THINKING_TIER_DEFAULTS }, + show_warnings: true, +}; diff --git a/src/config/schemas/unified-config.ts b/src/config/schemas/unified-config.ts new file mode 100644 index 00000000..78726c46 --- /dev/null +++ b/src/config/schemas/unified-config.ts @@ -0,0 +1,200 @@ +/** + * Main unified configuration interface, factory, and type guard. + * + * The UnifiedConfig type is the root of the entire config.yaml schema. + * This file imports all section types from their respective schema modules. + */ + +import type { AccountConfig, ProfileConfig, DashboardAuthConfig } from './auth'; +import { DEFAULT_DASHBOARD_AUTH_CONFIG } from './auth'; +import type { CLIProxyConfig } from './cliproxy'; +import { CLIPROXY_SUPPORTED_PROVIDERS, DEFAULT_CLIPROXY_SAFETY_CONFIG } from './cliproxy'; +import type { LoggingConfig, PreferencesConfig } from './logging'; +import { DEFAULT_LOGGING_CONFIG } from './logging'; +import type { WebSearchConfig } from './websearch'; +import type { + GlobalEnvConfig, + ContinuityConfig, + CopilotConfig, + CursorConfig, + CliproxyServerConfig, + OpenAICompatProxyConfig, + ImageAnalysisConfig, +} from './providers'; +import { + DEFAULT_COPILOT_CONFIG, + DEFAULT_CURSOR_CONFIG, + DEFAULT_CLIPROXY_SERVER_CONFIG, + DEFAULT_OPENAI_COMPAT_PROXY_CONFIG, + DEFAULT_IMAGE_ANALYSIS_CONFIG, + DEFAULT_GLOBAL_ENV, +} from './providers'; +import { UNIFIED_CONFIG_VERSION } from './version'; +import type { QuotaManagementConfig } from './quota'; +import { DEFAULT_QUOTA_MANAGEMENT_CONFIG } from './quota'; +import type { ThinkingConfig } from './thinking'; +import { DEFAULT_THINKING_CONFIG } from './thinking'; +import type { OfficialChannelsConfig } from './channels'; +import { DEFAULT_OFFICIAL_CHANNELS_CONFIG } from './channels'; +import type { BrowserConfig } from './browser'; +import { DEFAULT_BROWSER_CONFIG } from './browser'; + +/** + * Main unified configuration structure. + * Stored in ~/.ccs/config.yaml + */ +export interface UnifiedConfig { + /** Config version */ + version: number; + /** Flag indicating setup wizard has been completed */ + setup_completed?: boolean; + /** Default profile name to use when none specified */ + default?: string; + /** Account-based profiles (isolated Claude instances) */ + accounts: Record; + /** API-based profiles (env var injection) */ + profiles: Record; + /** CLIProxy configuration */ + cliproxy: CLIProxyConfig; + /** OpenAI-compatible local proxy configuration */ + proxy?: OpenAICompatProxyConfig; + /** CCS-owned structured logging configuration */ + logging?: LoggingConfig; + /** User preferences */ + preferences: PreferencesConfig; + /** WebSearch configuration */ + websearch?: WebSearchConfig; + /** Global environment variables for all non-Claude subscription profiles */ + global_env?: GlobalEnvConfig; + /** Cross-profile continuity inheritance mapping */ + continuity?: ContinuityConfig; + /** Copilot API configuration (GitHub Copilot proxy) */ + copilot?: CopilotConfig; + /** Cursor IDE configuration (Cursor proxy daemon) */ + cursor?: CursorConfig; + /** CLIProxy server configuration for remote/local mode */ + cliproxy_server?: CliproxyServerConfig; + /** Quota management configuration (v7+) */ + quota_management?: QuotaManagementConfig; + /** Thinking/reasoning budget configuration (v8+) */ + thinking?: ThinkingConfig; + /** Official Channels runtime auto-enable preferences (v11+) */ + channels?: OfficialChannelsConfig; + /** Dashboard authentication configuration (optional) */ + dashboard_auth?: DashboardAuthConfig; + /** Browser automation configuration */ + browser?: BrowserConfig; + /** Image analysis configuration (vision via CLIProxy) */ + image_analysis?: ImageAnalysisConfig; +} + +/** + * Create an empty unified config with defaults. + */ +export function createEmptyUnifiedConfig(): UnifiedConfig { + return { + version: UNIFIED_CONFIG_VERSION, + default: undefined, + accounts: {}, + profiles: {}, + cliproxy: { + backend: 'original', + oauth_accounts: {}, + providers: [...CLIPROXY_SUPPORTED_PROVIDERS], + variants: {}, + logging: { + enabled: false, + request_log: false, + }, + safety: { ...DEFAULT_CLIPROXY_SAFETY_CONFIG }, + auto_sync: true, + routing: { + strategy: 'round-robin', + session_affinity: false, + session_affinity_ttl: '1h', + }, + }, + proxy: { + port: DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.port, + profile_ports: { ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.profile_ports }, + routing: { + ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.routing, + }, + }, + logging: { ...DEFAULT_LOGGING_CONFIG }, + preferences: { + theme: 'system', + telemetry: false, + auto_update: true, + }, + websearch: { + enabled: true, + providers: { + exa: { + enabled: false, + max_results: 5, + }, + tavily: { + enabled: false, + max_results: 5, + }, + brave: { + enabled: false, + max_results: 5, + }, + searxng: { + enabled: false, + url: '', + max_results: 5, + }, + duckduckgo: { + enabled: true, + max_results: 5, + }, + gemini: { + enabled: false, + model: 'gemini-2.5-flash', + timeout: 55, + }, + opencode: { + enabled: false, + model: 'opencode/grok-code', + timeout: 90, + }, + grok: { + enabled: false, + timeout: 55, + }, + }, + }, + global_env: { + enabled: true, + env: { ...DEFAULT_GLOBAL_ENV }, + }, + copilot: { ...DEFAULT_COPILOT_CONFIG }, + cursor: { ...DEFAULT_CURSOR_CONFIG }, + cliproxy_server: { ...DEFAULT_CLIPROXY_SERVER_CONFIG }, + quota_management: { ...DEFAULT_QUOTA_MANAGEMENT_CONFIG }, + thinking: { ...DEFAULT_THINKING_CONFIG }, + channels: { ...DEFAULT_OFFICIAL_CHANNELS_CONFIG }, + dashboard_auth: { ...DEFAULT_DASHBOARD_AUTH_CONFIG }, + browser: { + claude: { ...DEFAULT_BROWSER_CONFIG.claude }, + codex: { ...DEFAULT_BROWSER_CONFIG.codex }, + }, + image_analysis: { ...DEFAULT_IMAGE_ANALYSIS_CONFIG }, + }; +} + +/** + * Type guard for UnifiedConfig. + * Relaxed validation: accepts configs with version >= 1 and any subset of sections. + * Missing sections will be filled with defaults during merge. + */ +export function isUnifiedConfig(obj: unknown): obj is UnifiedConfig { + if (typeof obj !== 'object' || obj === null) return false; + const config = obj as Record; + // Only require version to be a number >= 1 (allow future versions) + // Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig + return typeof config.version === 'number' && config.version >= 1; +} diff --git a/src/config/schemas/version.ts b/src/config/schemas/version.ts new file mode 100644 index 00000000..98979314 --- /dev/null +++ b/src/config/schemas/version.ts @@ -0,0 +1,23 @@ +/** + * Unified config version constant. + * + * Central source of truth for the current config schema version. + * Incremented whenever new sections are added to config.yaml. + */ + +/** + * Unified config version. + * Version 2 = YAML unified format + * Version 3 = WebSearch config with model configuration for Gemini/OpenCode + * Version 4 = Copilot API integration (GitHub Copilot proxy) + * Version 5 = Remote proxy configuration (connect to remote CLIProxyAPI) + * Version 6 = Customizable auth tokens (API key and management secret) + * Version 7 = Quota management for hybrid auto+manual account control + * Version 8 = Thinking/reasoning budget configuration + * Version 9 = Real WebSearch backends (DuckDuckGo/Brave) with legacy CLI fallback + * Version 10 = Exa + Tavily WebSearch backends + * Version 11 = Discord Channels runtime auto-enable preferences + * Version 12 = Official Channels multi-provider support (Telegram, Discord, iMessage) + * Version 13 = Browser automation defaults to safe manual/off exposure + */ +export const UNIFIED_CONFIG_VERSION = 13; diff --git a/src/config/schemas/websearch.ts b/src/config/schemas/websearch.ts new file mode 100644 index 00000000..d2714c41 --- /dev/null +++ b/src/config/schemas/websearch.ts @@ -0,0 +1,148 @@ +/** + * WebSearch backend configuration types. + * + * Covers all supported search backends: + * - API-backed: Exa, Tavily, Brave + * - Self-hosted: SearXNG + * - Zero-setup: DuckDuckGo + * - Legacy CLI fallbacks: Gemini, Grok, OpenCode + */ + +/** + * DuckDuckGo WebSearch configuration. + */ +export interface DuckDuckGoWebSearchConfig { + /** Enable DuckDuckGo HTML search fallback (default: true) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Brave WebSearch configuration. + */ +export interface BraveWebSearchConfig { + /** Enable Brave Search when BRAVE_API_KEY is available (default: false) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Exa WebSearch configuration. + */ +export interface ExaWebSearchConfig { + /** Enable Exa Search when EXA_API_KEY is available (default: false) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Tavily WebSearch configuration. + */ +export interface TavilyWebSearchConfig { + /** Enable Tavily Search when TAVILY_API_KEY is available (default: false) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * SearXNG WebSearch configuration. + */ +export interface SearxngWebSearchConfig { + /** Enable SearXNG JSON search backend (default: false) */ + enabled?: boolean; + /** Base SearXNG URL, e.g. https://search.example.com (default: '') */ + url?: string; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Gemini CLI WebSearch configuration. + */ +export interface GeminiWebSearchConfig { + /** Enable Gemini CLI legacy fallback (default: false) */ + enabled?: boolean; + /** Model to use (default: gemini-2.5-flash) */ + model?: string; + /** Timeout in seconds (default: 55) */ + timeout?: number; +} + +/** + * Grok CLI WebSearch configuration. + */ +export interface GrokWebSearchConfig { + /** Enable Grok CLI legacy fallback (default: false - requires GROK_API_KEY) */ + enabled?: boolean; + /** Timeout in seconds (default: 55) */ + timeout?: number; +} + +/** + * OpenCode CLI WebSearch configuration. + */ +export interface OpenCodeWebSearchConfig { + /** Enable OpenCode CLI legacy fallback (default: false) */ + enabled?: boolean; + /** Model to use (default: opencode/grok-code) */ + model?: string; + /** Timeout in seconds (default: 60) */ + timeout?: number; +} + +/** + * WebSearch providers configuration. + * Uses deterministic search backends first, with optional legacy CLI fallback. + */ +export interface WebSearchProvidersConfig { + /** Exa Search API - API-backed search with strong relevance and content extraction */ + exa?: ExaWebSearchConfig; + /** Tavily Search API - API-backed search optimized for agent/tool usage */ + tavily?: TavilyWebSearchConfig; + /** Brave Search API - higher quality results when BRAVE_API_KEY is set */ + brave?: BraveWebSearchConfig; + /** SearXNG JSON search - self-hosted or public instance backend */ + searxng?: SearxngWebSearchConfig; + /** DuckDuckGo HTML search - zero setup default backend */ + duckduckgo?: DuckDuckGoWebSearchConfig; + /** Gemini CLI - optional legacy LLM fallback */ + gemini?: GeminiWebSearchConfig; + /** Grok CLI - optional legacy LLM fallback */ + grok?: GrokWebSearchConfig; + /** OpenCode - optional legacy LLM fallback */ + opencode?: OpenCodeWebSearchConfig; +} + +/** + * WebSearch configuration. + * Uses deterministic local backends for third-party profiles. + * Legacy AI CLI fallbacks remain available for compatibility only. + */ +export interface WebSearchConfig { + /** Master switch - enable/disable WebSearch (default: true) */ + enabled?: boolean; + /** Individual provider configurations */ + providers?: WebSearchProvidersConfig; + // Legacy fields (deprecated, kept for backwards compatibility) + /** @deprecated Use providers.gemini instead */ + gemini?: { + enabled?: boolean; + timeout?: number; + }; + /** @deprecated Unused */ + mode?: 'sequential' | 'parallel'; + /** @deprecated Unused */ + provider?: 'auto' | 'web-search-prime' | 'brave' | 'tavily'; + /** @deprecated Unused */ + fallback?: boolean; + /** @deprecated Unused */ + webSearchPrimeUrl?: string; + /** @deprecated Unused */ + selectedProviders?: string[]; + /** @deprecated Unused */ + customMcp?: unknown[]; +} diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index ab4eca6c..ec5eef2f 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -7,1122 +7,8 @@ * - *.settings.json (env vars) * * Into a single config.yaml structure. - */ - -import type { TargetType } from '../targets/target-adapter'; -import type { CLIProxyProvider, CliproxyRoutingStrategy } from '../cliproxy/types'; -import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities'; - -/** - * Unified config version. - * Version 2 = YAML unified format - * Version 3 = WebSearch config with model configuration for Gemini/OpenCode - * Version 4 = Copilot API integration (GitHub Copilot proxy) - * Version 5 = Remote proxy configuration (connect to remote CLIProxyAPI) - * Version 6 = Customizable auth tokens (API key and management secret) - * Version 7 = Quota management for hybrid auto+manual account control - * Version 8 = Thinking/reasoning budget configuration - * Version 9 = Real WebSearch backends (DuckDuckGo/Brave) with legacy CLI fallback - * Version 10 = Exa + Tavily WebSearch backends - * Version 11 = Discord Channels runtime auto-enable preferences - * Version 12 = Official Channels multi-provider support (Telegram, Discord, iMessage) - * Version 13 = Browser automation defaults to safe manual/off exposure - */ -export const UNIFIED_CONFIG_VERSION = 13; - -/** - * Supported CLIProxy providers. - * Derived from CLIPROXY_PROVIDER_IDS — single source of truth in provider-capabilities.ts. - */ -export const CLIPROXY_SUPPORTED_PROVIDERS = CLIPROXY_PROVIDER_IDS; - -/** - * Account configuration (formerly in profiles.json). - * Represents an isolated Claude instance via CLAUDE_CONFIG_DIR. - */ -export interface AccountConfig { - /** ISO timestamp when account was created */ - created: string; - /** ISO timestamp of last usage, null if never used */ - last_used: string | null; - /** Context mode for project workspace data */ - context_mode?: 'isolated' | 'shared'; - /** Context-sharing group when context_mode='shared' */ - context_group?: string; - /** Shared continuity depth when context_mode='shared' */ - continuity_mode?: 'standard' | 'deeper'; - /** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */ - bare?: boolean; -} - -/** - * API-based profile configuration. - * Injects environment variables for alternative providers (GLM, Kimi, etc.). * - * Settings are stored in separate *.settings.json files (matching Claude's pattern) - * to allow users to edit them directly without touching config.yaml. + * Types have been reorganized into src/config/schemas/ for maintainability. + * This file re-exports everything for backward compatibility. */ -export interface ProfileConfig { - /** Profile type - currently only 'api' */ - type: 'api'; - /** Path to settings file (e.g., "~/.ccs/glm.settings.json") */ - settings: string; - /** Target CLI to use for this profile (default: 'claude') */ - target?: TargetType; -} - -/** - * CLIProxy OAuth account nickname mapping. - * Maps user-friendly nicknames to email addresses. - */ -export type OAuthAccounts = Record; - -/** - * CLIProxy variant configuration. - * User-defined variants of built-in OAuth providers. - * - * Settings are stored in separate *.settings.json files (matching Claude's pattern) - * to allow users to edit them directly without touching config.yaml. - */ -export interface CLIProxyVariantConfig { - /** Base provider to use */ - provider: CLIProxyProvider; - /** Account nickname (references oauth_accounts) */ - account?: string; - /** Path to settings file (e.g., "~/.ccs/gemini-custom.settings.json") */ - settings?: string; - /** Unique port for variant isolation (8318-8417) */ - port?: number; - /** Per-variant auth override (optional) */ - auth?: CLIProxyAuthConfig; - /** Target CLI to use for this variant (default: 'claude') */ - target?: TargetType; -} - -/** - * Per-tier provider+model mapping for composite variants. - */ -export interface CompositeTierConfig { - /** Provider for this tier */ - provider: CLIProxyProvider; - /** Model ID to use for this tier */ - model: string; - /** Account nickname (optional, references oauth_accounts) */ - account?: string; - /** Fallback provider+model if primary fails */ - fallback?: { - provider: CLIProxyProvider; - model: string; - account?: string; - }; - /** Per-tier thinking budget override (e.g. 'xhigh', 'medium', 'off') */ - thinking?: string; -} - -/** - * Composite variant configuration. - * Mixes different providers per Claude tier (opus, sonnet, haiku) in a single profile. - * Uses CLIProxyAPI root endpoints (/v1/messages) for model-based routing - * instead of provider-specific endpoints (/api/provider/{provider}). - */ -export interface CompositeVariantConfig { - /** Discriminator for composite type */ - type: 'composite'; - /** Which tier ANTHROPIC_MODEL equals (default must be one of the three) */ - default_tier: 'opus' | 'sonnet' | 'haiku'; - /** Per-tier provider+model mapping */ - tiers: { - opus: CompositeTierConfig; - sonnet: CompositeTierConfig; - haiku: CompositeTierConfig; - }; - /** Path to settings file */ - settings?: string; - /** Shared port for the composite profile */ - port?: number; - /** Per-variant auth override (optional) */ - auth?: CLIProxyAuthConfig; - /** Target CLI to use for this composite variant (default: 'claude') */ - target?: TargetType; -} - -/** - * CLIProxy authentication configuration. - * Allows customization of API key and management secret for CLIProxyAPI. - */ -export interface CLIProxyAuthConfig { - /** API key for CCS-managed requests (default: 'ccs-internal-managed') */ - api_key?: string; - /** Management secret for Control Panel login (default: 'ccs') */ - management_secret?: string; -} - -/** - * CLIProxy logging configuration. - * Controls whether CLIProxyAPI writes logs to disk. - * Logs can grow to several GB if left enabled. - */ -export interface CLIProxyLoggingConfig { - /** Enable logging to file (default: false to prevent disk bloat) */ - enabled?: boolean; - /** Enable request logging for debugging (default: false) */ - request_log?: boolean; -} - -/** - * CLIProxy safety configuration. - * Controls high-risk flow safeguards for supported providers. - */ -export interface CLIProxySafetyConfig { - /** Allow skipping AGY responsibility checks and Gemini dashboard typed acknowledgement */ - antigravity_ack_bypass?: boolean; -} - -/** - * Default CLIProxy safety configuration. - */ -export const DEFAULT_CLIPROXY_SAFETY_CONFIG: CLIProxySafetyConfig = { - antigravity_ack_bypass: false, -}; - -/** - * Token refresh configuration. - * Manages background token refresh worker settings. - */ -export interface TokenRefreshSettings { - /** Enable background token refresh (default: false) */ - enabled?: boolean; - /** Refresh check interval in minutes (default: 30) */ - interval_minutes?: number; - /** Preemptive refresh time in minutes (default: 45) */ - preemptive_minutes?: number; - /** Maximum retry attempts per token (default: 3) */ - max_retries?: number; - /** Enable verbose logging (default: false) */ - verbose?: boolean; -} - -export interface CLIProxyRoutingConfig { - /** Credential selection strategy when multiple accounts match */ - strategy?: CliproxyRoutingStrategy; - /** Keep one conversation pinned to the same account when possible */ - session_affinity?: boolean; - /** Go-style duration for session-affinity binding retention */ - session_affinity_ttl?: string; -} - -/** - * CLIProxy configuration section. - */ -export interface CLIProxyConfig { - /** Backend selection: 'original' or 'plus' (default: 'original') */ - backend?: 'original' | 'plus'; - /** Nickname to email mapping for OAuth accounts */ - oauth_accounts: OAuthAccounts; - /** Built-in providers (read-only, for reference) */ - providers: readonly string[]; - /** User-defined provider variants (single-provider or composite) */ - variants: Record; - /** Logging configuration (disabled by default) */ - logging?: CLIProxyLoggingConfig; - /** Safety controls for high-risk provider flows */ - safety?: CLIProxySafetyConfig; - /** Kiro: disable incognito browser mode (use normal browser to save credentials) */ - kiro_no_incognito?: boolean; - /** Global auth configuration for CLIProxyAPI */ - auth?: CLIProxyAuthConfig; - /** Background token refresh worker settings */ - token_refresh?: TokenRefreshSettings; - /** Auto-sync API profiles to local CLIProxy config on settings change (default: true) */ - auto_sync?: boolean; - /** Routing strategy for multi-account CLIProxy selection */ - routing?: CLIProxyRoutingConfig; -} - -export type LoggingLevel = 'error' | 'warn' | 'info' | 'debug'; - -/** - * CCS-owned structured logging configuration. - * Separate from cliproxy.logging, which controls CLIProxy runtime files. - */ -export interface LoggingConfig { - /** Enable CCS-owned structured runtime logging */ - enabled: boolean; - /** Minimum level written to disk */ - level: LoggingLevel; - /** Rotate current log when it reaches this size in MB */ - rotate_mb: number; - /** Keep archived segments for this many days */ - retain_days: number; - /** Redact sensitive values before persistence */ - redact: boolean; - /** In-memory recent event buffer size for dashboard reads */ - live_buffer_size: number; -} - -export const DEFAULT_LOGGING_CONFIG: LoggingConfig = { - enabled: true, - level: 'info', - rotate_mb: 10, - retain_days: 7, - redact: true, - live_buffer_size: 250, -}; - -/** - * User preferences. - */ -export interface PreferencesConfig { - /** UI theme preference */ - theme?: 'light' | 'dark' | 'system'; - /** Enable anonymous telemetry */ - telemetry?: boolean; - /** Enable automatic update checks */ - auto_update?: boolean; -} - -/** - * DuckDuckGo WebSearch configuration. - */ -export interface DuckDuckGoWebSearchConfig { - /** Enable DuckDuckGo HTML search fallback (default: true) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Brave WebSearch configuration. - */ -export interface BraveWebSearchConfig { - /** Enable Brave Search when BRAVE_API_KEY is available (default: false) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Exa WebSearch configuration. - */ -export interface ExaWebSearchConfig { - /** Enable Exa Search when EXA_API_KEY is available (default: false) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Tavily WebSearch configuration. - */ -export interface TavilyWebSearchConfig { - /** Enable Tavily Search when TAVILY_API_KEY is available (default: false) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * SearXNG WebSearch configuration. - */ -export interface SearxngWebSearchConfig { - /** Enable SearXNG JSON search backend (default: false) */ - enabled?: boolean; - /** Base SearXNG URL, e.g. https://search.example.com (default: '') */ - url?: string; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Gemini CLI WebSearch configuration. - */ -export interface GeminiWebSearchConfig { - /** Enable Gemini CLI legacy fallback (default: false) */ - enabled?: boolean; - /** Model to use (default: gemini-2.5-flash) */ - model?: string; - /** Timeout in seconds (default: 55) */ - timeout?: number; -} - -/** - * Grok CLI WebSearch configuration. - */ -export interface GrokWebSearchConfig { - /** Enable Grok CLI legacy fallback (default: false - requires GROK_API_KEY) */ - enabled?: boolean; - /** Timeout in seconds (default: 55) */ - timeout?: number; -} - -/** - * OpenCode CLI WebSearch configuration. - */ -export interface OpenCodeWebSearchConfig { - /** Enable OpenCode CLI legacy fallback (default: false) */ - enabled?: boolean; - /** Model to use (default: opencode/grok-code) */ - model?: string; - /** Timeout in seconds (default: 60) */ - timeout?: number; -} - -/** - * WebSearch providers configuration. - * Uses deterministic search backends first, with optional legacy CLI fallback. - */ -export interface WebSearchProvidersConfig { - /** Exa Search API - API-backed search with strong relevance and content extraction */ - exa?: ExaWebSearchConfig; - /** Tavily Search API - API-backed search optimized for agent/tool usage */ - tavily?: TavilyWebSearchConfig; - /** Brave Search API - higher quality results when BRAVE_API_KEY is set */ - brave?: BraveWebSearchConfig; - /** SearXNG JSON search - self-hosted or public instance backend */ - searxng?: SearxngWebSearchConfig; - /** DuckDuckGo HTML search - zero setup default backend */ - duckduckgo?: DuckDuckGoWebSearchConfig; - /** Gemini CLI - optional legacy LLM fallback */ - gemini?: GeminiWebSearchConfig; - /** Grok CLI - optional legacy LLM fallback */ - grok?: GrokWebSearchConfig; - /** OpenCode - optional legacy LLM fallback */ - opencode?: OpenCodeWebSearchConfig; -} - -/** - * Copilot API account type. - */ -export type CopilotAccountType = 'individual' | 'business' | 'enterprise'; - -/** - * Copilot API configuration. - * Enables GitHub Copilot subscription usage via copilot-api proxy. - * Strictly opt-in - disabled by default. - * - * !! DISCLAIMER - USE AT YOUR OWN RISK !! - * This uses an UNOFFICIAL reverse-engineered API. - * Excessive usage may trigger GitHub account restrictions. - * CCS provides NO WARRANTY and accepts NO RESPONSIBILITY for any consequences. - */ -export interface CopilotConfig { - /** Enable Copilot integration (default: false) - must be explicitly enabled */ - enabled: boolean; - /** Auto-start copilot-api daemon when using profile (default: false) */ - auto_start: boolean; - /** Port for copilot-api proxy (default: 4141) */ - port: number; - /** GitHub Copilot account type (default: individual) */ - account_type: CopilotAccountType; - /** Rate limit in seconds between requests (null = no limit) */ - rate_limit: number | null; - /** Wait instead of error when rate limit is hit (default: true) */ - wait_on_limit: boolean; - /** Default model ID (e.g., claude-sonnet-4.5) */ - model: string; - /** Model mapping for Claude tiers - maps opus/sonnet/haiku to specific models */ - opus_model?: string; - sonnet_model?: string; - haiku_model?: string; -} - -/** - * Cursor IDE integration configuration. - * Enables Cursor IDE usage via cursor proxy daemon. - */ -export interface CursorConfig { - /** Enable Cursor integration (default: false) */ - enabled: boolean; - /** Port for cursor proxy daemon (default: 20129) */ - port: number; - /** Auto-start daemon when CCS starts (default: false) */ - auto_start: boolean; - /** Enable ghost mode to disable telemetry (default: true) */ - ghost_mode: boolean; - /** Default model ID used by Cursor integration */ - model: string; - /** Optional tier mapping for Claude-compatible model routing */ - opus_model?: string; - /** Optional tier mapping for Claude-compatible model routing */ - sonnet_model?: string; - /** Optional tier mapping for Claude-compatible model routing */ - haiku_model?: string; -} - -/** - * Remote proxy configuration. - * Connect to a remote CLIProxyAPI instance instead of spawning local binary. - */ -export interface ProxyRemoteConfig { - /** Enable remote proxy mode (default: false = local mode) */ - enabled: boolean; - /** Remote proxy hostname or IP (empty = not configured) */ - host: string; - /** - * Remote proxy port. - * Optional - defaults based on protocol: - * - HTTPS: 443 - * - HTTP: 8317 - * When empty/undefined, uses protocol default. - */ - port?: number; - /** Protocol for remote connection */ - protocol: 'http' | 'https'; - /** Auth token for remote proxy API endpoints (optional, sent as header) */ - auth_token: string; - /** - * Management key for remote proxy management API endpoints. - * CLIProxyAPI uses separate authentication for management endpoints - * (/v0/management/*) via 'secret-key' config. - * If not set, falls back to auth_token for backwards compatibility. - */ - management_key?: string; - /** Connection timeout in milliseconds (default: 2000) */ - timeout?: number; - /** Enable auto-sync profiles to remote on settings change (default: false) */ - auto_sync?: boolean; -} - -/** - * Fallback configuration when remote proxy is unreachable. - */ -export interface ProxyFallbackConfig { - /** Enable fallback to local proxy (default: true) */ - enabled: boolean; - /** Auto-start local proxy without prompting (default: false = prompt user) */ - auto_start: boolean; -} - -/** - * Local proxy configuration. - */ -export interface ProxyLocalConfig { - /** Local proxy port (default: 8317) */ - port: number; - /** Auto-start local binary (default: true) */ - auto_start: boolean; -} - -export interface OpenAICompatProxyRoutingConfig { - default?: string; - background?: string; - think?: string; - longContext?: string; - webSearch?: string; - longContextThreshold?: number; -} - -export interface OpenAICompatProxyConfig { - /** Default local port for OpenAI-compatible proxy instances */ - port?: number; - /** Optional profile-scoped local port overrides */ - profile_ports?: Record; - routing?: OpenAICompatProxyRoutingConfig; -} - -/** - * CLIProxy server configuration section. - * Controls whether CCS uses local or remote CLIProxyAPI instance. - */ -export interface CliproxyServerConfig { - /** Remote proxy settings */ - remote: ProxyRemoteConfig; - /** Fallback behavior when remote is unreachable */ - fallback: ProxyFallbackConfig; - /** Local proxy settings */ - local: ProxyLocalConfig; -} - -/** - * Global environment variables configuration. - * These env vars are injected into ALL non-Claude subscription profiles. - * Useful for disabling telemetry, bug commands, error reporting, etc. - */ -export interface GlobalEnvConfig { - /** Enable global env injection (default: true) */ - enabled: boolean; - /** Environment variables to inject */ - env: Record; -} - -/** - * Cross-profile continuity inheritance configuration. - * Maps execution profile names to source account profiles for CLAUDE_CONFIG_DIR reuse. - */ -export interface ContinuityConfig { - /** Profile name -> source account profile name */ - inherit_from_account?: Record; -} - -/** - * Default global env vars for third-party profiles. - * These disable Claude Code telemetry/reporting since we're using proxy. - */ -export const DEFAULT_GLOBAL_ENV: Record = { - DISABLE_BUG_COMMAND: '1', - DISABLE_ERROR_REPORTING: '1', - DISABLE_TELEMETRY: '1', -}; - -/** - * WebSearch configuration. - * Uses deterministic local backends for third-party profiles. - * Legacy AI CLI fallbacks remain available for compatibility only. - */ -export interface WebSearchConfig { - /** Master switch - enable/disable WebSearch (default: true) */ - enabled?: boolean; - /** Individual provider configurations */ - providers?: WebSearchProvidersConfig; - // Legacy fields (deprecated, kept for backwards compatibility) - /** @deprecated Use providers.gemini instead */ - gemini?: { - enabled?: boolean; - timeout?: number; - }; - /** @deprecated Unused */ - mode?: 'sequential' | 'parallel'; - /** @deprecated Unused */ - provider?: 'auto' | 'web-search-prime' | 'brave' | 'tavily'; - /** @deprecated Unused */ - fallback?: boolean; - /** @deprecated Unused */ - webSearchPrimeUrl?: string; - /** @deprecated Unused */ - selectedProviders?: string[]; - /** @deprecated Unused */ - customMcp?: unknown[]; -} - -// ============================================================================ -// QUOTA MANAGEMENT CONFIGURATION (v7+) -// ============================================================================ - -/** - * Auto quota management configuration. - * Controls automatic failover behavior. - */ -export interface AutoQuotaConfig { - /** Enable pre-flight quota check before requests (default: true) */ - preflight_check: boolean; - /** Quota percentage below which account is "exhausted" (default: 5) */ - exhaustion_threshold: number; - /** Tier priority for failover, highest to lowest (default: ['paid']) */ - tier_priority: string[]; - /** Minutes to skip exhausted account before retry (default: 5) */ - cooldown_minutes: number; -} - -/** - * Runtime quota monitor configuration. - * Controls adaptive polling during active sessions. - */ -export interface RuntimeMonitorConfig { - /** Enable runtime monitoring during sessions (default: true) */ - enabled: boolean; - /** Poll interval in seconds when quota > warn_threshold (default: 300) */ - normal_interval_seconds: number; - /** Poll interval in seconds when quota <= warn_threshold (default: 60) */ - critical_interval_seconds: number; - /** Quota percentage that triggers fast polling + warning (default: 20) */ - warn_threshold: number; - /** Quota percentage that triggers cooldown + switch (default: 5) */ - exhaustion_threshold: number; - /** Minutes to cooldown exhausted account (default: 5) */ - cooldown_minutes: number; -} - -/** - * Manual quota management configuration. - * User-controlled overrides for account selection. - */ -export interface ManualQuotaConfig { - /** User-paused accounts (stored in accounts.json) */ - paused_accounts: string[]; - /** Force use of specific account (overrides auto-selection) */ - forced_default: string | null; - /** Lock to specific tier only */ - tier_lock: string | null; -} - -/** - * Quota management mode. - * - auto: Fully automatic failover based on quota - * - manual: User controls everything, no auto-switching - * - hybrid: Auto-failover with user overrides (default) - */ -export type QuotaManagementMode = 'auto' | 'manual' | 'hybrid'; - -/** - * Quota management configuration section. - * Controls hybrid auto+manual account selection for multi-account setups. - */ -export interface QuotaManagementConfig { - /** Management mode (default: hybrid) */ - mode: QuotaManagementMode; - /** Auto mode settings */ - auto: AutoQuotaConfig; - /** Manual mode settings */ - manual: ManualQuotaConfig; - /** Runtime monitor settings */ - runtime_monitor: RuntimeMonitorConfig; -} - -/** - * Default auto quota configuration. - */ -export const DEFAULT_AUTO_QUOTA_CONFIG: AutoQuotaConfig = { - preflight_check: true, - exhaustion_threshold: 5, - tier_priority: ['ultra', 'pro', 'free'], - cooldown_minutes: 5, -}; - -/** - * Default manual quota configuration. - */ -export const DEFAULT_MANUAL_QUOTA_CONFIG: ManualQuotaConfig = { - paused_accounts: [], - forced_default: null, - tier_lock: null, -}; - -/** - * Default runtime monitor configuration. - */ -export const DEFAULT_RUNTIME_MONITOR_CONFIG: RuntimeMonitorConfig = { - enabled: true, - normal_interval_seconds: 300, - critical_interval_seconds: 60, - warn_threshold: 20, - exhaustion_threshold: 5, - cooldown_minutes: 5, -}; - -/** - * Default quota management configuration. - */ -export const DEFAULT_QUOTA_MANAGEMENT_CONFIG: QuotaManagementConfig = { - mode: 'hybrid', - auto: { ...DEFAULT_AUTO_QUOTA_CONFIG }, - manual: { ...DEFAULT_MANUAL_QUOTA_CONFIG }, - runtime_monitor: { ...DEFAULT_RUNTIME_MONITOR_CONFIG }, -}; - -// ============================================================================ -// THINKING CONFIGURATION (v8+) -// ============================================================================ - -/** - * Thinking mode for auto/manual/off control. - * - auto: Apply tier-based defaults (opus→high, sonnet→medium, haiku→low) - * - off: Disable thinking entirely - * - manual: Use explicit override value - */ -export type ThinkingMode = 'auto' | 'off' | 'manual'; - -/** - * Tier-to-thinking level defaults. - * Maps Claude tier names to thinking level names. - */ -export interface ThinkingTierDefaults { - /** Thinking level for opus tier (default: 'high') */ - opus: string; - /** Thinking level for sonnet tier (default: 'medium') */ - sonnet: string; - /** Thinking level for haiku tier (default: 'low') */ - haiku: string; -} - -/** - * Thinking configuration section. - * Controls thinking/reasoning budget injection for CLIProxy providers. - */ -export interface ThinkingConfig { - /** Thinking mode (default: 'auto') */ - mode: ThinkingMode; - /** Manual override value (level name or budget number) */ - override?: string | number; - /** Tier-to-level mapping */ - tier_defaults: ThinkingTierDefaults; - /** Per-provider overrides (e.g., { gemini: { opus: 'high' } }) */ - provider_overrides?: Record>; - /** Show warning when values are clamped (default: true) */ - show_warnings?: boolean; -} - -/** - * Default thinking tier defaults. - */ -export const DEFAULT_THINKING_TIER_DEFAULTS: ThinkingTierDefaults = { - opus: 'high', - sonnet: 'medium', - haiku: 'low', -}; - -/** - * Default thinking configuration. - */ -export const DEFAULT_THINKING_CONFIG: ThinkingConfig = { - mode: 'auto', - tier_defaults: { ...DEFAULT_THINKING_TIER_DEFAULTS }, - show_warnings: true, -}; - -/** - * Supported Anthropic official channel IDs. - */ -export type OfficialChannelId = 'telegram' | 'discord' | 'imessage'; - -/** - * Official Channels configuration. - * Controls runtime-only injection of Anthropic's official channel plugins. - */ -export interface OfficialChannelsConfig { - /** Selected official channels to auto-enable for compatible sessions */ - selected: OfficialChannelId[]; - /** Also add --dangerously-skip-permissions when auto-enable is active */ - unattended: boolean; -} - -/** - * Default Official Channels configuration. - * Disabled by default because the feature requires explicit user setup. - */ -export const DEFAULT_OFFICIAL_CHANNELS_CONFIG: OfficialChannelsConfig = { - selected: [], - unattended: false, -}; - -/** - * Dashboard authentication configuration. - * Optional login protection for CCS dashboard. - * Disabled by default for backward compatibility. - */ -export interface DashboardAuthConfig { - /** Enable dashboard authentication (default: false) */ - enabled: boolean; - /** Username for dashboard login */ - username: string; - /** Bcrypt-hashed password (use: npx bcrypt-cli hash 'password') */ - password_hash: string; - /** Session timeout in hours (default: 24) */ - session_timeout_hours?: number; -} - -/** - * Default dashboard auth configuration. - * Disabled by default - must be explicitly enabled. - */ -export const DEFAULT_DASHBOARD_AUTH_CONFIG: DashboardAuthConfig = { - enabled: false, - username: '', - password_hash: '', - session_timeout_hours: 24, -}; - -/** - * Browser automation configuration. - * Controls Claude browser attach and Codex browser tooling. - */ -export type BrowserToolPolicy = 'auto' | 'manual'; -export type BrowserEvalMode = 'disabled' | 'readonly' | 'readwrite'; - -export interface BrowserClaudeConfig { - /** Enable Claude browser attach (default: false) */ - enabled: boolean; - /** Control whether Claude browser attach is exposed automatically or only via --browser */ - policy: BrowserToolPolicy; - /** Chrome user-data directory used for attach mode */ - user_data_dir: string; - /** DevTools port used for attach mode (default: 9222) */ - devtools_port: number; - /** Eval access mode exposed through browser settings/status surfaces */ - eval_mode?: BrowserEvalMode; -} - -export interface BrowserCodexConfig { - /** Enable Codex browser tooling injection (default: false) */ - enabled: boolean; - /** Control whether Codex browser tooling is exposed automatically or only via --browser */ - policy: BrowserToolPolicy; - /** Eval access mode exposed through browser settings/status surfaces */ - eval_mode?: BrowserEvalMode; -} - -export interface BrowserConfig { - claude: BrowserClaudeConfig; - codex: BrowserCodexConfig; -} - -export const DEFAULT_BROWSER_CONFIG: BrowserConfig = { - claude: { - enabled: false, - policy: 'manual', - user_data_dir: '', - devtools_port: 9222, - eval_mode: 'readonly', - }, - codex: { - enabled: false, - policy: 'manual', - eval_mode: 'readonly', - }, -}; - -/** - * Image analysis configuration. - * Routes image/PDF files through CLIProxy for vision analysis. - */ -export interface ImageAnalysisConfig { - /** Enable image analysis via CLIProxy (default: true) */ - enabled: boolean; - /** Timeout in seconds (default: 60) */ - timeout: number; - /** Provider-to-model mapping for vision analysis */ - provider_models: Record; - /** Fallback backend used when a profile does not resolve to a provider-specific backend */ - fallback_backend?: string; - /** Explicit profile-name-to-backend overrides for settings/custom aliases */ - profile_backends?: Record; -} - -/** - * Default image analysis configuration. - * Enabled by default for CLIProxy providers with vision support. - */ -export const DEFAULT_IMAGE_ANALYSIS_CONFIG: ImageAnalysisConfig = { - enabled: true, - timeout: 60, - provider_models: { - agy: 'gemini-3-1-flash-preview', - gemini: 'gemini-3-flash-preview', - codex: 'gpt-5.1-codex-mini', - kiro: 'kiro-claude-haiku-4-5', - ghcp: 'claude-haiku-4.5', - claude: 'claude-haiku-4-5-20251001', - // 'vision-model' is a generic placeholder - users can override via config.yaml - qwen: 'vision-model', - iflow: 'qwen3-vl-plus', - kimi: 'vision-model', - }, - fallback_backend: 'gemini', - profile_backends: {}, -}; - -/** - * Main unified configuration structure. - * Stored in ~/.ccs/config.yaml - */ -export interface UnifiedConfig { - /** Config version (7 for quota management) */ - version: number; - /** Flag indicating setup wizard has been completed */ - setup_completed?: boolean; - /** Default profile name to use when none specified */ - default?: string; - /** Account-based profiles (isolated Claude instances) */ - accounts: Record; - /** API-based profiles (env var injection) */ - profiles: Record; - /** CLIProxy configuration */ - cliproxy: CLIProxyConfig; - /** OpenAI-compatible local proxy configuration */ - proxy?: OpenAICompatProxyConfig; - /** CCS-owned structured logging configuration */ - logging?: LoggingConfig; - /** User preferences */ - preferences: PreferencesConfig; - /** WebSearch configuration */ - websearch?: WebSearchConfig; - /** Global environment variables for all non-Claude subscription profiles */ - global_env?: GlobalEnvConfig; - /** Cross-profile continuity inheritance mapping */ - continuity?: ContinuityConfig; - /** Copilot API configuration (GitHub Copilot proxy) */ - copilot?: CopilotConfig; - /** Cursor IDE configuration (Cursor proxy daemon) */ - cursor?: CursorConfig; - /** CLIProxy server configuration for remote/local mode */ - cliproxy_server?: CliproxyServerConfig; - /** Quota management configuration (v7+) */ - quota_management?: QuotaManagementConfig; - /** Thinking/reasoning budget configuration (v8+) */ - thinking?: ThinkingConfig; - /** Discord Channels runtime auto-enable preferences (v11+) */ - channels?: OfficialChannelsConfig; - /** Dashboard authentication configuration (optional) */ - dashboard_auth?: DashboardAuthConfig; - /** Browser automation configuration */ - browser?: BrowserConfig; - /** Image analysis configuration (vision via CLIProxy) */ - image_analysis?: ImageAnalysisConfig; -} - -/** - * Default Copilot configuration. - * Strictly opt-in - disabled by default. - * Uses gpt-4.1 as default model (free tier compatible). - */ -export const DEFAULT_COPILOT_CONFIG: CopilotConfig = { - enabled: false, - auto_start: false, - port: 4141, - account_type: 'individual', - rate_limit: null, - wait_on_limit: true, - model: 'gpt-4.1', // Free tier compatible -}; - -/** - * Default Cursor configuration. - * Disabled by default, ghost mode enabled for privacy. - */ -export const DEFAULT_CURSOR_CONFIG: CursorConfig = { - enabled: false, - port: 20129, - auto_start: false, - ghost_mode: true, - model: 'gpt-5.3-codex', -}; - -/** - * Default CLIProxy server configuration. - * Local mode by default - remote must be explicitly enabled. - * Port is optional for remote - defaults based on protocol. - */ -export const DEFAULT_CLIPROXY_SERVER_CONFIG: CliproxyServerConfig = { - remote: { - enabled: false, - host: '', - // port is intentionally omitted - will use protocol default (443 for HTTPS, 8317 for HTTP) - protocol: 'http', - auth_token: '', - }, - fallback: { - enabled: true, - auto_start: false, - }, - local: { - port: 8317, - auto_start: true, - }, -}; - -export const DEFAULT_OPENAI_COMPAT_PROXY_CONFIG: OpenAICompatProxyConfig = { - profile_ports: {}, - routing: { - longContextThreshold: 60_000, - }, -}; - -/** - * Create an empty unified config with defaults. - */ -export function createEmptyUnifiedConfig(): UnifiedConfig { - return { - version: UNIFIED_CONFIG_VERSION, - default: undefined, - accounts: {}, - profiles: {}, - cliproxy: { - backend: 'original', - oauth_accounts: {}, - providers: [...CLIPROXY_SUPPORTED_PROVIDERS], - variants: {}, - logging: { - enabled: false, - request_log: false, - }, - safety: { ...DEFAULT_CLIPROXY_SAFETY_CONFIG }, - auto_sync: true, - routing: { - strategy: 'round-robin', - session_affinity: false, - session_affinity_ttl: '1h', - }, - }, - proxy: { - port: DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.port, - profile_ports: { ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.profile_ports }, - routing: { - ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.routing, - }, - }, - logging: { ...DEFAULT_LOGGING_CONFIG }, - preferences: { - theme: 'system', - telemetry: false, - auto_update: true, - }, - websearch: { - enabled: true, - providers: { - exa: { - enabled: false, - max_results: 5, - }, - tavily: { - enabled: false, - max_results: 5, - }, - brave: { - enabled: false, - max_results: 5, - }, - searxng: { - enabled: false, - url: '', - max_results: 5, - }, - duckduckgo: { - enabled: true, - max_results: 5, - }, - gemini: { - enabled: false, - model: 'gemini-2.5-flash', - timeout: 55, - }, - opencode: { - enabled: false, - model: 'opencode/grok-code', - timeout: 90, - }, - grok: { - enabled: false, - timeout: 55, - }, - }, - }, - global_env: { - enabled: true, - env: { ...DEFAULT_GLOBAL_ENV }, - }, - copilot: { ...DEFAULT_COPILOT_CONFIG }, - cursor: { ...DEFAULT_CURSOR_CONFIG }, - cliproxy_server: { ...DEFAULT_CLIPROXY_SERVER_CONFIG }, - quota_management: { ...DEFAULT_QUOTA_MANAGEMENT_CONFIG }, - thinking: { ...DEFAULT_THINKING_CONFIG }, - channels: { ...DEFAULT_OFFICIAL_CHANNELS_CONFIG }, - dashboard_auth: { ...DEFAULT_DASHBOARD_AUTH_CONFIG }, - browser: { - claude: { ...DEFAULT_BROWSER_CONFIG.claude }, - codex: { ...DEFAULT_BROWSER_CONFIG.codex }, - }, - image_analysis: { ...DEFAULT_IMAGE_ANALYSIS_CONFIG }, - }; -} - -/** - * Type guard for UnifiedConfig. - * Relaxed validation: accepts configs with version >= 1 and any subset of sections. - * Missing sections will be filled with defaults during merge. - */ -export function isUnifiedConfig(obj: unknown): obj is UnifiedConfig { - if (typeof obj !== 'object' || obj === null) return false; - const config = obj as Record; - // Only require version to be a number >= 1 (allow future versions) - // Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig - return typeof config.version === 'number' && config.version >= 1; -} +export * from './schemas/index'; From 06bce198eb8b9c6508e7b5fc3339cbda54faf608 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 13:57:08 -0400 Subject: [PATCH 03/12] Revert "refactor(config): reorganize unified-config-types into schemas directory" This reverts commit 51df0ee55b52ecc858544ec8d8ff7e7932db0f11. --- .../__tests__/schemas-reexport.test.ts | 198 --- src/config/schemas/auth.ts | 109 -- src/config/schemas/browser.ts | 71 -- src/config/schemas/channels.ts | 32 - src/config/schemas/cliproxy.ts | 151 --- src/config/schemas/copilot-cursor.ts | 93 -- src/config/schemas/index.ts | 112 -- src/config/schemas/logging.ts | 53 - src/config/schemas/providers.ts | 30 - src/config/schemas/proxy-server.ts | 193 --- src/config/schemas/quota.ts | 121 -- src/config/schemas/thinking.ts | 66 - src/config/schemas/unified-config.ts | 200 --- src/config/schemas/version.ts | 23 - src/config/schemas/websearch.ts | 148 --- src/config/unified-config-types.ts | 1122 ++++++++++++++++- 16 files changed, 1118 insertions(+), 1604 deletions(-) delete mode 100644 src/config/schemas/__tests__/schemas-reexport.test.ts delete mode 100644 src/config/schemas/auth.ts delete mode 100644 src/config/schemas/browser.ts delete mode 100644 src/config/schemas/channels.ts delete mode 100644 src/config/schemas/cliproxy.ts delete mode 100644 src/config/schemas/copilot-cursor.ts delete mode 100644 src/config/schemas/index.ts delete mode 100644 src/config/schemas/logging.ts delete mode 100644 src/config/schemas/providers.ts delete mode 100644 src/config/schemas/proxy-server.ts delete mode 100644 src/config/schemas/quota.ts delete mode 100644 src/config/schemas/thinking.ts delete mode 100644 src/config/schemas/unified-config.ts delete mode 100644 src/config/schemas/version.ts delete mode 100644 src/config/schemas/websearch.ts diff --git a/src/config/schemas/__tests__/schemas-reexport.test.ts b/src/config/schemas/__tests__/schemas-reexport.test.ts deleted file mode 100644 index 5bdda5d9..00000000 --- a/src/config/schemas/__tests__/schemas-reexport.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -/** - * Tests: config schemas re-export backward compatibility. - * - * Verifies that every type, interface, constant, and function originally - * exported from unified-config-types.ts is still accessible via both - * the barrel file and the schemas/index barrel. - */ - -import { describe, it, expect } from 'bun:test'; - -// Import from the backward-compatible barrel (this is what all existing code uses) -import * as barrel from '../../unified-config-types'; - -// Import from the new schemas barrel (this is what the barrel delegates to) -import * as schemas from '../index'; - -// --------------------------------------------------------------------------- -// Type-level checks (compile-time, not runtime) -// --------------------------------------------------------------------------- - -// Verify key interfaces are accessible as types -import type { - UnifiedConfig, - AccountConfig, - ProfileConfig, - OAuthAccounts, - CLIProxyAuthConfig, - TokenRefreshSettings, - DashboardAuthConfig, - CLIProxyVariantConfig, - CompositeTierConfig, - CompositeVariantConfig, - CLIProxyLoggingConfig, - CLIProxySafetyConfig, - CLIProxyRoutingConfig, - CLIProxyConfig, - AutoQuotaConfig, - RuntimeMonitorConfig, - ManualQuotaConfig, - QuotaManagementMode, - QuotaManagementConfig, - ThinkingMode, - ThinkingTierDefaults, - ThinkingConfig, - OfficialChannelId, - OfficialChannelsConfig, - DuckDuckGoWebSearchConfig, - BraveWebSearchConfig, - ExaWebSearchConfig, - TavilyWebSearchConfig, - SearxngWebSearchConfig, - GeminiWebSearchConfig, - GrokWebSearchConfig, - OpenCodeWebSearchConfig, - WebSearchProvidersConfig, - WebSearchConfig, - BrowserToolPolicy, - BrowserEvalMode, - BrowserClaudeConfig, - BrowserCodexConfig, - BrowserConfig, - LoggingLevel, - LoggingConfig, - PreferencesConfig, - CopilotAccountType, - CopilotConfig, - CursorConfig, - ProxyRemoteConfig, - ProxyFallbackConfig, - ProxyLocalConfig, - OpenAICompatProxyRoutingConfig, - OpenAICompatProxyConfig, - CliproxyServerConfig, - GlobalEnvConfig, - ContinuityConfig, - ImageAnalysisConfig, -} from '../../unified-config-types'; - -describe('config schemas backward compatibility', () => { - // ------------------------------------------------------------------------- - // Constants - // ------------------------------------------------------------------------- - it('re-exports UNIFIED_CONFIG_VERSION', () => { - expect(barrel.UNIFIED_CONFIG_VERSION).toBe(13); - expect(schemas.UNIFIED_CONFIG_VERSION).toBe(13); - }); - - it('re-exports CLIPROXY_SUPPORTED_PROVIDERS', () => { - expect(Array.isArray(barrel.CLIPROXY_SUPPORTED_PROVIDERS)).toBe(true); - expect(barrel.CLIPROXY_SUPPORTED_PROVIDERS.length).toBeGreaterThan(0); - expect(barrel.CLIPROXY_SUPPORTED_PROVIDERS).toEqual(schemas.CLIPROXY_SUPPORTED_PROVIDERS); - }); - - // ------------------------------------------------------------------------- - // Default constants - // ------------------------------------------------------------------------- - const defaultConstants = [ - 'DEFAULT_CLIPROXY_SAFETY_CONFIG', - 'DEFAULT_LOGGING_CONFIG', - 'DEFAULT_OFFICIAL_CHANNELS_CONFIG', - 'DEFAULT_BROWSER_CONFIG', - 'DEFAULT_DASHBOARD_AUTH_CONFIG', - 'DEFAULT_AUTO_QUOTA_CONFIG', - 'DEFAULT_MANUAL_QUOTA_CONFIG', - 'DEFAULT_RUNTIME_MONITOR_CONFIG', - 'DEFAULT_QUOTA_MANAGEMENT_CONFIG', - 'DEFAULT_THINKING_TIER_DEFAULTS', - 'DEFAULT_THINKING_CONFIG', - 'DEFAULT_COPILOT_CONFIG', - 'DEFAULT_CURSOR_CONFIG', - 'DEFAULT_CLIPROXY_SERVER_CONFIG', - 'DEFAULT_OPENAI_COMPAT_PROXY_CONFIG', - 'DEFAULT_GLOBAL_ENV', - 'DEFAULT_IMAGE_ANALYSIS_CONFIG', - ] as const; - - for (const name of defaultConstants) { - it(`re-exports ${name}`, () => { - expect(barrel[name]).toBeDefined(); - expect(barrel[name]).toEqual(schemas[name]); - }); - } - - // ------------------------------------------------------------------------- - // Functions - // ------------------------------------------------------------------------- - it('re-exports createEmptyUnifiedConfig', () => { - expect(typeof barrel.createEmptyUnifiedConfig).toBe('function'); - expect(typeof schemas.createEmptyUnifiedConfig).toBe('function'); - - const config = barrel.createEmptyUnifiedConfig(); - expect(config.version).toBe(13); - expect(config.accounts).toEqual({}); - expect(config.profiles).toEqual({}); - expect(config.cliproxy).toBeDefined(); - expect(config.cliproxy.oauth_accounts).toEqual({}); - expect(config.cliproxy.variants).toEqual({}); - expect(config.logging).toBeDefined(); - expect(config.preferences).toBeDefined(); - expect(config.browser).toBeDefined(); - expect(config.image_analysis).toBeDefined(); - expect(config.quota_management).toBeDefined(); - expect(config.thinking).toBeDefined(); - expect(config.channels).toBeDefined(); - expect(config.dashboard_auth).toBeDefined(); - expect(config.copilot).toBeDefined(); - expect(config.cursor).toBeDefined(); - expect(config.cliproxy_server).toBeDefined(); - expect(config.websearch).toBeDefined(); - }); - - it('re-exports isUnifiedConfig', () => { - expect(typeof barrel.isUnifiedConfig).toBe('function'); - expect(typeof schemas.isUnifiedConfig).toBe('function'); - - expect(barrel.isUnifiedConfig({ version: 13 })).toBe(true); - expect(barrel.isUnifiedConfig(null)).toBe(false); - expect(barrel.isUnifiedConfig({})).toBe(false); - expect(barrel.isUnifiedConfig({ version: 0 })).toBe(false); - expect(barrel.isUnifiedConfig({ version: 1 })).toBe(true); - expect(barrel.isUnifiedConfig('not an object')).toBe(false); - }); - - // ------------------------------------------------------------------------- - // Barrel has all expected runtime exports (type-only exports are verified - // at compile time via the import type block above — they are erased at - // runtime and cannot be checked with the `in` operator). - // ------------------------------------------------------------------------- - const expectedRuntimeExports = [ - 'UNIFIED_CONFIG_VERSION', - 'CLIPROXY_SUPPORTED_PROVIDERS', - 'createEmptyUnifiedConfig', - 'isUnifiedConfig', - 'DEFAULT_CLIPROXY_SAFETY_CONFIG', - 'DEFAULT_LOGGING_CONFIG', - 'DEFAULT_OFFICIAL_CHANNELS_CONFIG', - 'DEFAULT_BROWSER_CONFIG', - 'DEFAULT_DASHBOARD_AUTH_CONFIG', - 'DEFAULT_AUTO_QUOTA_CONFIG', - 'DEFAULT_MANUAL_QUOTA_CONFIG', - 'DEFAULT_RUNTIME_MONITOR_CONFIG', - 'DEFAULT_QUOTA_MANAGEMENT_CONFIG', - 'DEFAULT_THINKING_TIER_DEFAULTS', - 'DEFAULT_THINKING_CONFIG', - 'DEFAULT_COPILOT_CONFIG', - 'DEFAULT_CURSOR_CONFIG', - 'DEFAULT_CLIPROXY_SERVER_CONFIG', - 'DEFAULT_OPENAI_COMPAT_PROXY_CONFIG', - 'DEFAULT_GLOBAL_ENV', - 'DEFAULT_IMAGE_ANALYSIS_CONFIG', - ] as const; - - for (const name of expectedRuntimeExports) { - it(`barrel exports "${name}"`, () => { - expect(name in barrel).toBe(true); - }); - } -}); diff --git a/src/config/schemas/auth.ts b/src/config/schemas/auth.ts deleted file mode 100644 index 4056ecf8..00000000 --- a/src/config/schemas/auth.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Account, profile, and authentication config types. - * - * Covers: - * - AccountConfig: isolated Claude instances via CLAUDE_CONFIG_DIR - * - ProfileConfig: API-based profiles (env var injection) - * - OAuthAccounts: CLIProxy nickname-to-email mapping - * - CLIProxyAuthConfig: API key and management secret customization - * - TokenRefreshSettings: background token refresh worker config - * - DashboardAuthConfig: dashboard login protection - */ - -import type { TargetType } from '../../targets/target-adapter'; - -/** - * Account configuration (formerly in profiles.json). - * Represents an isolated Claude instance via CLAUDE_CONFIG_DIR. - */ -export interface AccountConfig { - /** ISO timestamp when account was created */ - created: string; - /** ISO timestamp of last usage, null if never used */ - last_used: string | null; - /** Context mode for project workspace data */ - context_mode?: 'isolated' | 'shared'; - /** Context-sharing group when context_mode='shared' */ - context_group?: string; - /** Shared continuity depth when context_mode='shared' */ - continuity_mode?: 'standard' | 'deeper'; - /** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */ - bare?: boolean; -} - -/** - * API-based profile configuration. - * Injects environment variables for alternative providers (GLM, Kimi, etc.). - * - * Settings are stored in separate *.settings.json files (matching Claude's pattern) - * to allow users to edit them directly without touching config.yaml. - */ -export interface ProfileConfig { - /** Profile type - currently only 'api' */ - type: 'api'; - /** Path to settings file (e.g., "~/.ccs/glm.settings.json") */ - settings: string; - /** Target CLI to use for this profile (default: 'claude') */ - target?: TargetType; -} - -/** - * CLIProxy OAuth account nickname mapping. - * Maps user-friendly nicknames to email addresses. - */ -export type OAuthAccounts = Record; - -/** - * CLIProxy authentication configuration. - * Allows customization of API key and management secret for CLIProxyAPI. - */ -export interface CLIProxyAuthConfig { - /** API key for CCS-managed requests (default: 'ccs-internal-managed') */ - api_key?: string; - /** Management secret for Control Panel login (default: 'ccs') */ - management_secret?: string; -} - -/** - * Token refresh configuration. - * Manages background token refresh worker settings. - */ -export interface TokenRefreshSettings { - /** Enable background token refresh (default: false) */ - enabled?: boolean; - /** Refresh check interval in minutes (default: 30) */ - interval_minutes?: number; - /** Preemptive refresh time in minutes (default: 45) */ - preemptive_minutes?: number; - /** Maximum retry attempts per token (default: 3) */ - max_retries?: number; - /** Enable verbose logging (default: false) */ - verbose?: boolean; -} - -/** - * Dashboard authentication configuration. - * Optional login protection for CCS dashboard. - * Disabled by default for backward compatibility. - */ -export interface DashboardAuthConfig { - /** Enable dashboard authentication (default: false) */ - enabled: boolean; - /** Username for dashboard login */ - username: string; - /** Bcrypt-hashed password (use: npx bcrypt-cli hash 'password') */ - password_hash: string; - /** Session timeout in hours (default: 24) */ - session_timeout_hours?: number; -} - -/** - * Default dashboard auth configuration. - * Disabled by default - must be explicitly enabled. - */ -export const DEFAULT_DASHBOARD_AUTH_CONFIG: DashboardAuthConfig = { - enabled: false, - username: '', - password_hash: '', - session_timeout_hours: 24, -}; diff --git a/src/config/schemas/browser.ts b/src/config/schemas/browser.ts deleted file mode 100644 index f4facb4b..00000000 --- a/src/config/schemas/browser.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Browser automation configuration types and defaults. - * - * Controls Claude browser attach and Codex browser tooling. - * Version 13+ feature. - */ - -/** - * Browser tool exposure policy. - */ -export type BrowserToolPolicy = 'auto' | 'manual'; - -/** - * Browser eval access mode. - */ -export type BrowserEvalMode = 'disabled' | 'readonly' | 'readwrite'; - -/** - * Claude browser attach configuration. - */ -export interface BrowserClaudeConfig { - /** Enable Claude browser attach (default: false) */ - enabled: boolean; - /** Control whether Claude browser attach is exposed automatically or only via --browser */ - policy: BrowserToolPolicy; - /** Chrome user-data directory used for attach mode */ - user_data_dir: string; - /** DevTools port used for attach mode (default: 9222) */ - devtools_port: number; - /** Eval access mode exposed through browser settings/status surfaces */ - eval_mode?: BrowserEvalMode; -} - -/** - * Codex browser tooling configuration. - */ -export interface BrowserCodexConfig { - /** Enable Codex browser tooling injection (default: false) */ - enabled: boolean; - /** Control whether Codex browser tooling is exposed automatically or only via --browser */ - policy: BrowserToolPolicy; - /** Eval access mode exposed through browser settings/status surfaces */ - eval_mode?: BrowserEvalMode; -} - -/** - * Browser automation configuration. - * Controls Claude browser attach and Codex browser tooling. - */ -export interface BrowserConfig { - claude: BrowserClaudeConfig; - codex: BrowserCodexConfig; -} - -/** - * Default browser configuration. - */ -export const DEFAULT_BROWSER_CONFIG: BrowserConfig = { - claude: { - enabled: false, - policy: 'manual', - user_data_dir: '', - devtools_port: 9222, - eval_mode: 'readonly', - }, - codex: { - enabled: false, - policy: 'manual', - eval_mode: 'readonly', - }, -}; diff --git a/src/config/schemas/channels.ts b/src/config/schemas/channels.ts deleted file mode 100644 index f8d5274c..00000000 --- a/src/config/schemas/channels.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Official Channels configuration types and defaults. - * - * Controls runtime-only injection of Anthropic's official channel plugins - * (Telegram, Discord, iMessage). - * Version 12+ feature. - */ - -/** - * Supported Anthropic official channel IDs. - */ -export type OfficialChannelId = 'telegram' | 'discord' | 'imessage'; - -/** - * Official Channels configuration. - * Controls runtime-only injection of Anthropic's official channel plugins. - */ -export interface OfficialChannelsConfig { - /** Selected official channels to auto-enable for compatible sessions */ - selected: OfficialChannelId[]; - /** Also add --dangerously-skip-permissions when auto-enable is active */ - unattended: boolean; -} - -/** - * Default Official Channels configuration. - * Disabled by default because the feature requires explicit user setup. - */ -export const DEFAULT_OFFICIAL_CHANNELS_CONFIG: OfficialChannelsConfig = { - selected: [], - unattended: false, -}; diff --git a/src/config/schemas/cliproxy.ts b/src/config/schemas/cliproxy.ts deleted file mode 100644 index 1765335f..00000000 --- a/src/config/schemas/cliproxy.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * CLIProxy configuration types and defaults. - * - * Covers provider/variant/routing/safety/logging configuration - * for the CLIProxy integration layer. - */ - -import type { TargetType } from '../../targets/target-adapter'; -import type { CLIProxyProvider, CliproxyRoutingStrategy } from '../../cliproxy/types'; -import { CLIPROXY_PROVIDER_IDS } from '../../cliproxy/provider-capabilities'; -import type { OAuthAccounts, CLIProxyAuthConfig, TokenRefreshSettings } from './auth'; - -/** - * Supported CLIProxy providers. - * Derived from CLIPROXY_PROVIDER_IDS — single source of truth in provider-capabilities.ts. - */ -export { CLIPROXY_PROVIDER_IDS as CLIPROXY_SUPPORTED_PROVIDERS }; - -/** - * CLIProxy variant configuration. - * User-defined variants of built-in OAuth providers. - * - * Settings are stored in separate *.settings.json files (matching Claude's pattern) - * to allow users to edit them directly without touching config.yaml. - */ -export interface CLIProxyVariantConfig { - /** Base provider to use */ - provider: CLIProxyProvider; - /** Account nickname (references oauth_accounts) */ - account?: string; - /** Path to settings file (e.g., "~/.ccs/gemini-custom.settings.json") */ - settings?: string; - /** Unique port for variant isolation (8318-8417) */ - port?: number; - /** Per-variant auth override (optional) */ - auth?: CLIProxyAuthConfig; - /** Target CLI to use for this variant (default: 'claude') */ - target?: TargetType; -} - -/** - * Per-tier provider+model mapping for composite variants. - */ -export interface CompositeTierConfig { - /** Provider for this tier */ - provider: CLIProxyProvider; - /** Model ID to use for this tier */ - model: string; - /** Account nickname (optional, references oauth_accounts) */ - account?: string; - /** Fallback provider+model if primary fails */ - fallback?: { - provider: CLIProxyProvider; - model: string; - account?: string; - }; - /** Per-tier thinking budget override (e.g. 'xhigh', 'medium', 'off') */ - thinking?: string; -} - -/** - * Composite variant configuration. - * Mixes different providers per Claude tier (opus, sonnet, haiku) in a single profile. - * Uses CLIProxyAPI root endpoints (/v1/messages) for model-based routing - * instead of provider-specific endpoints (/api/provider/{provider}). - */ -export interface CompositeVariantConfig { - /** Discriminator for composite type */ - type: 'composite'; - /** Which tier ANTHROPIC_MODEL equals (default must be one of the three) */ - default_tier: 'opus' | 'sonnet' | 'haiku'; - /** Per-tier provider+model mapping */ - tiers: { - opus: CompositeTierConfig; - sonnet: CompositeTierConfig; - haiku: CompositeTierConfig; - }; - /** Path to settings file */ - settings?: string; - /** Shared port for the composite profile */ - port?: number; - /** Per-variant auth override (optional) */ - auth?: CLIProxyAuthConfig; - /** Target CLI to use for this composite variant (default: 'claude') */ - target?: TargetType; -} - -/** - * CLIProxy logging configuration. - * Controls whether CLIProxyAPI writes logs to disk. - * Logs can grow to several GB if left enabled. - */ -export interface CLIProxyLoggingConfig { - /** Enable logging to file (default: false to prevent disk bloat) */ - enabled?: boolean; - /** Enable request logging for debugging (default: false) */ - request_log?: boolean; -} - -/** - * CLIProxy safety configuration. - * Controls high-risk flow safeguards for supported providers. - */ -export interface CLIProxySafetyConfig { - /** Allow skipping AGY responsibility checks and Gemini dashboard typed acknowledgement */ - antigravity_ack_bypass?: boolean; -} - -/** - * Default CLIProxy safety configuration. - */ -export const DEFAULT_CLIPROXY_SAFETY_CONFIG: CLIProxySafetyConfig = { - antigravity_ack_bypass: false, -}; - -export interface CLIProxyRoutingConfig { - /** Credential selection strategy when multiple accounts match */ - strategy?: CliproxyRoutingStrategy; - /** Keep one conversation pinned to the same account when possible */ - session_affinity?: boolean; - /** Go-style duration for session-affinity binding retention */ - session_affinity_ttl?: string; -} - -/** - * CLIProxy configuration section. - */ -export interface CLIProxyConfig { - /** Backend selection: 'original' or 'plus' (default: 'original') */ - backend?: 'original' | 'plus'; - /** Nickname to email mapping for OAuth accounts */ - oauth_accounts: OAuthAccounts; - /** Built-in providers (read-only, for reference) */ - providers: readonly string[]; - /** User-defined provider variants (single-provider or composite) */ - variants: Record; - /** Logging configuration (disabled by default) */ - logging?: CLIProxyLoggingConfig; - /** Safety controls for high-risk provider flows */ - safety?: CLIProxySafetyConfig; - /** Kiro: disable incognito browser mode (use normal browser to save credentials) */ - kiro_no_incognito?: boolean; - /** Global auth configuration for CLIProxyAPI */ - auth?: CLIProxyAuthConfig; - /** Background token refresh worker settings */ - token_refresh?: TokenRefreshSettings; - /** Auto-sync API profiles to local CLIProxy config on settings change (default: true) */ - auto_sync?: boolean; - /** Routing strategy for multi-account CLIProxy selection */ - routing?: CLIProxyRoutingConfig; -} diff --git a/src/config/schemas/copilot-cursor.ts b/src/config/schemas/copilot-cursor.ts deleted file mode 100644 index e0b33fbb..00000000 --- a/src/config/schemas/copilot-cursor.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copilot and Cursor IDE integration configuration types and defaults. - * - * Covers: - * - CopilotConfig: GitHub Copilot proxy integration (strictly opt-in) - * - CursorConfig: Cursor IDE proxy daemon - */ - -/** - * Copilot API account type. - */ -export type CopilotAccountType = 'individual' | 'business' | 'enterprise'; - -/** - * Copilot API configuration. - * Enables GitHub Copilot subscription usage via copilot-api proxy. - * Strictly opt-in - disabled by default. - * - * !! DISCLAIMER - USE AT YOUR OWN RISK !! - * This uses an UNOFFICIAL reverse-engineered API. - * Excessive usage may trigger GitHub account restrictions. - * CCS provides NO WARRANTY and accepts NO RESPONSIBILITY for any consequences. - */ -export interface CopilotConfig { - /** Enable Copilot integration (default: false) - must be explicitly enabled */ - enabled: boolean; - /** Auto-start copilot-api daemon when using profile (default: false) */ - auto_start: boolean; - /** Port for copilot-api proxy (default: 4141) */ - port: number; - /** GitHub Copilot account type (default: individual) */ - account_type: CopilotAccountType; - /** Rate limit in seconds between requests (null = no limit) */ - rate_limit: number | null; - /** Wait instead of error when rate limit is hit (default: true) */ - wait_on_limit: boolean; - /** Default model ID (e.g., claude-sonnet-4.5) */ - model: string; - /** Model mapping for Claude tiers - maps opus/sonnet/haiku to specific models */ - opus_model?: string; - sonnet_model?: string; - haiku_model?: string; -} - -/** - * Cursor IDE integration configuration. - * Enables Cursor IDE usage via cursor proxy daemon. - */ -export interface CursorConfig { - /** Enable Cursor integration (default: false) */ - enabled: boolean; - /** Port for cursor proxy daemon (default: 20129) */ - port: number; - /** Auto-start daemon when CCS starts (default: false) */ - auto_start: boolean; - /** Enable ghost mode to disable telemetry (default: true) */ - ghost_mode: boolean; - /** Default model ID used by Cursor integration */ - model: string; - /** Optional tier mapping for Claude-compatible model routing */ - opus_model?: string; - /** Optional tier mapping for Claude-compatible model routing */ - sonnet_model?: string; - /** Optional tier mapping for Claude-compatible model routing */ - haiku_model?: string; -} - -/** - * Default Copilot configuration. - * Strictly opt-in - disabled by default. - * Uses gpt-4.1 as default model (free tier compatible). - */ -export const DEFAULT_COPILOT_CONFIG: CopilotConfig = { - enabled: false, - auto_start: false, - port: 4141, - account_type: 'individual', - rate_limit: null, - wait_on_limit: true, - model: 'gpt-4.1', -}; - -/** - * Default Cursor configuration. - * Disabled by default, ghost mode enabled for privacy. - */ -export const DEFAULT_CURSOR_CONFIG: CursorConfig = { - enabled: false, - port: 20129, - auto_start: false, - ghost_mode: true, - model: 'gpt-5.3-codex', -}; diff --git a/src/config/schemas/index.ts b/src/config/schemas/index.ts deleted file mode 100644 index ff0313f3..00000000 --- a/src/config/schemas/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Config schema barrel re-exports. - * - * All types, interfaces, constants, and functions originally in - * unified-config-types.ts are re-exported here for backward compatibility. - * Each module is responsible for a focused domain of the config schema. - */ - -// Version constant -export { UNIFIED_CONFIG_VERSION } from './version'; - -// Account, profile, OAuth, auth types -export type { - AccountConfig, - ProfileConfig, - OAuthAccounts, - CLIProxyAuthConfig, - TokenRefreshSettings, - DashboardAuthConfig, -} from './auth'; -export { DEFAULT_DASHBOARD_AUTH_CONFIG } from './auth'; - -// CLIProxy provider, variant, routing, safety, logging types -export { CLIPROXY_SUPPORTED_PROVIDERS, DEFAULT_CLIPROXY_SAFETY_CONFIG } from './cliproxy'; -export type { - CLIProxyVariantConfig, - CompositeTierConfig, - CompositeVariantConfig, - CLIProxyLoggingConfig, - CLIProxySafetyConfig, - CLIProxyRoutingConfig, - CLIProxyConfig, -} from './cliproxy'; - -// Quota management types and defaults -export { - DEFAULT_AUTO_QUOTA_CONFIG, - DEFAULT_MANUAL_QUOTA_CONFIG, - DEFAULT_RUNTIME_MONITOR_CONFIG, - DEFAULT_QUOTA_MANAGEMENT_CONFIG, -} from './quota'; -export type { - AutoQuotaConfig, - RuntimeMonitorConfig, - ManualQuotaConfig, - QuotaManagementMode, - QuotaManagementConfig, -} from './quota'; - -// Thinking/reasoning budget types and defaults -export { DEFAULT_THINKING_TIER_DEFAULTS, DEFAULT_THINKING_CONFIG } from './thinking'; -export type { ThinkingMode, ThinkingTierDefaults, ThinkingConfig } from './thinking'; - -// Official channels types and defaults -export { DEFAULT_OFFICIAL_CHANNELS_CONFIG } from './channels'; -export type { OfficialChannelId, OfficialChannelsConfig } from './channels'; - -// WebSearch backend types -export type { - DuckDuckGoWebSearchConfig, - BraveWebSearchConfig, - ExaWebSearchConfig, - TavilyWebSearchConfig, - SearxngWebSearchConfig, - GeminiWebSearchConfig, - GrokWebSearchConfig, - OpenCodeWebSearchConfig, - WebSearchProvidersConfig, - WebSearchConfig, -} from './websearch'; - -// Browser automation types and defaults -export { DEFAULT_BROWSER_CONFIG } from './browser'; -export type { - BrowserToolPolicy, - BrowserEvalMode, - BrowserClaudeConfig, - BrowserCodexConfig, - BrowserConfig, -} from './browser'; - -// Logging and preferences types and defaults -export { DEFAULT_LOGGING_CONFIG } from './logging'; -export type { LoggingLevel, LoggingConfig, PreferencesConfig } from './logging'; - -// Provider integration types and defaults -export { - DEFAULT_GLOBAL_ENV, - DEFAULT_COPILOT_CONFIG, - DEFAULT_CURSOR_CONFIG, - DEFAULT_CLIPROXY_SERVER_CONFIG, - DEFAULT_OPENAI_COMPAT_PROXY_CONFIG, - DEFAULT_IMAGE_ANALYSIS_CONFIG, -} from './providers'; -export type { - CopilotAccountType, - CopilotConfig, - CursorConfig, - ProxyRemoteConfig, - ProxyFallbackConfig, - ProxyLocalConfig, - OpenAICompatProxyRoutingConfig, - OpenAICompatProxyConfig, - CliproxyServerConfig, - GlobalEnvConfig, - ContinuityConfig, - ImageAnalysisConfig, -} from './providers'; - -// Main unified config interface, factory, and type guard -export { createEmptyUnifiedConfig, isUnifiedConfig } from './unified-config'; -export type { UnifiedConfig } from './unified-config'; diff --git a/src/config/schemas/logging.ts b/src/config/schemas/logging.ts deleted file mode 100644 index 05fe3061..00000000 --- a/src/config/schemas/logging.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Logging and preferences configuration types and defaults. - * - * Covers: - * - LoggingConfig: CCS-owned structured runtime logging - * - LoggingLevel: log severity levels - * - PreferencesConfig: user preferences (theme, telemetry, auto-update) - */ - -export type LoggingLevel = 'error' | 'warn' | 'info' | 'debug'; - -/** - * CCS-owned structured logging configuration. - * Separate from cliproxy.logging, which controls CLIProxy runtime files. - */ -export interface LoggingConfig { - /** Enable CCS-owned structured runtime logging */ - enabled: boolean; - /** Minimum level written to disk */ - level: LoggingLevel; - /** Rotate current log when it reaches this size in MB */ - rotate_mb: number; - /** Keep archived segments for this many days */ - retain_days: number; - /** Redact sensitive values before persistence */ - redact: boolean; - /** In-memory recent event buffer size for dashboard reads */ - live_buffer_size: number; -} - -/** - * Default logging configuration. - */ -export const DEFAULT_LOGGING_CONFIG: LoggingConfig = { - enabled: true, - level: 'info', - rotate_mb: 10, - retain_days: 7, - redact: true, - live_buffer_size: 250, -}; - -/** - * User preferences. - */ -export interface PreferencesConfig { - /** UI theme preference */ - theme?: 'light' | 'dark' | 'system'; - /** Enable anonymous telemetry */ - telemetry?: boolean; - /** Enable automatic update checks */ - auto_update?: boolean; -} diff --git a/src/config/schemas/providers.ts b/src/config/schemas/providers.ts deleted file mode 100644 index 78a6dc8f..00000000 --- a/src/config/schemas/providers.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Provider integration configuration types and defaults. - * - * Re-exports from focused sub-modules for backward compatibility. - * Actual definitions live in: - * - copilot-cursor.ts: CopilotConfig, CursorConfig + defaults - * - proxy-server.ts: CliproxyServerConfig, OpenAICompatProxyConfig, - * GlobalEnvConfig, ContinuityConfig, ImageAnalysisConfig + defaults - */ - -export type { CopilotAccountType, CopilotConfig, CursorConfig } from './copilot-cursor'; -export { DEFAULT_COPILOT_CONFIG, DEFAULT_CURSOR_CONFIG } from './copilot-cursor'; - -export type { - ProxyRemoteConfig, - ProxyFallbackConfig, - ProxyLocalConfig, - OpenAICompatProxyRoutingConfig, - OpenAICompatProxyConfig, - CliproxyServerConfig, - GlobalEnvConfig, - ContinuityConfig, - ImageAnalysisConfig, -} from './proxy-server'; -export { - DEFAULT_CLIPROXY_SERVER_CONFIG, - DEFAULT_OPENAI_COMPAT_PROXY_CONFIG, - DEFAULT_GLOBAL_ENV, - DEFAULT_IMAGE_ANALYSIS_CONFIG, -} from './proxy-server'; diff --git a/src/config/schemas/proxy-server.ts b/src/config/schemas/proxy-server.ts deleted file mode 100644 index 745ebab3..00000000 --- a/src/config/schemas/proxy-server.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * Proxy server, global env, continuity, and image analysis types and defaults. - * - * Covers: - * - CliproxyServerConfig: remote/local CLIProxy server mode - * - OpenAICompatProxyConfig: OpenAI-compatible local proxy - * - GlobalEnvConfig: global environment variable injection - * - ContinuityConfig: cross-profile continuity inheritance - * - ImageAnalysisConfig: vision analysis via CLIProxy - */ - -/** - * Remote proxy configuration. - * Connect to a remote CLIProxyAPI instance instead of spawning local binary. - */ -export interface ProxyRemoteConfig { - /** Enable remote proxy mode (default: false = local mode) */ - enabled: boolean; - /** Remote proxy hostname or IP (empty = not configured) */ - host: string; - /** - * Remote proxy port. - * Optional - defaults based on protocol: - * - HTTPS: 443 - * - HTTP: 8317 - * When empty/undefined, uses protocol default. - */ - port?: number; - /** Protocol for remote connection */ - protocol: 'http' | 'https'; - /** Auth token for remote proxy API endpoints (optional, sent as header) */ - auth_token: string; - /** - * Management key for remote proxy management API endpoints. - * CLIProxyAPI uses separate authentication for management endpoints - * (/v0/management/*) via 'secret-key' config. - * If not set, falls back to auth_token for backwards compatibility. - */ - management_key?: string; - /** Connection timeout in milliseconds (default: 2000) */ - timeout?: number; - /** Enable auto-sync profiles to remote on settings change (default: false) */ - auto_sync?: boolean; -} - -/** - * Fallback configuration when remote proxy is unreachable. - */ -export interface ProxyFallbackConfig { - /** Enable fallback to local proxy (default: true) */ - enabled: boolean; - /** Auto-start local proxy without prompting (default: false = prompt user) */ - auto_start: boolean; -} - -/** - * Local proxy configuration. - */ -export interface ProxyLocalConfig { - /** Local proxy port (default: 8317) */ - port: number; - /** Auto-start local binary (default: true) */ - auto_start: boolean; -} - -export interface OpenAICompatProxyRoutingConfig { - default?: string; - background?: string; - think?: string; - longContext?: string; - webSearch?: string; - longContextThreshold?: number; -} - -export interface OpenAICompatProxyConfig { - /** Default local port for OpenAI-compatible proxy instances */ - port?: number; - /** Optional profile-scoped local port overrides */ - profile_ports?: Record; - routing?: OpenAICompatProxyRoutingConfig; -} - -/** - * CLIProxy server configuration section. - * Controls whether CCS uses local or remote CLIProxyAPI instance. - */ -export interface CliproxyServerConfig { - /** Remote proxy settings */ - remote: ProxyRemoteConfig; - /** Fallback behavior when remote is unreachable */ - fallback: ProxyFallbackConfig; - /** Local proxy settings */ - local: ProxyLocalConfig; -} - -/** - * Global environment variables configuration. - * These env vars are injected into ALL non-Claude subscription profiles. - * Useful for disabling telemetry, bug commands, error reporting, etc. - */ -export interface GlobalEnvConfig { - /** Enable global env injection (default: true) */ - enabled: boolean; - /** Environment variables to inject */ - env: Record; -} - -/** - * Cross-profile continuity inheritance configuration. - * Maps execution profile names to source account profiles for CLAUDE_CONFIG_DIR reuse. - */ -export interface ContinuityConfig { - /** Profile name -> source account profile name */ - inherit_from_account?: Record; -} - -/** - * Default global env vars for third-party profiles. - * These disable Claude Code telemetry/reporting since we're using proxy. - */ -export const DEFAULT_GLOBAL_ENV: Record = { - DISABLE_BUG_COMMAND: '1', - DISABLE_ERROR_REPORTING: '1', - DISABLE_TELEMETRY: '1', -}; - -/** - * Default CLIProxy server configuration. - * Local mode by default - remote must be explicitly enabled. - * Port is optional for remote - defaults based on protocol. - */ -export const DEFAULT_CLIPROXY_SERVER_CONFIG: CliproxyServerConfig = { - remote: { - enabled: false, - host: '', - protocol: 'http', - auth_token: '', - }, - fallback: { - enabled: true, - auto_start: false, - }, - local: { - port: 8317, - auto_start: true, - }, -}; - -export const DEFAULT_OPENAI_COMPAT_PROXY_CONFIG: OpenAICompatProxyConfig = { - profile_ports: {}, - routing: { - longContextThreshold: 60_000, - }, -}; - -/** - * Image analysis configuration. - * Routes image/PDF files through CLIProxy for vision analysis. - */ -export interface ImageAnalysisConfig { - /** Enable image analysis via CLIProxy (default: true) */ - enabled: boolean; - /** Timeout in seconds (default: 60) */ - timeout: number; - /** Provider-to-model mapping for vision analysis */ - provider_models: Record; - /** Fallback backend used when a profile does not resolve to a provider-specific backend */ - fallback_backend?: string; - /** Explicit profile-name-to-backend overrides for settings/custom aliases */ - profile_backends?: Record; -} - -/** - * Default image analysis configuration. - * Enabled by default for CLIProxy providers with vision support. - */ -export const DEFAULT_IMAGE_ANALYSIS_CONFIG: ImageAnalysisConfig = { - enabled: true, - timeout: 60, - provider_models: { - agy: 'gemini-3-1-flash-preview', - gemini: 'gemini-3-flash-preview', - codex: 'gpt-5.1-codex-mini', - kiro: 'kiro-claude-haiku-4-5', - ghcp: 'claude-haiku-4.5', - claude: 'claude-haiku-4.5-20251001', - qwen: 'vision-model', - iflow: 'qwen3-vl-plus', - kimi: 'vision-model', - }, - fallback_backend: 'gemini', - profile_backends: {}, -}; diff --git a/src/config/schemas/quota.ts b/src/config/schemas/quota.ts deleted file mode 100644 index 0d380694..00000000 --- a/src/config/schemas/quota.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Quota management configuration types and defaults. - * - * Controls hybrid auto+manual account selection for multi-account setups. - * Version 7+ feature. - */ - -// ============================================================================ -// QUOTA MANAGEMENT CONFIGURATION (v7+) -// ============================================================================ - -/** - * Auto quota management configuration. - * Controls automatic failover behavior. - */ -export interface AutoQuotaConfig { - /** Enable pre-flight quota check before requests (default: true) */ - preflight_check: boolean; - /** Quota percentage below which account is "exhausted" (default: 5) */ - exhaustion_threshold: number; - /** Tier priority for failover, highest to lowest (default: ['paid']) */ - tier_priority: string[]; - /** Minutes to skip exhausted account before retry (default: 5) */ - cooldown_minutes: number; -} - -/** - * Runtime quota monitor configuration. - * Controls adaptive polling during active sessions. - */ -export interface RuntimeMonitorConfig { - /** Enable runtime monitoring during sessions (default: true) */ - enabled: boolean; - /** Poll interval in seconds when quota > warn_threshold (default: 300) */ - normal_interval_seconds: number; - /** Poll interval in seconds when quota <= warn_threshold (default: 60) */ - critical_interval_seconds: number; - /** Quota percentage that triggers fast polling + warning (default: 20) */ - warn_threshold: number; - /** Quota percentage that triggers cooldown + switch (default: 5) */ - exhaustion_threshold: number; - /** Minutes to cooldown exhausted account (default: 5) */ - cooldown_minutes: number; -} - -/** - * Manual quota management configuration. - * User-controlled overrides for account selection. - */ -export interface ManualQuotaConfig { - /** User-paused accounts (stored in accounts.json) */ - paused_accounts: string[]; - /** Force use of specific account (overrides auto-selection) */ - forced_default: string | null; - /** Lock to specific tier only */ - tier_lock: string | null; -} - -/** - * Quota management mode. - * - auto: Fully automatic failover based on quota - * - manual: User controls everything, no auto-switching - * - hybrid: Auto-failover with user overrides (default) - */ -export type QuotaManagementMode = 'auto' | 'manual' | 'hybrid'; - -/** - * Quota management configuration section. - * Controls hybrid auto+manual account selection for multi-account setups. - */ -export interface QuotaManagementConfig { - /** Management mode (default: hybrid) */ - mode: QuotaManagementMode; - /** Auto mode settings */ - auto: AutoQuotaConfig; - /** Manual mode settings */ - manual: ManualQuotaConfig; - /** Runtime monitor settings */ - runtime_monitor: RuntimeMonitorConfig; -} - -/** - * Default auto quota configuration. - */ -export const DEFAULT_AUTO_QUOTA_CONFIG: AutoQuotaConfig = { - preflight_check: true, - exhaustion_threshold: 5, - tier_priority: ['ultra', 'pro', 'free'], - cooldown_minutes: 5, -}; - -/** - * Default manual quota configuration. - */ -export const DEFAULT_MANUAL_QUOTA_CONFIG: ManualQuotaConfig = { - paused_accounts: [], - forced_default: null, - tier_lock: null, -}; - -/** - * Default runtime monitor configuration. - */ -export const DEFAULT_RUNTIME_MONITOR_CONFIG: RuntimeMonitorConfig = { - enabled: true, - normal_interval_seconds: 300, - critical_interval_seconds: 60, - warn_threshold: 20, - exhaustion_threshold: 5, - cooldown_minutes: 5, -}; - -/** - * Default quota management configuration. - */ -export const DEFAULT_QUOTA_MANAGEMENT_CONFIG: QuotaManagementConfig = { - mode: 'hybrid', - auto: { ...DEFAULT_AUTO_QUOTA_CONFIG }, - manual: { ...DEFAULT_MANUAL_QUOTA_CONFIG }, - runtime_monitor: { ...DEFAULT_RUNTIME_MONITOR_CONFIG }, -}; diff --git a/src/config/schemas/thinking.ts b/src/config/schemas/thinking.ts deleted file mode 100644 index 81951ad2..00000000 --- a/src/config/schemas/thinking.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Thinking/reasoning budget configuration types and defaults. - * - * Controls thinking budget injection for CLIProxy providers. - * Version 8+ feature. - */ - -// ============================================================================ -// THINKING CONFIGURATION (v8+) -// ============================================================================ - -/** - * Thinking mode for auto/manual/off control. - * - auto: Apply tier-based defaults (opus→high, sonnet→medium, haiku→low) - * - off: Disable thinking entirely - * - manual: Use explicit override value - */ -export type ThinkingMode = 'auto' | 'off' | 'manual'; - -/** - * Tier-to-thinking level defaults. - * Maps Claude tier names to thinking level names. - */ -export interface ThinkingTierDefaults { - /** Thinking level for opus tier (default: 'high') */ - opus: string; - /** Thinking level for sonnet tier (default: 'medium') */ - sonnet: string; - /** Thinking level for haiku tier (default: 'low') */ - haiku: string; -} - -/** - * Thinking configuration section. - * Controls thinking/reasoning budget injection for CLIProxy providers. - */ -export interface ThinkingConfig { - /** Thinking mode (default: 'auto') */ - mode: ThinkingMode; - /** Manual override value (level name or budget number) */ - override?: string | number; - /** Tier-to-level mapping */ - tier_defaults: ThinkingTierDefaults; - /** Per-provider overrides (e.g., { gemini: { opus: 'high' } }) */ - provider_overrides?: Record>; - /** Show warning when values are clamped (default: true) */ - show_warnings?: boolean; -} - -/** - * Default thinking tier defaults. - */ -export const DEFAULT_THINKING_TIER_DEFAULTS: ThinkingTierDefaults = { - opus: 'high', - sonnet: 'medium', - haiku: 'low', -}; - -/** - * Default thinking configuration. - */ -export const DEFAULT_THINKING_CONFIG: ThinkingConfig = { - mode: 'auto', - tier_defaults: { ...DEFAULT_THINKING_TIER_DEFAULTS }, - show_warnings: true, -}; diff --git a/src/config/schemas/unified-config.ts b/src/config/schemas/unified-config.ts deleted file mode 100644 index 78726c46..00000000 --- a/src/config/schemas/unified-config.ts +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Main unified configuration interface, factory, and type guard. - * - * The UnifiedConfig type is the root of the entire config.yaml schema. - * This file imports all section types from their respective schema modules. - */ - -import type { AccountConfig, ProfileConfig, DashboardAuthConfig } from './auth'; -import { DEFAULT_DASHBOARD_AUTH_CONFIG } from './auth'; -import type { CLIProxyConfig } from './cliproxy'; -import { CLIPROXY_SUPPORTED_PROVIDERS, DEFAULT_CLIPROXY_SAFETY_CONFIG } from './cliproxy'; -import type { LoggingConfig, PreferencesConfig } from './logging'; -import { DEFAULT_LOGGING_CONFIG } from './logging'; -import type { WebSearchConfig } from './websearch'; -import type { - GlobalEnvConfig, - ContinuityConfig, - CopilotConfig, - CursorConfig, - CliproxyServerConfig, - OpenAICompatProxyConfig, - ImageAnalysisConfig, -} from './providers'; -import { - DEFAULT_COPILOT_CONFIG, - DEFAULT_CURSOR_CONFIG, - DEFAULT_CLIPROXY_SERVER_CONFIG, - DEFAULT_OPENAI_COMPAT_PROXY_CONFIG, - DEFAULT_IMAGE_ANALYSIS_CONFIG, - DEFAULT_GLOBAL_ENV, -} from './providers'; -import { UNIFIED_CONFIG_VERSION } from './version'; -import type { QuotaManagementConfig } from './quota'; -import { DEFAULT_QUOTA_MANAGEMENT_CONFIG } from './quota'; -import type { ThinkingConfig } from './thinking'; -import { DEFAULT_THINKING_CONFIG } from './thinking'; -import type { OfficialChannelsConfig } from './channels'; -import { DEFAULT_OFFICIAL_CHANNELS_CONFIG } from './channels'; -import type { BrowserConfig } from './browser'; -import { DEFAULT_BROWSER_CONFIG } from './browser'; - -/** - * Main unified configuration structure. - * Stored in ~/.ccs/config.yaml - */ -export interface UnifiedConfig { - /** Config version */ - version: number; - /** Flag indicating setup wizard has been completed */ - setup_completed?: boolean; - /** Default profile name to use when none specified */ - default?: string; - /** Account-based profiles (isolated Claude instances) */ - accounts: Record; - /** API-based profiles (env var injection) */ - profiles: Record; - /** CLIProxy configuration */ - cliproxy: CLIProxyConfig; - /** OpenAI-compatible local proxy configuration */ - proxy?: OpenAICompatProxyConfig; - /** CCS-owned structured logging configuration */ - logging?: LoggingConfig; - /** User preferences */ - preferences: PreferencesConfig; - /** WebSearch configuration */ - websearch?: WebSearchConfig; - /** Global environment variables for all non-Claude subscription profiles */ - global_env?: GlobalEnvConfig; - /** Cross-profile continuity inheritance mapping */ - continuity?: ContinuityConfig; - /** Copilot API configuration (GitHub Copilot proxy) */ - copilot?: CopilotConfig; - /** Cursor IDE configuration (Cursor proxy daemon) */ - cursor?: CursorConfig; - /** CLIProxy server configuration for remote/local mode */ - cliproxy_server?: CliproxyServerConfig; - /** Quota management configuration (v7+) */ - quota_management?: QuotaManagementConfig; - /** Thinking/reasoning budget configuration (v8+) */ - thinking?: ThinkingConfig; - /** Official Channels runtime auto-enable preferences (v11+) */ - channels?: OfficialChannelsConfig; - /** Dashboard authentication configuration (optional) */ - dashboard_auth?: DashboardAuthConfig; - /** Browser automation configuration */ - browser?: BrowserConfig; - /** Image analysis configuration (vision via CLIProxy) */ - image_analysis?: ImageAnalysisConfig; -} - -/** - * Create an empty unified config with defaults. - */ -export function createEmptyUnifiedConfig(): UnifiedConfig { - return { - version: UNIFIED_CONFIG_VERSION, - default: undefined, - accounts: {}, - profiles: {}, - cliproxy: { - backend: 'original', - oauth_accounts: {}, - providers: [...CLIPROXY_SUPPORTED_PROVIDERS], - variants: {}, - logging: { - enabled: false, - request_log: false, - }, - safety: { ...DEFAULT_CLIPROXY_SAFETY_CONFIG }, - auto_sync: true, - routing: { - strategy: 'round-robin', - session_affinity: false, - session_affinity_ttl: '1h', - }, - }, - proxy: { - port: DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.port, - profile_ports: { ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.profile_ports }, - routing: { - ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.routing, - }, - }, - logging: { ...DEFAULT_LOGGING_CONFIG }, - preferences: { - theme: 'system', - telemetry: false, - auto_update: true, - }, - websearch: { - enabled: true, - providers: { - exa: { - enabled: false, - max_results: 5, - }, - tavily: { - enabled: false, - max_results: 5, - }, - brave: { - enabled: false, - max_results: 5, - }, - searxng: { - enabled: false, - url: '', - max_results: 5, - }, - duckduckgo: { - enabled: true, - max_results: 5, - }, - gemini: { - enabled: false, - model: 'gemini-2.5-flash', - timeout: 55, - }, - opencode: { - enabled: false, - model: 'opencode/grok-code', - timeout: 90, - }, - grok: { - enabled: false, - timeout: 55, - }, - }, - }, - global_env: { - enabled: true, - env: { ...DEFAULT_GLOBAL_ENV }, - }, - copilot: { ...DEFAULT_COPILOT_CONFIG }, - cursor: { ...DEFAULT_CURSOR_CONFIG }, - cliproxy_server: { ...DEFAULT_CLIPROXY_SERVER_CONFIG }, - quota_management: { ...DEFAULT_QUOTA_MANAGEMENT_CONFIG }, - thinking: { ...DEFAULT_THINKING_CONFIG }, - channels: { ...DEFAULT_OFFICIAL_CHANNELS_CONFIG }, - dashboard_auth: { ...DEFAULT_DASHBOARD_AUTH_CONFIG }, - browser: { - claude: { ...DEFAULT_BROWSER_CONFIG.claude }, - codex: { ...DEFAULT_BROWSER_CONFIG.codex }, - }, - image_analysis: { ...DEFAULT_IMAGE_ANALYSIS_CONFIG }, - }; -} - -/** - * Type guard for UnifiedConfig. - * Relaxed validation: accepts configs with version >= 1 and any subset of sections. - * Missing sections will be filled with defaults during merge. - */ -export function isUnifiedConfig(obj: unknown): obj is UnifiedConfig { - if (typeof obj !== 'object' || obj === null) return false; - const config = obj as Record; - // Only require version to be a number >= 1 (allow future versions) - // Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig - return typeof config.version === 'number' && config.version >= 1; -} diff --git a/src/config/schemas/version.ts b/src/config/schemas/version.ts deleted file mode 100644 index 98979314..00000000 --- a/src/config/schemas/version.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Unified config version constant. - * - * Central source of truth for the current config schema version. - * Incremented whenever new sections are added to config.yaml. - */ - -/** - * Unified config version. - * Version 2 = YAML unified format - * Version 3 = WebSearch config with model configuration for Gemini/OpenCode - * Version 4 = Copilot API integration (GitHub Copilot proxy) - * Version 5 = Remote proxy configuration (connect to remote CLIProxyAPI) - * Version 6 = Customizable auth tokens (API key and management secret) - * Version 7 = Quota management for hybrid auto+manual account control - * Version 8 = Thinking/reasoning budget configuration - * Version 9 = Real WebSearch backends (DuckDuckGo/Brave) with legacy CLI fallback - * Version 10 = Exa + Tavily WebSearch backends - * Version 11 = Discord Channels runtime auto-enable preferences - * Version 12 = Official Channels multi-provider support (Telegram, Discord, iMessage) - * Version 13 = Browser automation defaults to safe manual/off exposure - */ -export const UNIFIED_CONFIG_VERSION = 13; diff --git a/src/config/schemas/websearch.ts b/src/config/schemas/websearch.ts deleted file mode 100644 index d2714c41..00000000 --- a/src/config/schemas/websearch.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * WebSearch backend configuration types. - * - * Covers all supported search backends: - * - API-backed: Exa, Tavily, Brave - * - Self-hosted: SearXNG - * - Zero-setup: DuckDuckGo - * - Legacy CLI fallbacks: Gemini, Grok, OpenCode - */ - -/** - * DuckDuckGo WebSearch configuration. - */ -export interface DuckDuckGoWebSearchConfig { - /** Enable DuckDuckGo HTML search fallback (default: true) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Brave WebSearch configuration. - */ -export interface BraveWebSearchConfig { - /** Enable Brave Search when BRAVE_API_KEY is available (default: false) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Exa WebSearch configuration. - */ -export interface ExaWebSearchConfig { - /** Enable Exa Search when EXA_API_KEY is available (default: false) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Tavily WebSearch configuration. - */ -export interface TavilyWebSearchConfig { - /** Enable Tavily Search when TAVILY_API_KEY is available (default: false) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * SearXNG WebSearch configuration. - */ -export interface SearxngWebSearchConfig { - /** Enable SearXNG JSON search backend (default: false) */ - enabled?: boolean; - /** Base SearXNG URL, e.g. https://search.example.com (default: '') */ - url?: string; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Gemini CLI WebSearch configuration. - */ -export interface GeminiWebSearchConfig { - /** Enable Gemini CLI legacy fallback (default: false) */ - enabled?: boolean; - /** Model to use (default: gemini-2.5-flash) */ - model?: string; - /** Timeout in seconds (default: 55) */ - timeout?: number; -} - -/** - * Grok CLI WebSearch configuration. - */ -export interface GrokWebSearchConfig { - /** Enable Grok CLI legacy fallback (default: false - requires GROK_API_KEY) */ - enabled?: boolean; - /** Timeout in seconds (default: 55) */ - timeout?: number; -} - -/** - * OpenCode CLI WebSearch configuration. - */ -export interface OpenCodeWebSearchConfig { - /** Enable OpenCode CLI legacy fallback (default: false) */ - enabled?: boolean; - /** Model to use (default: opencode/grok-code) */ - model?: string; - /** Timeout in seconds (default: 60) */ - timeout?: number; -} - -/** - * WebSearch providers configuration. - * Uses deterministic search backends first, with optional legacy CLI fallback. - */ -export interface WebSearchProvidersConfig { - /** Exa Search API - API-backed search with strong relevance and content extraction */ - exa?: ExaWebSearchConfig; - /** Tavily Search API - API-backed search optimized for agent/tool usage */ - tavily?: TavilyWebSearchConfig; - /** Brave Search API - higher quality results when BRAVE_API_KEY is set */ - brave?: BraveWebSearchConfig; - /** SearXNG JSON search - self-hosted or public instance backend */ - searxng?: SearxngWebSearchConfig; - /** DuckDuckGo HTML search - zero setup default backend */ - duckduckgo?: DuckDuckGoWebSearchConfig; - /** Gemini CLI - optional legacy LLM fallback */ - gemini?: GeminiWebSearchConfig; - /** Grok CLI - optional legacy LLM fallback */ - grok?: GrokWebSearchConfig; - /** OpenCode - optional legacy LLM fallback */ - opencode?: OpenCodeWebSearchConfig; -} - -/** - * WebSearch configuration. - * Uses deterministic local backends for third-party profiles. - * Legacy AI CLI fallbacks remain available for compatibility only. - */ -export interface WebSearchConfig { - /** Master switch - enable/disable WebSearch (default: true) */ - enabled?: boolean; - /** Individual provider configurations */ - providers?: WebSearchProvidersConfig; - // Legacy fields (deprecated, kept for backwards compatibility) - /** @deprecated Use providers.gemini instead */ - gemini?: { - enabled?: boolean; - timeout?: number; - }; - /** @deprecated Unused */ - mode?: 'sequential' | 'parallel'; - /** @deprecated Unused */ - provider?: 'auto' | 'web-search-prime' | 'brave' | 'tavily'; - /** @deprecated Unused */ - fallback?: boolean; - /** @deprecated Unused */ - webSearchPrimeUrl?: string; - /** @deprecated Unused */ - selectedProviders?: string[]; - /** @deprecated Unused */ - customMcp?: unknown[]; -} diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index ec5eef2f..ab4eca6c 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -7,8 +7,1122 @@ * - *.settings.json (env vars) * * Into a single config.yaml structure. - * - * Types have been reorganized into src/config/schemas/ for maintainability. - * This file re-exports everything for backward compatibility. */ -export * from './schemas/index'; + +import type { TargetType } from '../targets/target-adapter'; +import type { CLIProxyProvider, CliproxyRoutingStrategy } from '../cliproxy/types'; +import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities'; + +/** + * Unified config version. + * Version 2 = YAML unified format + * Version 3 = WebSearch config with model configuration for Gemini/OpenCode + * Version 4 = Copilot API integration (GitHub Copilot proxy) + * Version 5 = Remote proxy configuration (connect to remote CLIProxyAPI) + * Version 6 = Customizable auth tokens (API key and management secret) + * Version 7 = Quota management for hybrid auto+manual account control + * Version 8 = Thinking/reasoning budget configuration + * Version 9 = Real WebSearch backends (DuckDuckGo/Brave) with legacy CLI fallback + * Version 10 = Exa + Tavily WebSearch backends + * Version 11 = Discord Channels runtime auto-enable preferences + * Version 12 = Official Channels multi-provider support (Telegram, Discord, iMessage) + * Version 13 = Browser automation defaults to safe manual/off exposure + */ +export const UNIFIED_CONFIG_VERSION = 13; + +/** + * Supported CLIProxy providers. + * Derived from CLIPROXY_PROVIDER_IDS — single source of truth in provider-capabilities.ts. + */ +export const CLIPROXY_SUPPORTED_PROVIDERS = CLIPROXY_PROVIDER_IDS; + +/** + * Account configuration (formerly in profiles.json). + * Represents an isolated Claude instance via CLAUDE_CONFIG_DIR. + */ +export interface AccountConfig { + /** ISO timestamp when account was created */ + created: string; + /** ISO timestamp of last usage, null if never used */ + last_used: string | null; + /** Context mode for project workspace data */ + context_mode?: 'isolated' | 'shared'; + /** Context-sharing group when context_mode='shared' */ + context_group?: string; + /** Shared continuity depth when context_mode='shared' */ + continuity_mode?: 'standard' | 'deeper'; + /** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */ + bare?: boolean; +} + +/** + * API-based profile configuration. + * Injects environment variables for alternative providers (GLM, Kimi, etc.). + * + * Settings are stored in separate *.settings.json files (matching Claude's pattern) + * to allow users to edit them directly without touching config.yaml. + */ +export interface ProfileConfig { + /** Profile type - currently only 'api' */ + type: 'api'; + /** Path to settings file (e.g., "~/.ccs/glm.settings.json") */ + settings: string; + /** Target CLI to use for this profile (default: 'claude') */ + target?: TargetType; +} + +/** + * CLIProxy OAuth account nickname mapping. + * Maps user-friendly nicknames to email addresses. + */ +export type OAuthAccounts = Record; + +/** + * CLIProxy variant configuration. + * User-defined variants of built-in OAuth providers. + * + * Settings are stored in separate *.settings.json files (matching Claude's pattern) + * to allow users to edit them directly without touching config.yaml. + */ +export interface CLIProxyVariantConfig { + /** Base provider to use */ + provider: CLIProxyProvider; + /** Account nickname (references oauth_accounts) */ + account?: string; + /** Path to settings file (e.g., "~/.ccs/gemini-custom.settings.json") */ + settings?: string; + /** Unique port for variant isolation (8318-8417) */ + port?: number; + /** Per-variant auth override (optional) */ + auth?: CLIProxyAuthConfig; + /** Target CLI to use for this variant (default: 'claude') */ + target?: TargetType; +} + +/** + * Per-tier provider+model mapping for composite variants. + */ +export interface CompositeTierConfig { + /** Provider for this tier */ + provider: CLIProxyProvider; + /** Model ID to use for this tier */ + model: string; + /** Account nickname (optional, references oauth_accounts) */ + account?: string; + /** Fallback provider+model if primary fails */ + fallback?: { + provider: CLIProxyProvider; + model: string; + account?: string; + }; + /** Per-tier thinking budget override (e.g. 'xhigh', 'medium', 'off') */ + thinking?: string; +} + +/** + * Composite variant configuration. + * Mixes different providers per Claude tier (opus, sonnet, haiku) in a single profile. + * Uses CLIProxyAPI root endpoints (/v1/messages) for model-based routing + * instead of provider-specific endpoints (/api/provider/{provider}). + */ +export interface CompositeVariantConfig { + /** Discriminator for composite type */ + type: 'composite'; + /** Which tier ANTHROPIC_MODEL equals (default must be one of the three) */ + default_tier: 'opus' | 'sonnet' | 'haiku'; + /** Per-tier provider+model mapping */ + tiers: { + opus: CompositeTierConfig; + sonnet: CompositeTierConfig; + haiku: CompositeTierConfig; + }; + /** Path to settings file */ + settings?: string; + /** Shared port for the composite profile */ + port?: number; + /** Per-variant auth override (optional) */ + auth?: CLIProxyAuthConfig; + /** Target CLI to use for this composite variant (default: 'claude') */ + target?: TargetType; +} + +/** + * CLIProxy authentication configuration. + * Allows customization of API key and management secret for CLIProxyAPI. + */ +export interface CLIProxyAuthConfig { + /** API key for CCS-managed requests (default: 'ccs-internal-managed') */ + api_key?: string; + /** Management secret for Control Panel login (default: 'ccs') */ + management_secret?: string; +} + +/** + * CLIProxy logging configuration. + * Controls whether CLIProxyAPI writes logs to disk. + * Logs can grow to several GB if left enabled. + */ +export interface CLIProxyLoggingConfig { + /** Enable logging to file (default: false to prevent disk bloat) */ + enabled?: boolean; + /** Enable request logging for debugging (default: false) */ + request_log?: boolean; +} + +/** + * CLIProxy safety configuration. + * Controls high-risk flow safeguards for supported providers. + */ +export interface CLIProxySafetyConfig { + /** Allow skipping AGY responsibility checks and Gemini dashboard typed acknowledgement */ + antigravity_ack_bypass?: boolean; +} + +/** + * Default CLIProxy safety configuration. + */ +export const DEFAULT_CLIPROXY_SAFETY_CONFIG: CLIProxySafetyConfig = { + antigravity_ack_bypass: false, +}; + +/** + * Token refresh configuration. + * Manages background token refresh worker settings. + */ +export interface TokenRefreshSettings { + /** Enable background token refresh (default: false) */ + enabled?: boolean; + /** Refresh check interval in minutes (default: 30) */ + interval_minutes?: number; + /** Preemptive refresh time in minutes (default: 45) */ + preemptive_minutes?: number; + /** Maximum retry attempts per token (default: 3) */ + max_retries?: number; + /** Enable verbose logging (default: false) */ + verbose?: boolean; +} + +export interface CLIProxyRoutingConfig { + /** Credential selection strategy when multiple accounts match */ + strategy?: CliproxyRoutingStrategy; + /** Keep one conversation pinned to the same account when possible */ + session_affinity?: boolean; + /** Go-style duration for session-affinity binding retention */ + session_affinity_ttl?: string; +} + +/** + * CLIProxy configuration section. + */ +export interface CLIProxyConfig { + /** Backend selection: 'original' or 'plus' (default: 'original') */ + backend?: 'original' | 'plus'; + /** Nickname to email mapping for OAuth accounts */ + oauth_accounts: OAuthAccounts; + /** Built-in providers (read-only, for reference) */ + providers: readonly string[]; + /** User-defined provider variants (single-provider or composite) */ + variants: Record; + /** Logging configuration (disabled by default) */ + logging?: CLIProxyLoggingConfig; + /** Safety controls for high-risk provider flows */ + safety?: CLIProxySafetyConfig; + /** Kiro: disable incognito browser mode (use normal browser to save credentials) */ + kiro_no_incognito?: boolean; + /** Global auth configuration for CLIProxyAPI */ + auth?: CLIProxyAuthConfig; + /** Background token refresh worker settings */ + token_refresh?: TokenRefreshSettings; + /** Auto-sync API profiles to local CLIProxy config on settings change (default: true) */ + auto_sync?: boolean; + /** Routing strategy for multi-account CLIProxy selection */ + routing?: CLIProxyRoutingConfig; +} + +export type LoggingLevel = 'error' | 'warn' | 'info' | 'debug'; + +/** + * CCS-owned structured logging configuration. + * Separate from cliproxy.logging, which controls CLIProxy runtime files. + */ +export interface LoggingConfig { + /** Enable CCS-owned structured runtime logging */ + enabled: boolean; + /** Minimum level written to disk */ + level: LoggingLevel; + /** Rotate current log when it reaches this size in MB */ + rotate_mb: number; + /** Keep archived segments for this many days */ + retain_days: number; + /** Redact sensitive values before persistence */ + redact: boolean; + /** In-memory recent event buffer size for dashboard reads */ + live_buffer_size: number; +} + +export const DEFAULT_LOGGING_CONFIG: LoggingConfig = { + enabled: true, + level: 'info', + rotate_mb: 10, + retain_days: 7, + redact: true, + live_buffer_size: 250, +}; + +/** + * User preferences. + */ +export interface PreferencesConfig { + /** UI theme preference */ + theme?: 'light' | 'dark' | 'system'; + /** Enable anonymous telemetry */ + telemetry?: boolean; + /** Enable automatic update checks */ + auto_update?: boolean; +} + +/** + * DuckDuckGo WebSearch configuration. + */ +export interface DuckDuckGoWebSearchConfig { + /** Enable DuckDuckGo HTML search fallback (default: true) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Brave WebSearch configuration. + */ +export interface BraveWebSearchConfig { + /** Enable Brave Search when BRAVE_API_KEY is available (default: false) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Exa WebSearch configuration. + */ +export interface ExaWebSearchConfig { + /** Enable Exa Search when EXA_API_KEY is available (default: false) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Tavily WebSearch configuration. + */ +export interface TavilyWebSearchConfig { + /** Enable Tavily Search when TAVILY_API_KEY is available (default: false) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * SearXNG WebSearch configuration. + */ +export interface SearxngWebSearchConfig { + /** Enable SearXNG JSON search backend (default: false) */ + enabled?: boolean; + /** Base SearXNG URL, e.g. https://search.example.com (default: '') */ + url?: string; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Gemini CLI WebSearch configuration. + */ +export interface GeminiWebSearchConfig { + /** Enable Gemini CLI legacy fallback (default: false) */ + enabled?: boolean; + /** Model to use (default: gemini-2.5-flash) */ + model?: string; + /** Timeout in seconds (default: 55) */ + timeout?: number; +} + +/** + * Grok CLI WebSearch configuration. + */ +export interface GrokWebSearchConfig { + /** Enable Grok CLI legacy fallback (default: false - requires GROK_API_KEY) */ + enabled?: boolean; + /** Timeout in seconds (default: 55) */ + timeout?: number; +} + +/** + * OpenCode CLI WebSearch configuration. + */ +export interface OpenCodeWebSearchConfig { + /** Enable OpenCode CLI legacy fallback (default: false) */ + enabled?: boolean; + /** Model to use (default: opencode/grok-code) */ + model?: string; + /** Timeout in seconds (default: 60) */ + timeout?: number; +} + +/** + * WebSearch providers configuration. + * Uses deterministic search backends first, with optional legacy CLI fallback. + */ +export interface WebSearchProvidersConfig { + /** Exa Search API - API-backed search with strong relevance and content extraction */ + exa?: ExaWebSearchConfig; + /** Tavily Search API - API-backed search optimized for agent/tool usage */ + tavily?: TavilyWebSearchConfig; + /** Brave Search API - higher quality results when BRAVE_API_KEY is set */ + brave?: BraveWebSearchConfig; + /** SearXNG JSON search - self-hosted or public instance backend */ + searxng?: SearxngWebSearchConfig; + /** DuckDuckGo HTML search - zero setup default backend */ + duckduckgo?: DuckDuckGoWebSearchConfig; + /** Gemini CLI - optional legacy LLM fallback */ + gemini?: GeminiWebSearchConfig; + /** Grok CLI - optional legacy LLM fallback */ + grok?: GrokWebSearchConfig; + /** OpenCode - optional legacy LLM fallback */ + opencode?: OpenCodeWebSearchConfig; +} + +/** + * Copilot API account type. + */ +export type CopilotAccountType = 'individual' | 'business' | 'enterprise'; + +/** + * Copilot API configuration. + * Enables GitHub Copilot subscription usage via copilot-api proxy. + * Strictly opt-in - disabled by default. + * + * !! DISCLAIMER - USE AT YOUR OWN RISK !! + * This uses an UNOFFICIAL reverse-engineered API. + * Excessive usage may trigger GitHub account restrictions. + * CCS provides NO WARRANTY and accepts NO RESPONSIBILITY for any consequences. + */ +export interface CopilotConfig { + /** Enable Copilot integration (default: false) - must be explicitly enabled */ + enabled: boolean; + /** Auto-start copilot-api daemon when using profile (default: false) */ + auto_start: boolean; + /** Port for copilot-api proxy (default: 4141) */ + port: number; + /** GitHub Copilot account type (default: individual) */ + account_type: CopilotAccountType; + /** Rate limit in seconds between requests (null = no limit) */ + rate_limit: number | null; + /** Wait instead of error when rate limit is hit (default: true) */ + wait_on_limit: boolean; + /** Default model ID (e.g., claude-sonnet-4.5) */ + model: string; + /** Model mapping for Claude tiers - maps opus/sonnet/haiku to specific models */ + opus_model?: string; + sonnet_model?: string; + haiku_model?: string; +} + +/** + * Cursor IDE integration configuration. + * Enables Cursor IDE usage via cursor proxy daemon. + */ +export interface CursorConfig { + /** Enable Cursor integration (default: false) */ + enabled: boolean; + /** Port for cursor proxy daemon (default: 20129) */ + port: number; + /** Auto-start daemon when CCS starts (default: false) */ + auto_start: boolean; + /** Enable ghost mode to disable telemetry (default: true) */ + ghost_mode: boolean; + /** Default model ID used by Cursor integration */ + model: string; + /** Optional tier mapping for Claude-compatible model routing */ + opus_model?: string; + /** Optional tier mapping for Claude-compatible model routing */ + sonnet_model?: string; + /** Optional tier mapping for Claude-compatible model routing */ + haiku_model?: string; +} + +/** + * Remote proxy configuration. + * Connect to a remote CLIProxyAPI instance instead of spawning local binary. + */ +export interface ProxyRemoteConfig { + /** Enable remote proxy mode (default: false = local mode) */ + enabled: boolean; + /** Remote proxy hostname or IP (empty = not configured) */ + host: string; + /** + * Remote proxy port. + * Optional - defaults based on protocol: + * - HTTPS: 443 + * - HTTP: 8317 + * When empty/undefined, uses protocol default. + */ + port?: number; + /** Protocol for remote connection */ + protocol: 'http' | 'https'; + /** Auth token for remote proxy API endpoints (optional, sent as header) */ + auth_token: string; + /** + * Management key for remote proxy management API endpoints. + * CLIProxyAPI uses separate authentication for management endpoints + * (/v0/management/*) via 'secret-key' config. + * If not set, falls back to auth_token for backwards compatibility. + */ + management_key?: string; + /** Connection timeout in milliseconds (default: 2000) */ + timeout?: number; + /** Enable auto-sync profiles to remote on settings change (default: false) */ + auto_sync?: boolean; +} + +/** + * Fallback configuration when remote proxy is unreachable. + */ +export interface ProxyFallbackConfig { + /** Enable fallback to local proxy (default: true) */ + enabled: boolean; + /** Auto-start local proxy without prompting (default: false = prompt user) */ + auto_start: boolean; +} + +/** + * Local proxy configuration. + */ +export interface ProxyLocalConfig { + /** Local proxy port (default: 8317) */ + port: number; + /** Auto-start local binary (default: true) */ + auto_start: boolean; +} + +export interface OpenAICompatProxyRoutingConfig { + default?: string; + background?: string; + think?: string; + longContext?: string; + webSearch?: string; + longContextThreshold?: number; +} + +export interface OpenAICompatProxyConfig { + /** Default local port for OpenAI-compatible proxy instances */ + port?: number; + /** Optional profile-scoped local port overrides */ + profile_ports?: Record; + routing?: OpenAICompatProxyRoutingConfig; +} + +/** + * CLIProxy server configuration section. + * Controls whether CCS uses local or remote CLIProxyAPI instance. + */ +export interface CliproxyServerConfig { + /** Remote proxy settings */ + remote: ProxyRemoteConfig; + /** Fallback behavior when remote is unreachable */ + fallback: ProxyFallbackConfig; + /** Local proxy settings */ + local: ProxyLocalConfig; +} + +/** + * Global environment variables configuration. + * These env vars are injected into ALL non-Claude subscription profiles. + * Useful for disabling telemetry, bug commands, error reporting, etc. + */ +export interface GlobalEnvConfig { + /** Enable global env injection (default: true) */ + enabled: boolean; + /** Environment variables to inject */ + env: Record; +} + +/** + * Cross-profile continuity inheritance configuration. + * Maps execution profile names to source account profiles for CLAUDE_CONFIG_DIR reuse. + */ +export interface ContinuityConfig { + /** Profile name -> source account profile name */ + inherit_from_account?: Record; +} + +/** + * Default global env vars for third-party profiles. + * These disable Claude Code telemetry/reporting since we're using proxy. + */ +export const DEFAULT_GLOBAL_ENV: Record = { + DISABLE_BUG_COMMAND: '1', + DISABLE_ERROR_REPORTING: '1', + DISABLE_TELEMETRY: '1', +}; + +/** + * WebSearch configuration. + * Uses deterministic local backends for third-party profiles. + * Legacy AI CLI fallbacks remain available for compatibility only. + */ +export interface WebSearchConfig { + /** Master switch - enable/disable WebSearch (default: true) */ + enabled?: boolean; + /** Individual provider configurations */ + providers?: WebSearchProvidersConfig; + // Legacy fields (deprecated, kept for backwards compatibility) + /** @deprecated Use providers.gemini instead */ + gemini?: { + enabled?: boolean; + timeout?: number; + }; + /** @deprecated Unused */ + mode?: 'sequential' | 'parallel'; + /** @deprecated Unused */ + provider?: 'auto' | 'web-search-prime' | 'brave' | 'tavily'; + /** @deprecated Unused */ + fallback?: boolean; + /** @deprecated Unused */ + webSearchPrimeUrl?: string; + /** @deprecated Unused */ + selectedProviders?: string[]; + /** @deprecated Unused */ + customMcp?: unknown[]; +} + +// ============================================================================ +// QUOTA MANAGEMENT CONFIGURATION (v7+) +// ============================================================================ + +/** + * Auto quota management configuration. + * Controls automatic failover behavior. + */ +export interface AutoQuotaConfig { + /** Enable pre-flight quota check before requests (default: true) */ + preflight_check: boolean; + /** Quota percentage below which account is "exhausted" (default: 5) */ + exhaustion_threshold: number; + /** Tier priority for failover, highest to lowest (default: ['paid']) */ + tier_priority: string[]; + /** Minutes to skip exhausted account before retry (default: 5) */ + cooldown_minutes: number; +} + +/** + * Runtime quota monitor configuration. + * Controls adaptive polling during active sessions. + */ +export interface RuntimeMonitorConfig { + /** Enable runtime monitoring during sessions (default: true) */ + enabled: boolean; + /** Poll interval in seconds when quota > warn_threshold (default: 300) */ + normal_interval_seconds: number; + /** Poll interval in seconds when quota <= warn_threshold (default: 60) */ + critical_interval_seconds: number; + /** Quota percentage that triggers fast polling + warning (default: 20) */ + warn_threshold: number; + /** Quota percentage that triggers cooldown + switch (default: 5) */ + exhaustion_threshold: number; + /** Minutes to cooldown exhausted account (default: 5) */ + cooldown_minutes: number; +} + +/** + * Manual quota management configuration. + * User-controlled overrides for account selection. + */ +export interface ManualQuotaConfig { + /** User-paused accounts (stored in accounts.json) */ + paused_accounts: string[]; + /** Force use of specific account (overrides auto-selection) */ + forced_default: string | null; + /** Lock to specific tier only */ + tier_lock: string | null; +} + +/** + * Quota management mode. + * - auto: Fully automatic failover based on quota + * - manual: User controls everything, no auto-switching + * - hybrid: Auto-failover with user overrides (default) + */ +export type QuotaManagementMode = 'auto' | 'manual' | 'hybrid'; + +/** + * Quota management configuration section. + * Controls hybrid auto+manual account selection for multi-account setups. + */ +export interface QuotaManagementConfig { + /** Management mode (default: hybrid) */ + mode: QuotaManagementMode; + /** Auto mode settings */ + auto: AutoQuotaConfig; + /** Manual mode settings */ + manual: ManualQuotaConfig; + /** Runtime monitor settings */ + runtime_monitor: RuntimeMonitorConfig; +} + +/** + * Default auto quota configuration. + */ +export const DEFAULT_AUTO_QUOTA_CONFIG: AutoQuotaConfig = { + preflight_check: true, + exhaustion_threshold: 5, + tier_priority: ['ultra', 'pro', 'free'], + cooldown_minutes: 5, +}; + +/** + * Default manual quota configuration. + */ +export const DEFAULT_MANUAL_QUOTA_CONFIG: ManualQuotaConfig = { + paused_accounts: [], + forced_default: null, + tier_lock: null, +}; + +/** + * Default runtime monitor configuration. + */ +export const DEFAULT_RUNTIME_MONITOR_CONFIG: RuntimeMonitorConfig = { + enabled: true, + normal_interval_seconds: 300, + critical_interval_seconds: 60, + warn_threshold: 20, + exhaustion_threshold: 5, + cooldown_minutes: 5, +}; + +/** + * Default quota management configuration. + */ +export const DEFAULT_QUOTA_MANAGEMENT_CONFIG: QuotaManagementConfig = { + mode: 'hybrid', + auto: { ...DEFAULT_AUTO_QUOTA_CONFIG }, + manual: { ...DEFAULT_MANUAL_QUOTA_CONFIG }, + runtime_monitor: { ...DEFAULT_RUNTIME_MONITOR_CONFIG }, +}; + +// ============================================================================ +// THINKING CONFIGURATION (v8+) +// ============================================================================ + +/** + * Thinking mode for auto/manual/off control. + * - auto: Apply tier-based defaults (opus→high, sonnet→medium, haiku→low) + * - off: Disable thinking entirely + * - manual: Use explicit override value + */ +export type ThinkingMode = 'auto' | 'off' | 'manual'; + +/** + * Tier-to-thinking level defaults. + * Maps Claude tier names to thinking level names. + */ +export interface ThinkingTierDefaults { + /** Thinking level for opus tier (default: 'high') */ + opus: string; + /** Thinking level for sonnet tier (default: 'medium') */ + sonnet: string; + /** Thinking level for haiku tier (default: 'low') */ + haiku: string; +} + +/** + * Thinking configuration section. + * Controls thinking/reasoning budget injection for CLIProxy providers. + */ +export interface ThinkingConfig { + /** Thinking mode (default: 'auto') */ + mode: ThinkingMode; + /** Manual override value (level name or budget number) */ + override?: string | number; + /** Tier-to-level mapping */ + tier_defaults: ThinkingTierDefaults; + /** Per-provider overrides (e.g., { gemini: { opus: 'high' } }) */ + provider_overrides?: Record>; + /** Show warning when values are clamped (default: true) */ + show_warnings?: boolean; +} + +/** + * Default thinking tier defaults. + */ +export const DEFAULT_THINKING_TIER_DEFAULTS: ThinkingTierDefaults = { + opus: 'high', + sonnet: 'medium', + haiku: 'low', +}; + +/** + * Default thinking configuration. + */ +export const DEFAULT_THINKING_CONFIG: ThinkingConfig = { + mode: 'auto', + tier_defaults: { ...DEFAULT_THINKING_TIER_DEFAULTS }, + show_warnings: true, +}; + +/** + * Supported Anthropic official channel IDs. + */ +export type OfficialChannelId = 'telegram' | 'discord' | 'imessage'; + +/** + * Official Channels configuration. + * Controls runtime-only injection of Anthropic's official channel plugins. + */ +export interface OfficialChannelsConfig { + /** Selected official channels to auto-enable for compatible sessions */ + selected: OfficialChannelId[]; + /** Also add --dangerously-skip-permissions when auto-enable is active */ + unattended: boolean; +} + +/** + * Default Official Channels configuration. + * Disabled by default because the feature requires explicit user setup. + */ +export const DEFAULT_OFFICIAL_CHANNELS_CONFIG: OfficialChannelsConfig = { + selected: [], + unattended: false, +}; + +/** + * Dashboard authentication configuration. + * Optional login protection for CCS dashboard. + * Disabled by default for backward compatibility. + */ +export interface DashboardAuthConfig { + /** Enable dashboard authentication (default: false) */ + enabled: boolean; + /** Username for dashboard login */ + username: string; + /** Bcrypt-hashed password (use: npx bcrypt-cli hash 'password') */ + password_hash: string; + /** Session timeout in hours (default: 24) */ + session_timeout_hours?: number; +} + +/** + * Default dashboard auth configuration. + * Disabled by default - must be explicitly enabled. + */ +export const DEFAULT_DASHBOARD_AUTH_CONFIG: DashboardAuthConfig = { + enabled: false, + username: '', + password_hash: '', + session_timeout_hours: 24, +}; + +/** + * Browser automation configuration. + * Controls Claude browser attach and Codex browser tooling. + */ +export type BrowserToolPolicy = 'auto' | 'manual'; +export type BrowserEvalMode = 'disabled' | 'readonly' | 'readwrite'; + +export interface BrowserClaudeConfig { + /** Enable Claude browser attach (default: false) */ + enabled: boolean; + /** Control whether Claude browser attach is exposed automatically or only via --browser */ + policy: BrowserToolPolicy; + /** Chrome user-data directory used for attach mode */ + user_data_dir: string; + /** DevTools port used for attach mode (default: 9222) */ + devtools_port: number; + /** Eval access mode exposed through browser settings/status surfaces */ + eval_mode?: BrowserEvalMode; +} + +export interface BrowserCodexConfig { + /** Enable Codex browser tooling injection (default: false) */ + enabled: boolean; + /** Control whether Codex browser tooling is exposed automatically or only via --browser */ + policy: BrowserToolPolicy; + /** Eval access mode exposed through browser settings/status surfaces */ + eval_mode?: BrowserEvalMode; +} + +export interface BrowserConfig { + claude: BrowserClaudeConfig; + codex: BrowserCodexConfig; +} + +export const DEFAULT_BROWSER_CONFIG: BrowserConfig = { + claude: { + enabled: false, + policy: 'manual', + user_data_dir: '', + devtools_port: 9222, + eval_mode: 'readonly', + }, + codex: { + enabled: false, + policy: 'manual', + eval_mode: 'readonly', + }, +}; + +/** + * Image analysis configuration. + * Routes image/PDF files through CLIProxy for vision analysis. + */ +export interface ImageAnalysisConfig { + /** Enable image analysis via CLIProxy (default: true) */ + enabled: boolean; + /** Timeout in seconds (default: 60) */ + timeout: number; + /** Provider-to-model mapping for vision analysis */ + provider_models: Record; + /** Fallback backend used when a profile does not resolve to a provider-specific backend */ + fallback_backend?: string; + /** Explicit profile-name-to-backend overrides for settings/custom aliases */ + profile_backends?: Record; +} + +/** + * Default image analysis configuration. + * Enabled by default for CLIProxy providers with vision support. + */ +export const DEFAULT_IMAGE_ANALYSIS_CONFIG: ImageAnalysisConfig = { + enabled: true, + timeout: 60, + provider_models: { + agy: 'gemini-3-1-flash-preview', + gemini: 'gemini-3-flash-preview', + codex: 'gpt-5.1-codex-mini', + kiro: 'kiro-claude-haiku-4-5', + ghcp: 'claude-haiku-4.5', + claude: 'claude-haiku-4-5-20251001', + // 'vision-model' is a generic placeholder - users can override via config.yaml + qwen: 'vision-model', + iflow: 'qwen3-vl-plus', + kimi: 'vision-model', + }, + fallback_backend: 'gemini', + profile_backends: {}, +}; + +/** + * Main unified configuration structure. + * Stored in ~/.ccs/config.yaml + */ +export interface UnifiedConfig { + /** Config version (7 for quota management) */ + version: number; + /** Flag indicating setup wizard has been completed */ + setup_completed?: boolean; + /** Default profile name to use when none specified */ + default?: string; + /** Account-based profiles (isolated Claude instances) */ + accounts: Record; + /** API-based profiles (env var injection) */ + profiles: Record; + /** CLIProxy configuration */ + cliproxy: CLIProxyConfig; + /** OpenAI-compatible local proxy configuration */ + proxy?: OpenAICompatProxyConfig; + /** CCS-owned structured logging configuration */ + logging?: LoggingConfig; + /** User preferences */ + preferences: PreferencesConfig; + /** WebSearch configuration */ + websearch?: WebSearchConfig; + /** Global environment variables for all non-Claude subscription profiles */ + global_env?: GlobalEnvConfig; + /** Cross-profile continuity inheritance mapping */ + continuity?: ContinuityConfig; + /** Copilot API configuration (GitHub Copilot proxy) */ + copilot?: CopilotConfig; + /** Cursor IDE configuration (Cursor proxy daemon) */ + cursor?: CursorConfig; + /** CLIProxy server configuration for remote/local mode */ + cliproxy_server?: CliproxyServerConfig; + /** Quota management configuration (v7+) */ + quota_management?: QuotaManagementConfig; + /** Thinking/reasoning budget configuration (v8+) */ + thinking?: ThinkingConfig; + /** Discord Channels runtime auto-enable preferences (v11+) */ + channels?: OfficialChannelsConfig; + /** Dashboard authentication configuration (optional) */ + dashboard_auth?: DashboardAuthConfig; + /** Browser automation configuration */ + browser?: BrowserConfig; + /** Image analysis configuration (vision via CLIProxy) */ + image_analysis?: ImageAnalysisConfig; +} + +/** + * Default Copilot configuration. + * Strictly opt-in - disabled by default. + * Uses gpt-4.1 as default model (free tier compatible). + */ +export const DEFAULT_COPILOT_CONFIG: CopilotConfig = { + enabled: false, + auto_start: false, + port: 4141, + account_type: 'individual', + rate_limit: null, + wait_on_limit: true, + model: 'gpt-4.1', // Free tier compatible +}; + +/** + * Default Cursor configuration. + * Disabled by default, ghost mode enabled for privacy. + */ +export const DEFAULT_CURSOR_CONFIG: CursorConfig = { + enabled: false, + port: 20129, + auto_start: false, + ghost_mode: true, + model: 'gpt-5.3-codex', +}; + +/** + * Default CLIProxy server configuration. + * Local mode by default - remote must be explicitly enabled. + * Port is optional for remote - defaults based on protocol. + */ +export const DEFAULT_CLIPROXY_SERVER_CONFIG: CliproxyServerConfig = { + remote: { + enabled: false, + host: '', + // port is intentionally omitted - will use protocol default (443 for HTTPS, 8317 for HTTP) + protocol: 'http', + auth_token: '', + }, + fallback: { + enabled: true, + auto_start: false, + }, + local: { + port: 8317, + auto_start: true, + }, +}; + +export const DEFAULT_OPENAI_COMPAT_PROXY_CONFIG: OpenAICompatProxyConfig = { + profile_ports: {}, + routing: { + longContextThreshold: 60_000, + }, +}; + +/** + * Create an empty unified config with defaults. + */ +export function createEmptyUnifiedConfig(): UnifiedConfig { + return { + version: UNIFIED_CONFIG_VERSION, + default: undefined, + accounts: {}, + profiles: {}, + cliproxy: { + backend: 'original', + oauth_accounts: {}, + providers: [...CLIPROXY_SUPPORTED_PROVIDERS], + variants: {}, + logging: { + enabled: false, + request_log: false, + }, + safety: { ...DEFAULT_CLIPROXY_SAFETY_CONFIG }, + auto_sync: true, + routing: { + strategy: 'round-robin', + session_affinity: false, + session_affinity_ttl: '1h', + }, + }, + proxy: { + port: DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.port, + profile_ports: { ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.profile_ports }, + routing: { + ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.routing, + }, + }, + logging: { ...DEFAULT_LOGGING_CONFIG }, + preferences: { + theme: 'system', + telemetry: false, + auto_update: true, + }, + websearch: { + enabled: true, + providers: { + exa: { + enabled: false, + max_results: 5, + }, + tavily: { + enabled: false, + max_results: 5, + }, + brave: { + enabled: false, + max_results: 5, + }, + searxng: { + enabled: false, + url: '', + max_results: 5, + }, + duckduckgo: { + enabled: true, + max_results: 5, + }, + gemini: { + enabled: false, + model: 'gemini-2.5-flash', + timeout: 55, + }, + opencode: { + enabled: false, + model: 'opencode/grok-code', + timeout: 90, + }, + grok: { + enabled: false, + timeout: 55, + }, + }, + }, + global_env: { + enabled: true, + env: { ...DEFAULT_GLOBAL_ENV }, + }, + copilot: { ...DEFAULT_COPILOT_CONFIG }, + cursor: { ...DEFAULT_CURSOR_CONFIG }, + cliproxy_server: { ...DEFAULT_CLIPROXY_SERVER_CONFIG }, + quota_management: { ...DEFAULT_QUOTA_MANAGEMENT_CONFIG }, + thinking: { ...DEFAULT_THINKING_CONFIG }, + channels: { ...DEFAULT_OFFICIAL_CHANNELS_CONFIG }, + dashboard_auth: { ...DEFAULT_DASHBOARD_AUTH_CONFIG }, + browser: { + claude: { ...DEFAULT_BROWSER_CONFIG.claude }, + codex: { ...DEFAULT_BROWSER_CONFIG.codex }, + }, + image_analysis: { ...DEFAULT_IMAGE_ANALYSIS_CONFIG }, + }; +} + +/** + * Type guard for UnifiedConfig. + * Relaxed validation: accepts configs with version >= 1 and any subset of sections. + * Missing sections will be filled with defaults during merge. + */ +export function isUnifiedConfig(obj: unknown): obj is UnifiedConfig { + if (typeof obj !== 'object' || obj === null) return false; + const config = obj as Record; + // Only require version to be a number >= 1 (allow future versions) + // Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig + return typeof config.version === 'number' && config.version >= 1; +} From 1e9a7f3fa01e40ab569932e66ce37f8653bf8c97 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 14:03:48 -0400 Subject: [PATCH 04/12] refactor(config): add config-loader-facade with memoization Single import path for all config loading. Re-exports all 26 functions from unified-config-loader and 4 from config-manager. Adds memoization for loadOrCreateUnifiedConfig via getCachedConfig() with automatic cache invalidation on write operations (mutateConfig, updateConfig). Pure structural refactor -- no existing imports modified. --- .../__tests__/config-loader-facade.test.ts | 191 ++++++++++++++++++ src/config/config-loader-facade.ts | 120 +++++++++++ 2 files changed, 311 insertions(+) create mode 100644 src/config/__tests__/config-loader-facade.test.ts create mode 100644 src/config/config-loader-facade.ts diff --git a/src/config/__tests__/config-loader-facade.test.ts b/src/config/__tests__/config-loader-facade.test.ts new file mode 100644 index 00000000..f780bcc8 --- /dev/null +++ b/src/config/__tests__/config-loader-facade.test.ts @@ -0,0 +1,191 @@ +/** + * Config Loader Facade Unit Tests + * + * Tests memoization cache behavior, cache invalidation on write ops, + * and verifies all re-exports are present from underlying modules. + */ + +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as yaml from 'js-yaml'; + +/** + * Helper: create a temp home dir with a minimal valid config.yaml so + * loadOrCreateUnifiedConfig succeeds without touching the real ~/.ccs. + */ +function createTestHome(): string { + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-facade-test-')); + const ccsDir = path.join(tempHome, '.ccs'); + fs.mkdirSync(ccsDir, { recursive: true }); + const configPath = path.join(ccsDir, 'config.yaml'); + fs.writeFileSync(configPath, `version: 1\n`, 'utf8'); + return tempHome; +} + +/** + * Helper: get the facade module, bypassing the import cache each time. + * We use dynamic import with a cache-busting query param so beforeEach + * re-imports get a fresh module with a clean cache state. + */ +async function importFacade(): Promise { + return import(`../config-loader-facade?cachebust=${Date.now()}`); +} + +describe('config-loader-facade', () => { + let tempHome: string; + let originalCcsHome: string | undefined; + + beforeEach(() => { + tempHome = createTestHome(); + originalCcsHome = process.env.CCS_HOME; + process.env.CCS_HOME = tempHome; + }); + + afterEach(() => { + // Restore env + if (originalCcsHome !== undefined) { + process.env.CCS_HOME = originalCcsHome; + } else { + delete process.env.CCS_HOME; + } + + // Clean up temp dir + if (tempHome && fs.existsSync(tempHome)) { + fs.rmSync(tempHome, { recursive: true, force: true }); + } + }); + + describe('re-exports from unified-config-loader', () => { + it('should export all core loader functions', async () => { + const facade = await importFacade(); + + expect(typeof facade.loadUnifiedConfig).toBe('function'); + expect(typeof facade.loadOrCreateUnifiedConfig).toBe('function'); + expect(typeof facade.saveUnifiedConfig).toBe('function'); + expect(typeof facade.mutateUnifiedConfig).toBe('function'); + expect(typeof facade.updateUnifiedConfig).toBe('function'); + }); + + it('should export all path/format utilities', async () => { + const facade = await importFacade(); + + expect(typeof facade.getConfigYamlPath).toBe('function'); + expect(typeof facade.getConfigJsonPath).toBe('function'); + expect(typeof facade.hasUnifiedConfig).toBe('function'); + expect(typeof facade.hasLegacyConfig).toBe('function'); + expect(typeof facade.getConfigFormat).toBe('function'); + expect(typeof facade.isUnifiedMode).toBe('function'); + }); + + it('should export all profile getters', async () => { + const facade = await importFacade(); + + expect(typeof facade.getDefaultProfile).toBe('function'); + expect(typeof facade.setDefaultProfile).toBe('function'); + }); + + it('should export all section getters', async () => { + const facade = await importFacade(); + + expect(typeof facade.getWebSearchConfig).toBe('function'); + expect(typeof facade.getGlobalEnvConfig).toBe('function'); + expect(typeof facade.getContinuityInheritanceMap).toBe('function'); + expect(typeof facade.getCliproxySafetyConfig).toBe('function'); + expect(typeof facade.getThinkingConfig).toBe('function'); + expect(typeof facade.getOfficialChannelsConfig).toBe('function'); + expect(typeof facade.isDashboardAuthEnabled).toBe('function'); + expect(typeof facade.getDashboardAuthConfig).toBe('function'); + expect(typeof facade.getBrowserConfig).toBe('function'); + expect(typeof facade.getImageAnalysisConfig).toBe('function'); + expect(typeof facade.getLoggingConfig).toBe('function'); + expect(typeof facade.getCursorConfig).toBe('function'); + }); + }); + + describe('re-exports from config-manager', () => { + it('should export loadSettings, loadConfigSafe, readConfig, getCcsDir', async () => { + const facade = await importFacade(); + + expect(typeof facade.loadSettings).toBe('function'); + expect(typeof facade.loadConfigSafe).toBe('function'); + expect(typeof facade.readConfig).toBe('function'); + expect(typeof facade.getCcsDir).toBe('function'); + }); + }); + + describe('memoization', () => { + it('getCachedConfig returns same object on repeated calls', async () => { + const facade = await importFacade(); + + const first = facade.getCachedConfig(); + const second = facade.getCachedConfig(); + + // Same reference (cached, not re-read) + expect(first).toBe(second); + }); + + it('invalidateConfigCache forces re-read on next getCachedConfig', async () => { + const facade = await importFacade(); + + const first = facade.getCachedConfig(); + facade.invalidateConfigCache(); + const second = facade.getCachedConfig(); + + // Different reference after invalidation + expect(first).not.toBe(second); + // But same content + expect(first.version).toBe(second.version); + }); + + it('saveConfig updates cache and does not invalidate', async () => { + const facade = await importFacade(); + + const config = facade.getCachedConfig(); + config.default = 'test-profile'; + facade.saveConfig(config); + + const cached = facade.getCachedConfig(); + // Cache should hold the just-saved config (no re-read) + expect(cached).toBe(config); + expect(cached.default).toBe('test-profile'); + }); + + it('mutateConfig invalidates the cache', async () => { + const facade = await importFacade(); + + const before = facade.getCachedConfig(); + facade.mutateConfig((cfg) => { + cfg.default = 'mutated-profile'; + }); + const after = facade.getCachedConfig(); + + // Different reference (mutator may change it arbitrarily) + expect(before).not.toBe(after); + expect(after.default).toBe('mutated-profile'); + }); + + it('updateConfig invalidates the cache', async () => { + const facade = await importFacade(); + + const before = facade.getCachedConfig(); + facade.updateConfig({ default: 'updated-profile' }); + const after = facade.getCachedConfig(); + + expect(before).not.toBe(after); + expect(after.default).toBe('updated-profile'); + }); + + it('getCachedConfig returns valid config with expected fields', async () => { + const facade = await importFacade(); + + const config = facade.getCachedConfig(); + expect(config).toBeDefined(); + expect(typeof config.version).toBe('number'); + expect(config.accounts).toBeDefined(); + expect(config.profiles).toBeDefined(); + expect(config.cliproxy).toBeDefined(); + }); + }); +}); diff --git a/src/config/config-loader-facade.ts b/src/config/config-loader-facade.ts new file mode 100644 index 00000000..40b526fc --- /dev/null +++ b/src/config/config-loader-facade.ts @@ -0,0 +1,120 @@ +/** + * Config Loader Facade + * + * Single import path for all config loading operations. + * Re-exports everything from unified-config-loader and config-manager, + * and adds memoization for loadOrCreateUnifiedConfig to reduce file I/O. + * + * Usage: + * import { getCachedConfig, saveConfig, mutateConfig } from '../config/config-loader-facade'; + * import { getCcsDir, loadSettings } from '../config/config-loader-facade'; + */ + +// Re-export all functions from unified-config-loader +export { + loadUnifiedConfig, + loadOrCreateUnifiedConfig, + saveUnifiedConfig, + mutateUnifiedConfig, + updateUnifiedConfig, + getConfigYamlPath, + getConfigJsonPath, + hasUnifiedConfig, + hasLegacyConfig, + getConfigFormat, + isUnifiedMode, + getDefaultProfile, + setDefaultProfile, + getWebSearchConfig, + getGlobalEnvConfig, + getContinuityInheritanceMap, + getCliproxySafetyConfig, + getThinkingConfig, + getOfficialChannelsConfig, + isDashboardAuthEnabled, + getDashboardAuthConfig, + getBrowserConfig, + getImageAnalysisConfig, + getLoggingConfig, + getCursorConfig, +} from './unified-config-loader'; + +// Re-export types from unified-config-loader +export type { GeminiWebSearchInfo } from './unified-config-loader'; + +// Re-export selected functions from config-manager +export { loadSettings, loadConfigSafe, readConfig, getCcsDir } from '../utils/config-manager'; + +// Internal imports for memoization wrappers +import type { UnifiedConfig } from './unified-config-types'; +import { + loadOrCreateUnifiedConfig as _loadOrCreateUnifiedConfig, + saveUnifiedConfig as _saveUnifiedConfig, + mutateUnifiedConfig as _mutateUnifiedConfig, + updateUnifiedConfig as _updateUnifiedConfig, +} from './unified-config-loader'; + +// --------------------------------------------------------------------------- +// Memoization cache +// --------------------------------------------------------------------------- + +let _configCache: UnifiedConfig | null = null; + +/** + * Get the unified config with in-memory caching. + * First call reads from disk; subsequent calls return the cached object. + * + * Call invalidateConfigCache() or use mutateConfig()/updateConfig() + * to force a re-read from disk. + */ +export function getCachedConfig(): UnifiedConfig { + if (!_configCache) { + _configCache = _loadOrCreateUnifiedConfig(); + } + return _configCache; +} + +/** + * Clear the memoization cache. + * The next call to getCachedConfig() will re-read from disk. + */ +export function invalidateConfigCache(): void { + _configCache = null; +} + +/** + * Save config to disk and update the cache to the given object. + * Does NOT invalidate — the provided config IS the new cache value. + */ +export function saveConfig(config: UnifiedConfig): void { + _saveUnifiedConfig(config); + _configCache = config; +} + +/** + * Atomically mutate config (read-modify-write with lock) and invalidate cache. + * After mutation, the next getCachedConfig() call will re-read from disk. + */ +export function mutateConfig(mutator: (config: UnifiedConfig) => void): UnifiedConfig { + const result = _mutateUnifiedConfig(mutator); + _configCache = null; + return result; +} + +/** + * Partial-update config and invalidate cache. + * Shorthand for mutateConfig with Object.assign. + */ +export function updateConfig(updates: Partial): UnifiedConfig { + const result = _updateUnifiedConfig(updates); + _configCache = null; + return result; +} + +/** + * Get the current cache state (for diagnostics/testing). + * Returns true if a cached config exists, false otherwise. + */ +export function hasCachedConfig(): boolean { + return _configCache !== null; +} From 0868e92bb192ff5d584e99fb39216860ced3bc11 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 14:05:33 -0400 Subject: [PATCH 05/12] Revert "Revert "refactor(config): reorganize unified-config-types into schemas directory"" This reverts commit 06bce198eb8b9c6508e7b5fc3339cbda54faf608. --- .../__tests__/schemas-reexport.test.ts | 198 +++ src/config/schemas/auth.ts | 109 ++ src/config/schemas/browser.ts | 71 ++ src/config/schemas/channels.ts | 32 + src/config/schemas/cliproxy.ts | 151 +++ src/config/schemas/copilot-cursor.ts | 93 ++ src/config/schemas/index.ts | 112 ++ src/config/schemas/logging.ts | 53 + src/config/schemas/providers.ts | 30 + src/config/schemas/proxy-server.ts | 193 +++ src/config/schemas/quota.ts | 121 ++ src/config/schemas/thinking.ts | 66 + src/config/schemas/unified-config.ts | 200 +++ src/config/schemas/version.ts | 23 + src/config/schemas/websearch.ts | 148 +++ src/config/unified-config-types.ts | 1120 +---------------- 16 files changed, 1603 insertions(+), 1117 deletions(-) create mode 100644 src/config/schemas/__tests__/schemas-reexport.test.ts create mode 100644 src/config/schemas/auth.ts create mode 100644 src/config/schemas/browser.ts create mode 100644 src/config/schemas/channels.ts create mode 100644 src/config/schemas/cliproxy.ts create mode 100644 src/config/schemas/copilot-cursor.ts create mode 100644 src/config/schemas/index.ts create mode 100644 src/config/schemas/logging.ts create mode 100644 src/config/schemas/providers.ts create mode 100644 src/config/schemas/proxy-server.ts create mode 100644 src/config/schemas/quota.ts create mode 100644 src/config/schemas/thinking.ts create mode 100644 src/config/schemas/unified-config.ts create mode 100644 src/config/schemas/version.ts create mode 100644 src/config/schemas/websearch.ts diff --git a/src/config/schemas/__tests__/schemas-reexport.test.ts b/src/config/schemas/__tests__/schemas-reexport.test.ts new file mode 100644 index 00000000..5bdda5d9 --- /dev/null +++ b/src/config/schemas/__tests__/schemas-reexport.test.ts @@ -0,0 +1,198 @@ +/** + * Tests: config schemas re-export backward compatibility. + * + * Verifies that every type, interface, constant, and function originally + * exported from unified-config-types.ts is still accessible via both + * the barrel file and the schemas/index barrel. + */ + +import { describe, it, expect } from 'bun:test'; + +// Import from the backward-compatible barrel (this is what all existing code uses) +import * as barrel from '../../unified-config-types'; + +// Import from the new schemas barrel (this is what the barrel delegates to) +import * as schemas from '../index'; + +// --------------------------------------------------------------------------- +// Type-level checks (compile-time, not runtime) +// --------------------------------------------------------------------------- + +// Verify key interfaces are accessible as types +import type { + UnifiedConfig, + AccountConfig, + ProfileConfig, + OAuthAccounts, + CLIProxyAuthConfig, + TokenRefreshSettings, + DashboardAuthConfig, + CLIProxyVariantConfig, + CompositeTierConfig, + CompositeVariantConfig, + CLIProxyLoggingConfig, + CLIProxySafetyConfig, + CLIProxyRoutingConfig, + CLIProxyConfig, + AutoQuotaConfig, + RuntimeMonitorConfig, + ManualQuotaConfig, + QuotaManagementMode, + QuotaManagementConfig, + ThinkingMode, + ThinkingTierDefaults, + ThinkingConfig, + OfficialChannelId, + OfficialChannelsConfig, + DuckDuckGoWebSearchConfig, + BraveWebSearchConfig, + ExaWebSearchConfig, + TavilyWebSearchConfig, + SearxngWebSearchConfig, + GeminiWebSearchConfig, + GrokWebSearchConfig, + OpenCodeWebSearchConfig, + WebSearchProvidersConfig, + WebSearchConfig, + BrowserToolPolicy, + BrowserEvalMode, + BrowserClaudeConfig, + BrowserCodexConfig, + BrowserConfig, + LoggingLevel, + LoggingConfig, + PreferencesConfig, + CopilotAccountType, + CopilotConfig, + CursorConfig, + ProxyRemoteConfig, + ProxyFallbackConfig, + ProxyLocalConfig, + OpenAICompatProxyRoutingConfig, + OpenAICompatProxyConfig, + CliproxyServerConfig, + GlobalEnvConfig, + ContinuityConfig, + ImageAnalysisConfig, +} from '../../unified-config-types'; + +describe('config schemas backward compatibility', () => { + // ------------------------------------------------------------------------- + // Constants + // ------------------------------------------------------------------------- + it('re-exports UNIFIED_CONFIG_VERSION', () => { + expect(barrel.UNIFIED_CONFIG_VERSION).toBe(13); + expect(schemas.UNIFIED_CONFIG_VERSION).toBe(13); + }); + + it('re-exports CLIPROXY_SUPPORTED_PROVIDERS', () => { + expect(Array.isArray(barrel.CLIPROXY_SUPPORTED_PROVIDERS)).toBe(true); + expect(barrel.CLIPROXY_SUPPORTED_PROVIDERS.length).toBeGreaterThan(0); + expect(barrel.CLIPROXY_SUPPORTED_PROVIDERS).toEqual(schemas.CLIPROXY_SUPPORTED_PROVIDERS); + }); + + // ------------------------------------------------------------------------- + // Default constants + // ------------------------------------------------------------------------- + const defaultConstants = [ + 'DEFAULT_CLIPROXY_SAFETY_CONFIG', + 'DEFAULT_LOGGING_CONFIG', + 'DEFAULT_OFFICIAL_CHANNELS_CONFIG', + 'DEFAULT_BROWSER_CONFIG', + 'DEFAULT_DASHBOARD_AUTH_CONFIG', + 'DEFAULT_AUTO_QUOTA_CONFIG', + 'DEFAULT_MANUAL_QUOTA_CONFIG', + 'DEFAULT_RUNTIME_MONITOR_CONFIG', + 'DEFAULT_QUOTA_MANAGEMENT_CONFIG', + 'DEFAULT_THINKING_TIER_DEFAULTS', + 'DEFAULT_THINKING_CONFIG', + 'DEFAULT_COPILOT_CONFIG', + 'DEFAULT_CURSOR_CONFIG', + 'DEFAULT_CLIPROXY_SERVER_CONFIG', + 'DEFAULT_OPENAI_COMPAT_PROXY_CONFIG', + 'DEFAULT_GLOBAL_ENV', + 'DEFAULT_IMAGE_ANALYSIS_CONFIG', + ] as const; + + for (const name of defaultConstants) { + it(`re-exports ${name}`, () => { + expect(barrel[name]).toBeDefined(); + expect(barrel[name]).toEqual(schemas[name]); + }); + } + + // ------------------------------------------------------------------------- + // Functions + // ------------------------------------------------------------------------- + it('re-exports createEmptyUnifiedConfig', () => { + expect(typeof barrel.createEmptyUnifiedConfig).toBe('function'); + expect(typeof schemas.createEmptyUnifiedConfig).toBe('function'); + + const config = barrel.createEmptyUnifiedConfig(); + expect(config.version).toBe(13); + expect(config.accounts).toEqual({}); + expect(config.profiles).toEqual({}); + expect(config.cliproxy).toBeDefined(); + expect(config.cliproxy.oauth_accounts).toEqual({}); + expect(config.cliproxy.variants).toEqual({}); + expect(config.logging).toBeDefined(); + expect(config.preferences).toBeDefined(); + expect(config.browser).toBeDefined(); + expect(config.image_analysis).toBeDefined(); + expect(config.quota_management).toBeDefined(); + expect(config.thinking).toBeDefined(); + expect(config.channels).toBeDefined(); + expect(config.dashboard_auth).toBeDefined(); + expect(config.copilot).toBeDefined(); + expect(config.cursor).toBeDefined(); + expect(config.cliproxy_server).toBeDefined(); + expect(config.websearch).toBeDefined(); + }); + + it('re-exports isUnifiedConfig', () => { + expect(typeof barrel.isUnifiedConfig).toBe('function'); + expect(typeof schemas.isUnifiedConfig).toBe('function'); + + expect(barrel.isUnifiedConfig({ version: 13 })).toBe(true); + expect(barrel.isUnifiedConfig(null)).toBe(false); + expect(barrel.isUnifiedConfig({})).toBe(false); + expect(barrel.isUnifiedConfig({ version: 0 })).toBe(false); + expect(barrel.isUnifiedConfig({ version: 1 })).toBe(true); + expect(barrel.isUnifiedConfig('not an object')).toBe(false); + }); + + // ------------------------------------------------------------------------- + // Barrel has all expected runtime exports (type-only exports are verified + // at compile time via the import type block above — they are erased at + // runtime and cannot be checked with the `in` operator). + // ------------------------------------------------------------------------- + const expectedRuntimeExports = [ + 'UNIFIED_CONFIG_VERSION', + 'CLIPROXY_SUPPORTED_PROVIDERS', + 'createEmptyUnifiedConfig', + 'isUnifiedConfig', + 'DEFAULT_CLIPROXY_SAFETY_CONFIG', + 'DEFAULT_LOGGING_CONFIG', + 'DEFAULT_OFFICIAL_CHANNELS_CONFIG', + 'DEFAULT_BROWSER_CONFIG', + 'DEFAULT_DASHBOARD_AUTH_CONFIG', + 'DEFAULT_AUTO_QUOTA_CONFIG', + 'DEFAULT_MANUAL_QUOTA_CONFIG', + 'DEFAULT_RUNTIME_MONITOR_CONFIG', + 'DEFAULT_QUOTA_MANAGEMENT_CONFIG', + 'DEFAULT_THINKING_TIER_DEFAULTS', + 'DEFAULT_THINKING_CONFIG', + 'DEFAULT_COPILOT_CONFIG', + 'DEFAULT_CURSOR_CONFIG', + 'DEFAULT_CLIPROXY_SERVER_CONFIG', + 'DEFAULT_OPENAI_COMPAT_PROXY_CONFIG', + 'DEFAULT_GLOBAL_ENV', + 'DEFAULT_IMAGE_ANALYSIS_CONFIG', + ] as const; + + for (const name of expectedRuntimeExports) { + it(`barrel exports "${name}"`, () => { + expect(name in barrel).toBe(true); + }); + } +}); diff --git a/src/config/schemas/auth.ts b/src/config/schemas/auth.ts new file mode 100644 index 00000000..4056ecf8 --- /dev/null +++ b/src/config/schemas/auth.ts @@ -0,0 +1,109 @@ +/** + * Account, profile, and authentication config types. + * + * Covers: + * - AccountConfig: isolated Claude instances via CLAUDE_CONFIG_DIR + * - ProfileConfig: API-based profiles (env var injection) + * - OAuthAccounts: CLIProxy nickname-to-email mapping + * - CLIProxyAuthConfig: API key and management secret customization + * - TokenRefreshSettings: background token refresh worker config + * - DashboardAuthConfig: dashboard login protection + */ + +import type { TargetType } from '../../targets/target-adapter'; + +/** + * Account configuration (formerly in profiles.json). + * Represents an isolated Claude instance via CLAUDE_CONFIG_DIR. + */ +export interface AccountConfig { + /** ISO timestamp when account was created */ + created: string; + /** ISO timestamp of last usage, null if never used */ + last_used: string | null; + /** Context mode for project workspace data */ + context_mode?: 'isolated' | 'shared'; + /** Context-sharing group when context_mode='shared' */ + context_group?: string; + /** Shared continuity depth when context_mode='shared' */ + continuity_mode?: 'standard' | 'deeper'; + /** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */ + bare?: boolean; +} + +/** + * API-based profile configuration. + * Injects environment variables for alternative providers (GLM, Kimi, etc.). + * + * Settings are stored in separate *.settings.json files (matching Claude's pattern) + * to allow users to edit them directly without touching config.yaml. + */ +export interface ProfileConfig { + /** Profile type - currently only 'api' */ + type: 'api'; + /** Path to settings file (e.g., "~/.ccs/glm.settings.json") */ + settings: string; + /** Target CLI to use for this profile (default: 'claude') */ + target?: TargetType; +} + +/** + * CLIProxy OAuth account nickname mapping. + * Maps user-friendly nicknames to email addresses. + */ +export type OAuthAccounts = Record; + +/** + * CLIProxy authentication configuration. + * Allows customization of API key and management secret for CLIProxyAPI. + */ +export interface CLIProxyAuthConfig { + /** API key for CCS-managed requests (default: 'ccs-internal-managed') */ + api_key?: string; + /** Management secret for Control Panel login (default: 'ccs') */ + management_secret?: string; +} + +/** + * Token refresh configuration. + * Manages background token refresh worker settings. + */ +export interface TokenRefreshSettings { + /** Enable background token refresh (default: false) */ + enabled?: boolean; + /** Refresh check interval in minutes (default: 30) */ + interval_minutes?: number; + /** Preemptive refresh time in minutes (default: 45) */ + preemptive_minutes?: number; + /** Maximum retry attempts per token (default: 3) */ + max_retries?: number; + /** Enable verbose logging (default: false) */ + verbose?: boolean; +} + +/** + * Dashboard authentication configuration. + * Optional login protection for CCS dashboard. + * Disabled by default for backward compatibility. + */ +export interface DashboardAuthConfig { + /** Enable dashboard authentication (default: false) */ + enabled: boolean; + /** Username for dashboard login */ + username: string; + /** Bcrypt-hashed password (use: npx bcrypt-cli hash 'password') */ + password_hash: string; + /** Session timeout in hours (default: 24) */ + session_timeout_hours?: number; +} + +/** + * Default dashboard auth configuration. + * Disabled by default - must be explicitly enabled. + */ +export const DEFAULT_DASHBOARD_AUTH_CONFIG: DashboardAuthConfig = { + enabled: false, + username: '', + password_hash: '', + session_timeout_hours: 24, +}; diff --git a/src/config/schemas/browser.ts b/src/config/schemas/browser.ts new file mode 100644 index 00000000..f4facb4b --- /dev/null +++ b/src/config/schemas/browser.ts @@ -0,0 +1,71 @@ +/** + * Browser automation configuration types and defaults. + * + * Controls Claude browser attach and Codex browser tooling. + * Version 13+ feature. + */ + +/** + * Browser tool exposure policy. + */ +export type BrowserToolPolicy = 'auto' | 'manual'; + +/** + * Browser eval access mode. + */ +export type BrowserEvalMode = 'disabled' | 'readonly' | 'readwrite'; + +/** + * Claude browser attach configuration. + */ +export interface BrowserClaudeConfig { + /** Enable Claude browser attach (default: false) */ + enabled: boolean; + /** Control whether Claude browser attach is exposed automatically or only via --browser */ + policy: BrowserToolPolicy; + /** Chrome user-data directory used for attach mode */ + user_data_dir: string; + /** DevTools port used for attach mode (default: 9222) */ + devtools_port: number; + /** Eval access mode exposed through browser settings/status surfaces */ + eval_mode?: BrowserEvalMode; +} + +/** + * Codex browser tooling configuration. + */ +export interface BrowserCodexConfig { + /** Enable Codex browser tooling injection (default: false) */ + enabled: boolean; + /** Control whether Codex browser tooling is exposed automatically or only via --browser */ + policy: BrowserToolPolicy; + /** Eval access mode exposed through browser settings/status surfaces */ + eval_mode?: BrowserEvalMode; +} + +/** + * Browser automation configuration. + * Controls Claude browser attach and Codex browser tooling. + */ +export interface BrowserConfig { + claude: BrowserClaudeConfig; + codex: BrowserCodexConfig; +} + +/** + * Default browser configuration. + */ +export const DEFAULT_BROWSER_CONFIG: BrowserConfig = { + claude: { + enabled: false, + policy: 'manual', + user_data_dir: '', + devtools_port: 9222, + eval_mode: 'readonly', + }, + codex: { + enabled: false, + policy: 'manual', + eval_mode: 'readonly', + }, +}; diff --git a/src/config/schemas/channels.ts b/src/config/schemas/channels.ts new file mode 100644 index 00000000..f8d5274c --- /dev/null +++ b/src/config/schemas/channels.ts @@ -0,0 +1,32 @@ +/** + * Official Channels configuration types and defaults. + * + * Controls runtime-only injection of Anthropic's official channel plugins + * (Telegram, Discord, iMessage). + * Version 12+ feature. + */ + +/** + * Supported Anthropic official channel IDs. + */ +export type OfficialChannelId = 'telegram' | 'discord' | 'imessage'; + +/** + * Official Channels configuration. + * Controls runtime-only injection of Anthropic's official channel plugins. + */ +export interface OfficialChannelsConfig { + /** Selected official channels to auto-enable for compatible sessions */ + selected: OfficialChannelId[]; + /** Also add --dangerously-skip-permissions when auto-enable is active */ + unattended: boolean; +} + +/** + * Default Official Channels configuration. + * Disabled by default because the feature requires explicit user setup. + */ +export const DEFAULT_OFFICIAL_CHANNELS_CONFIG: OfficialChannelsConfig = { + selected: [], + unattended: false, +}; diff --git a/src/config/schemas/cliproxy.ts b/src/config/schemas/cliproxy.ts new file mode 100644 index 00000000..1765335f --- /dev/null +++ b/src/config/schemas/cliproxy.ts @@ -0,0 +1,151 @@ +/** + * CLIProxy configuration types and defaults. + * + * Covers provider/variant/routing/safety/logging configuration + * for the CLIProxy integration layer. + */ + +import type { TargetType } from '../../targets/target-adapter'; +import type { CLIProxyProvider, CliproxyRoutingStrategy } from '../../cliproxy/types'; +import { CLIPROXY_PROVIDER_IDS } from '../../cliproxy/provider-capabilities'; +import type { OAuthAccounts, CLIProxyAuthConfig, TokenRefreshSettings } from './auth'; + +/** + * Supported CLIProxy providers. + * Derived from CLIPROXY_PROVIDER_IDS — single source of truth in provider-capabilities.ts. + */ +export { CLIPROXY_PROVIDER_IDS as CLIPROXY_SUPPORTED_PROVIDERS }; + +/** + * CLIProxy variant configuration. + * User-defined variants of built-in OAuth providers. + * + * Settings are stored in separate *.settings.json files (matching Claude's pattern) + * to allow users to edit them directly without touching config.yaml. + */ +export interface CLIProxyVariantConfig { + /** Base provider to use */ + provider: CLIProxyProvider; + /** Account nickname (references oauth_accounts) */ + account?: string; + /** Path to settings file (e.g., "~/.ccs/gemini-custom.settings.json") */ + settings?: string; + /** Unique port for variant isolation (8318-8417) */ + port?: number; + /** Per-variant auth override (optional) */ + auth?: CLIProxyAuthConfig; + /** Target CLI to use for this variant (default: 'claude') */ + target?: TargetType; +} + +/** + * Per-tier provider+model mapping for composite variants. + */ +export interface CompositeTierConfig { + /** Provider for this tier */ + provider: CLIProxyProvider; + /** Model ID to use for this tier */ + model: string; + /** Account nickname (optional, references oauth_accounts) */ + account?: string; + /** Fallback provider+model if primary fails */ + fallback?: { + provider: CLIProxyProvider; + model: string; + account?: string; + }; + /** Per-tier thinking budget override (e.g. 'xhigh', 'medium', 'off') */ + thinking?: string; +} + +/** + * Composite variant configuration. + * Mixes different providers per Claude tier (opus, sonnet, haiku) in a single profile. + * Uses CLIProxyAPI root endpoints (/v1/messages) for model-based routing + * instead of provider-specific endpoints (/api/provider/{provider}). + */ +export interface CompositeVariantConfig { + /** Discriminator for composite type */ + type: 'composite'; + /** Which tier ANTHROPIC_MODEL equals (default must be one of the three) */ + default_tier: 'opus' | 'sonnet' | 'haiku'; + /** Per-tier provider+model mapping */ + tiers: { + opus: CompositeTierConfig; + sonnet: CompositeTierConfig; + haiku: CompositeTierConfig; + }; + /** Path to settings file */ + settings?: string; + /** Shared port for the composite profile */ + port?: number; + /** Per-variant auth override (optional) */ + auth?: CLIProxyAuthConfig; + /** Target CLI to use for this composite variant (default: 'claude') */ + target?: TargetType; +} + +/** + * CLIProxy logging configuration. + * Controls whether CLIProxyAPI writes logs to disk. + * Logs can grow to several GB if left enabled. + */ +export interface CLIProxyLoggingConfig { + /** Enable logging to file (default: false to prevent disk bloat) */ + enabled?: boolean; + /** Enable request logging for debugging (default: false) */ + request_log?: boolean; +} + +/** + * CLIProxy safety configuration. + * Controls high-risk flow safeguards for supported providers. + */ +export interface CLIProxySafetyConfig { + /** Allow skipping AGY responsibility checks and Gemini dashboard typed acknowledgement */ + antigravity_ack_bypass?: boolean; +} + +/** + * Default CLIProxy safety configuration. + */ +export const DEFAULT_CLIPROXY_SAFETY_CONFIG: CLIProxySafetyConfig = { + antigravity_ack_bypass: false, +}; + +export interface CLIProxyRoutingConfig { + /** Credential selection strategy when multiple accounts match */ + strategy?: CliproxyRoutingStrategy; + /** Keep one conversation pinned to the same account when possible */ + session_affinity?: boolean; + /** Go-style duration for session-affinity binding retention */ + session_affinity_ttl?: string; +} + +/** + * CLIProxy configuration section. + */ +export interface CLIProxyConfig { + /** Backend selection: 'original' or 'plus' (default: 'original') */ + backend?: 'original' | 'plus'; + /** Nickname to email mapping for OAuth accounts */ + oauth_accounts: OAuthAccounts; + /** Built-in providers (read-only, for reference) */ + providers: readonly string[]; + /** User-defined provider variants (single-provider or composite) */ + variants: Record; + /** Logging configuration (disabled by default) */ + logging?: CLIProxyLoggingConfig; + /** Safety controls for high-risk provider flows */ + safety?: CLIProxySafetyConfig; + /** Kiro: disable incognito browser mode (use normal browser to save credentials) */ + kiro_no_incognito?: boolean; + /** Global auth configuration for CLIProxyAPI */ + auth?: CLIProxyAuthConfig; + /** Background token refresh worker settings */ + token_refresh?: TokenRefreshSettings; + /** Auto-sync API profiles to local CLIProxy config on settings change (default: true) */ + auto_sync?: boolean; + /** Routing strategy for multi-account CLIProxy selection */ + routing?: CLIProxyRoutingConfig; +} diff --git a/src/config/schemas/copilot-cursor.ts b/src/config/schemas/copilot-cursor.ts new file mode 100644 index 00000000..e0b33fbb --- /dev/null +++ b/src/config/schemas/copilot-cursor.ts @@ -0,0 +1,93 @@ +/** + * Copilot and Cursor IDE integration configuration types and defaults. + * + * Covers: + * - CopilotConfig: GitHub Copilot proxy integration (strictly opt-in) + * - CursorConfig: Cursor IDE proxy daemon + */ + +/** + * Copilot API account type. + */ +export type CopilotAccountType = 'individual' | 'business' | 'enterprise'; + +/** + * Copilot API configuration. + * Enables GitHub Copilot subscription usage via copilot-api proxy. + * Strictly opt-in - disabled by default. + * + * !! DISCLAIMER - USE AT YOUR OWN RISK !! + * This uses an UNOFFICIAL reverse-engineered API. + * Excessive usage may trigger GitHub account restrictions. + * CCS provides NO WARRANTY and accepts NO RESPONSIBILITY for any consequences. + */ +export interface CopilotConfig { + /** Enable Copilot integration (default: false) - must be explicitly enabled */ + enabled: boolean; + /** Auto-start copilot-api daemon when using profile (default: false) */ + auto_start: boolean; + /** Port for copilot-api proxy (default: 4141) */ + port: number; + /** GitHub Copilot account type (default: individual) */ + account_type: CopilotAccountType; + /** Rate limit in seconds between requests (null = no limit) */ + rate_limit: number | null; + /** Wait instead of error when rate limit is hit (default: true) */ + wait_on_limit: boolean; + /** Default model ID (e.g., claude-sonnet-4.5) */ + model: string; + /** Model mapping for Claude tiers - maps opus/sonnet/haiku to specific models */ + opus_model?: string; + sonnet_model?: string; + haiku_model?: string; +} + +/** + * Cursor IDE integration configuration. + * Enables Cursor IDE usage via cursor proxy daemon. + */ +export interface CursorConfig { + /** Enable Cursor integration (default: false) */ + enabled: boolean; + /** Port for cursor proxy daemon (default: 20129) */ + port: number; + /** Auto-start daemon when CCS starts (default: false) */ + auto_start: boolean; + /** Enable ghost mode to disable telemetry (default: true) */ + ghost_mode: boolean; + /** Default model ID used by Cursor integration */ + model: string; + /** Optional tier mapping for Claude-compatible model routing */ + opus_model?: string; + /** Optional tier mapping for Claude-compatible model routing */ + sonnet_model?: string; + /** Optional tier mapping for Claude-compatible model routing */ + haiku_model?: string; +} + +/** + * Default Copilot configuration. + * Strictly opt-in - disabled by default. + * Uses gpt-4.1 as default model (free tier compatible). + */ +export const DEFAULT_COPILOT_CONFIG: CopilotConfig = { + enabled: false, + auto_start: false, + port: 4141, + account_type: 'individual', + rate_limit: null, + wait_on_limit: true, + model: 'gpt-4.1', +}; + +/** + * Default Cursor configuration. + * Disabled by default, ghost mode enabled for privacy. + */ +export const DEFAULT_CURSOR_CONFIG: CursorConfig = { + enabled: false, + port: 20129, + auto_start: false, + ghost_mode: true, + model: 'gpt-5.3-codex', +}; diff --git a/src/config/schemas/index.ts b/src/config/schemas/index.ts new file mode 100644 index 00000000..ff0313f3 --- /dev/null +++ b/src/config/schemas/index.ts @@ -0,0 +1,112 @@ +/** + * Config schema barrel re-exports. + * + * All types, interfaces, constants, and functions originally in + * unified-config-types.ts are re-exported here for backward compatibility. + * Each module is responsible for a focused domain of the config schema. + */ + +// Version constant +export { UNIFIED_CONFIG_VERSION } from './version'; + +// Account, profile, OAuth, auth types +export type { + AccountConfig, + ProfileConfig, + OAuthAccounts, + CLIProxyAuthConfig, + TokenRefreshSettings, + DashboardAuthConfig, +} from './auth'; +export { DEFAULT_DASHBOARD_AUTH_CONFIG } from './auth'; + +// CLIProxy provider, variant, routing, safety, logging types +export { CLIPROXY_SUPPORTED_PROVIDERS, DEFAULT_CLIPROXY_SAFETY_CONFIG } from './cliproxy'; +export type { + CLIProxyVariantConfig, + CompositeTierConfig, + CompositeVariantConfig, + CLIProxyLoggingConfig, + CLIProxySafetyConfig, + CLIProxyRoutingConfig, + CLIProxyConfig, +} from './cliproxy'; + +// Quota management types and defaults +export { + DEFAULT_AUTO_QUOTA_CONFIG, + DEFAULT_MANUAL_QUOTA_CONFIG, + DEFAULT_RUNTIME_MONITOR_CONFIG, + DEFAULT_QUOTA_MANAGEMENT_CONFIG, +} from './quota'; +export type { + AutoQuotaConfig, + RuntimeMonitorConfig, + ManualQuotaConfig, + QuotaManagementMode, + QuotaManagementConfig, +} from './quota'; + +// Thinking/reasoning budget types and defaults +export { DEFAULT_THINKING_TIER_DEFAULTS, DEFAULT_THINKING_CONFIG } from './thinking'; +export type { ThinkingMode, ThinkingTierDefaults, ThinkingConfig } from './thinking'; + +// Official channels types and defaults +export { DEFAULT_OFFICIAL_CHANNELS_CONFIG } from './channels'; +export type { OfficialChannelId, OfficialChannelsConfig } from './channels'; + +// WebSearch backend types +export type { + DuckDuckGoWebSearchConfig, + BraveWebSearchConfig, + ExaWebSearchConfig, + TavilyWebSearchConfig, + SearxngWebSearchConfig, + GeminiWebSearchConfig, + GrokWebSearchConfig, + OpenCodeWebSearchConfig, + WebSearchProvidersConfig, + WebSearchConfig, +} from './websearch'; + +// Browser automation types and defaults +export { DEFAULT_BROWSER_CONFIG } from './browser'; +export type { + BrowserToolPolicy, + BrowserEvalMode, + BrowserClaudeConfig, + BrowserCodexConfig, + BrowserConfig, +} from './browser'; + +// Logging and preferences types and defaults +export { DEFAULT_LOGGING_CONFIG } from './logging'; +export type { LoggingLevel, LoggingConfig, PreferencesConfig } from './logging'; + +// Provider integration types and defaults +export { + DEFAULT_GLOBAL_ENV, + DEFAULT_COPILOT_CONFIG, + DEFAULT_CURSOR_CONFIG, + DEFAULT_CLIPROXY_SERVER_CONFIG, + DEFAULT_OPENAI_COMPAT_PROXY_CONFIG, + DEFAULT_IMAGE_ANALYSIS_CONFIG, +} from './providers'; +export type { + CopilotAccountType, + CopilotConfig, + CursorConfig, + ProxyRemoteConfig, + ProxyFallbackConfig, + ProxyLocalConfig, + OpenAICompatProxyRoutingConfig, + OpenAICompatProxyConfig, + CliproxyServerConfig, + GlobalEnvConfig, + ContinuityConfig, + ImageAnalysisConfig, +} from './providers'; + +// Main unified config interface, factory, and type guard +export { createEmptyUnifiedConfig, isUnifiedConfig } from './unified-config'; +export type { UnifiedConfig } from './unified-config'; diff --git a/src/config/schemas/logging.ts b/src/config/schemas/logging.ts new file mode 100644 index 00000000..05fe3061 --- /dev/null +++ b/src/config/schemas/logging.ts @@ -0,0 +1,53 @@ +/** + * Logging and preferences configuration types and defaults. + * + * Covers: + * - LoggingConfig: CCS-owned structured runtime logging + * - LoggingLevel: log severity levels + * - PreferencesConfig: user preferences (theme, telemetry, auto-update) + */ + +export type LoggingLevel = 'error' | 'warn' | 'info' | 'debug'; + +/** + * CCS-owned structured logging configuration. + * Separate from cliproxy.logging, which controls CLIProxy runtime files. + */ +export interface LoggingConfig { + /** Enable CCS-owned structured runtime logging */ + enabled: boolean; + /** Minimum level written to disk */ + level: LoggingLevel; + /** Rotate current log when it reaches this size in MB */ + rotate_mb: number; + /** Keep archived segments for this many days */ + retain_days: number; + /** Redact sensitive values before persistence */ + redact: boolean; + /** In-memory recent event buffer size for dashboard reads */ + live_buffer_size: number; +} + +/** + * Default logging configuration. + */ +export const DEFAULT_LOGGING_CONFIG: LoggingConfig = { + enabled: true, + level: 'info', + rotate_mb: 10, + retain_days: 7, + redact: true, + live_buffer_size: 250, +}; + +/** + * User preferences. + */ +export interface PreferencesConfig { + /** UI theme preference */ + theme?: 'light' | 'dark' | 'system'; + /** Enable anonymous telemetry */ + telemetry?: boolean; + /** Enable automatic update checks */ + auto_update?: boolean; +} diff --git a/src/config/schemas/providers.ts b/src/config/schemas/providers.ts new file mode 100644 index 00000000..78a6dc8f --- /dev/null +++ b/src/config/schemas/providers.ts @@ -0,0 +1,30 @@ +/** + * Provider integration configuration types and defaults. + * + * Re-exports from focused sub-modules for backward compatibility. + * Actual definitions live in: + * - copilot-cursor.ts: CopilotConfig, CursorConfig + defaults + * - proxy-server.ts: CliproxyServerConfig, OpenAICompatProxyConfig, + * GlobalEnvConfig, ContinuityConfig, ImageAnalysisConfig + defaults + */ + +export type { CopilotAccountType, CopilotConfig, CursorConfig } from './copilot-cursor'; +export { DEFAULT_COPILOT_CONFIG, DEFAULT_CURSOR_CONFIG } from './copilot-cursor'; + +export type { + ProxyRemoteConfig, + ProxyFallbackConfig, + ProxyLocalConfig, + OpenAICompatProxyRoutingConfig, + OpenAICompatProxyConfig, + CliproxyServerConfig, + GlobalEnvConfig, + ContinuityConfig, + ImageAnalysisConfig, +} from './proxy-server'; +export { + DEFAULT_CLIPROXY_SERVER_CONFIG, + DEFAULT_OPENAI_COMPAT_PROXY_CONFIG, + DEFAULT_GLOBAL_ENV, + DEFAULT_IMAGE_ANALYSIS_CONFIG, +} from './proxy-server'; diff --git a/src/config/schemas/proxy-server.ts b/src/config/schemas/proxy-server.ts new file mode 100644 index 00000000..745ebab3 --- /dev/null +++ b/src/config/schemas/proxy-server.ts @@ -0,0 +1,193 @@ +/** + * Proxy server, global env, continuity, and image analysis types and defaults. + * + * Covers: + * - CliproxyServerConfig: remote/local CLIProxy server mode + * - OpenAICompatProxyConfig: OpenAI-compatible local proxy + * - GlobalEnvConfig: global environment variable injection + * - ContinuityConfig: cross-profile continuity inheritance + * - ImageAnalysisConfig: vision analysis via CLIProxy + */ + +/** + * Remote proxy configuration. + * Connect to a remote CLIProxyAPI instance instead of spawning local binary. + */ +export interface ProxyRemoteConfig { + /** Enable remote proxy mode (default: false = local mode) */ + enabled: boolean; + /** Remote proxy hostname or IP (empty = not configured) */ + host: string; + /** + * Remote proxy port. + * Optional - defaults based on protocol: + * - HTTPS: 443 + * - HTTP: 8317 + * When empty/undefined, uses protocol default. + */ + port?: number; + /** Protocol for remote connection */ + protocol: 'http' | 'https'; + /** Auth token for remote proxy API endpoints (optional, sent as header) */ + auth_token: string; + /** + * Management key for remote proxy management API endpoints. + * CLIProxyAPI uses separate authentication for management endpoints + * (/v0/management/*) via 'secret-key' config. + * If not set, falls back to auth_token for backwards compatibility. + */ + management_key?: string; + /** Connection timeout in milliseconds (default: 2000) */ + timeout?: number; + /** Enable auto-sync profiles to remote on settings change (default: false) */ + auto_sync?: boolean; +} + +/** + * Fallback configuration when remote proxy is unreachable. + */ +export interface ProxyFallbackConfig { + /** Enable fallback to local proxy (default: true) */ + enabled: boolean; + /** Auto-start local proxy without prompting (default: false = prompt user) */ + auto_start: boolean; +} + +/** + * Local proxy configuration. + */ +export interface ProxyLocalConfig { + /** Local proxy port (default: 8317) */ + port: number; + /** Auto-start local binary (default: true) */ + auto_start: boolean; +} + +export interface OpenAICompatProxyRoutingConfig { + default?: string; + background?: string; + think?: string; + longContext?: string; + webSearch?: string; + longContextThreshold?: number; +} + +export interface OpenAICompatProxyConfig { + /** Default local port for OpenAI-compatible proxy instances */ + port?: number; + /** Optional profile-scoped local port overrides */ + profile_ports?: Record; + routing?: OpenAICompatProxyRoutingConfig; +} + +/** + * CLIProxy server configuration section. + * Controls whether CCS uses local or remote CLIProxyAPI instance. + */ +export interface CliproxyServerConfig { + /** Remote proxy settings */ + remote: ProxyRemoteConfig; + /** Fallback behavior when remote is unreachable */ + fallback: ProxyFallbackConfig; + /** Local proxy settings */ + local: ProxyLocalConfig; +} + +/** + * Global environment variables configuration. + * These env vars are injected into ALL non-Claude subscription profiles. + * Useful for disabling telemetry, bug commands, error reporting, etc. + */ +export interface GlobalEnvConfig { + /** Enable global env injection (default: true) */ + enabled: boolean; + /** Environment variables to inject */ + env: Record; +} + +/** + * Cross-profile continuity inheritance configuration. + * Maps execution profile names to source account profiles for CLAUDE_CONFIG_DIR reuse. + */ +export interface ContinuityConfig { + /** Profile name -> source account profile name */ + inherit_from_account?: Record; +} + +/** + * Default global env vars for third-party profiles. + * These disable Claude Code telemetry/reporting since we're using proxy. + */ +export const DEFAULT_GLOBAL_ENV: Record = { + DISABLE_BUG_COMMAND: '1', + DISABLE_ERROR_REPORTING: '1', + DISABLE_TELEMETRY: '1', +}; + +/** + * Default CLIProxy server configuration. + * Local mode by default - remote must be explicitly enabled. + * Port is optional for remote - defaults based on protocol. + */ +export const DEFAULT_CLIPROXY_SERVER_CONFIG: CliproxyServerConfig = { + remote: { + enabled: false, + host: '', + protocol: 'http', + auth_token: '', + }, + fallback: { + enabled: true, + auto_start: false, + }, + local: { + port: 8317, + auto_start: true, + }, +}; + +export const DEFAULT_OPENAI_COMPAT_PROXY_CONFIG: OpenAICompatProxyConfig = { + profile_ports: {}, + routing: { + longContextThreshold: 60_000, + }, +}; + +/** + * Image analysis configuration. + * Routes image/PDF files through CLIProxy for vision analysis. + */ +export interface ImageAnalysisConfig { + /** Enable image analysis via CLIProxy (default: true) */ + enabled: boolean; + /** Timeout in seconds (default: 60) */ + timeout: number; + /** Provider-to-model mapping for vision analysis */ + provider_models: Record; + /** Fallback backend used when a profile does not resolve to a provider-specific backend */ + fallback_backend?: string; + /** Explicit profile-name-to-backend overrides for settings/custom aliases */ + profile_backends?: Record; +} + +/** + * Default image analysis configuration. + * Enabled by default for CLIProxy providers with vision support. + */ +export const DEFAULT_IMAGE_ANALYSIS_CONFIG: ImageAnalysisConfig = { + enabled: true, + timeout: 60, + provider_models: { + agy: 'gemini-3-1-flash-preview', + gemini: 'gemini-3-flash-preview', + codex: 'gpt-5.1-codex-mini', + kiro: 'kiro-claude-haiku-4-5', + ghcp: 'claude-haiku-4.5', + claude: 'claude-haiku-4.5-20251001', + qwen: 'vision-model', + iflow: 'qwen3-vl-plus', + kimi: 'vision-model', + }, + fallback_backend: 'gemini', + profile_backends: {}, +}; diff --git a/src/config/schemas/quota.ts b/src/config/schemas/quota.ts new file mode 100644 index 00000000..0d380694 --- /dev/null +++ b/src/config/schemas/quota.ts @@ -0,0 +1,121 @@ +/** + * Quota management configuration types and defaults. + * + * Controls hybrid auto+manual account selection for multi-account setups. + * Version 7+ feature. + */ + +// ============================================================================ +// QUOTA MANAGEMENT CONFIGURATION (v7+) +// ============================================================================ + +/** + * Auto quota management configuration. + * Controls automatic failover behavior. + */ +export interface AutoQuotaConfig { + /** Enable pre-flight quota check before requests (default: true) */ + preflight_check: boolean; + /** Quota percentage below which account is "exhausted" (default: 5) */ + exhaustion_threshold: number; + /** Tier priority for failover, highest to lowest (default: ['paid']) */ + tier_priority: string[]; + /** Minutes to skip exhausted account before retry (default: 5) */ + cooldown_minutes: number; +} + +/** + * Runtime quota monitor configuration. + * Controls adaptive polling during active sessions. + */ +export interface RuntimeMonitorConfig { + /** Enable runtime monitoring during sessions (default: true) */ + enabled: boolean; + /** Poll interval in seconds when quota > warn_threshold (default: 300) */ + normal_interval_seconds: number; + /** Poll interval in seconds when quota <= warn_threshold (default: 60) */ + critical_interval_seconds: number; + /** Quota percentage that triggers fast polling + warning (default: 20) */ + warn_threshold: number; + /** Quota percentage that triggers cooldown + switch (default: 5) */ + exhaustion_threshold: number; + /** Minutes to cooldown exhausted account (default: 5) */ + cooldown_minutes: number; +} + +/** + * Manual quota management configuration. + * User-controlled overrides for account selection. + */ +export interface ManualQuotaConfig { + /** User-paused accounts (stored in accounts.json) */ + paused_accounts: string[]; + /** Force use of specific account (overrides auto-selection) */ + forced_default: string | null; + /** Lock to specific tier only */ + tier_lock: string | null; +} + +/** + * Quota management mode. + * - auto: Fully automatic failover based on quota + * - manual: User controls everything, no auto-switching + * - hybrid: Auto-failover with user overrides (default) + */ +export type QuotaManagementMode = 'auto' | 'manual' | 'hybrid'; + +/** + * Quota management configuration section. + * Controls hybrid auto+manual account selection for multi-account setups. + */ +export interface QuotaManagementConfig { + /** Management mode (default: hybrid) */ + mode: QuotaManagementMode; + /** Auto mode settings */ + auto: AutoQuotaConfig; + /** Manual mode settings */ + manual: ManualQuotaConfig; + /** Runtime monitor settings */ + runtime_monitor: RuntimeMonitorConfig; +} + +/** + * Default auto quota configuration. + */ +export const DEFAULT_AUTO_QUOTA_CONFIG: AutoQuotaConfig = { + preflight_check: true, + exhaustion_threshold: 5, + tier_priority: ['ultra', 'pro', 'free'], + cooldown_minutes: 5, +}; + +/** + * Default manual quota configuration. + */ +export const DEFAULT_MANUAL_QUOTA_CONFIG: ManualQuotaConfig = { + paused_accounts: [], + forced_default: null, + tier_lock: null, +}; + +/** + * Default runtime monitor configuration. + */ +export const DEFAULT_RUNTIME_MONITOR_CONFIG: RuntimeMonitorConfig = { + enabled: true, + normal_interval_seconds: 300, + critical_interval_seconds: 60, + warn_threshold: 20, + exhaustion_threshold: 5, + cooldown_minutes: 5, +}; + +/** + * Default quota management configuration. + */ +export const DEFAULT_QUOTA_MANAGEMENT_CONFIG: QuotaManagementConfig = { + mode: 'hybrid', + auto: { ...DEFAULT_AUTO_QUOTA_CONFIG }, + manual: { ...DEFAULT_MANUAL_QUOTA_CONFIG }, + runtime_monitor: { ...DEFAULT_RUNTIME_MONITOR_CONFIG }, +}; diff --git a/src/config/schemas/thinking.ts b/src/config/schemas/thinking.ts new file mode 100644 index 00000000..81951ad2 --- /dev/null +++ b/src/config/schemas/thinking.ts @@ -0,0 +1,66 @@ +/** + * Thinking/reasoning budget configuration types and defaults. + * + * Controls thinking budget injection for CLIProxy providers. + * Version 8+ feature. + */ + +// ============================================================================ +// THINKING CONFIGURATION (v8+) +// ============================================================================ + +/** + * Thinking mode for auto/manual/off control. + * - auto: Apply tier-based defaults (opus→high, sonnet→medium, haiku→low) + * - off: Disable thinking entirely + * - manual: Use explicit override value + */ +export type ThinkingMode = 'auto' | 'off' | 'manual'; + +/** + * Tier-to-thinking level defaults. + * Maps Claude tier names to thinking level names. + */ +export interface ThinkingTierDefaults { + /** Thinking level for opus tier (default: 'high') */ + opus: string; + /** Thinking level for sonnet tier (default: 'medium') */ + sonnet: string; + /** Thinking level for haiku tier (default: 'low') */ + haiku: string; +} + +/** + * Thinking configuration section. + * Controls thinking/reasoning budget injection for CLIProxy providers. + */ +export interface ThinkingConfig { + /** Thinking mode (default: 'auto') */ + mode: ThinkingMode; + /** Manual override value (level name or budget number) */ + override?: string | number; + /** Tier-to-level mapping */ + tier_defaults: ThinkingTierDefaults; + /** Per-provider overrides (e.g., { gemini: { opus: 'high' } }) */ + provider_overrides?: Record>; + /** Show warning when values are clamped (default: true) */ + show_warnings?: boolean; +} + +/** + * Default thinking tier defaults. + */ +export const DEFAULT_THINKING_TIER_DEFAULTS: ThinkingTierDefaults = { + opus: 'high', + sonnet: 'medium', + haiku: 'low', +}; + +/** + * Default thinking configuration. + */ +export const DEFAULT_THINKING_CONFIG: ThinkingConfig = { + mode: 'auto', + tier_defaults: { ...DEFAULT_THINKING_TIER_DEFAULTS }, + show_warnings: true, +}; diff --git a/src/config/schemas/unified-config.ts b/src/config/schemas/unified-config.ts new file mode 100644 index 00000000..78726c46 --- /dev/null +++ b/src/config/schemas/unified-config.ts @@ -0,0 +1,200 @@ +/** + * Main unified configuration interface, factory, and type guard. + * + * The UnifiedConfig type is the root of the entire config.yaml schema. + * This file imports all section types from their respective schema modules. + */ + +import type { AccountConfig, ProfileConfig, DashboardAuthConfig } from './auth'; +import { DEFAULT_DASHBOARD_AUTH_CONFIG } from './auth'; +import type { CLIProxyConfig } from './cliproxy'; +import { CLIPROXY_SUPPORTED_PROVIDERS, DEFAULT_CLIPROXY_SAFETY_CONFIG } from './cliproxy'; +import type { LoggingConfig, PreferencesConfig } from './logging'; +import { DEFAULT_LOGGING_CONFIG } from './logging'; +import type { WebSearchConfig } from './websearch'; +import type { + GlobalEnvConfig, + ContinuityConfig, + CopilotConfig, + CursorConfig, + CliproxyServerConfig, + OpenAICompatProxyConfig, + ImageAnalysisConfig, +} from './providers'; +import { + DEFAULT_COPILOT_CONFIG, + DEFAULT_CURSOR_CONFIG, + DEFAULT_CLIPROXY_SERVER_CONFIG, + DEFAULT_OPENAI_COMPAT_PROXY_CONFIG, + DEFAULT_IMAGE_ANALYSIS_CONFIG, + DEFAULT_GLOBAL_ENV, +} from './providers'; +import { UNIFIED_CONFIG_VERSION } from './version'; +import type { QuotaManagementConfig } from './quota'; +import { DEFAULT_QUOTA_MANAGEMENT_CONFIG } from './quota'; +import type { ThinkingConfig } from './thinking'; +import { DEFAULT_THINKING_CONFIG } from './thinking'; +import type { OfficialChannelsConfig } from './channels'; +import { DEFAULT_OFFICIAL_CHANNELS_CONFIG } from './channels'; +import type { BrowserConfig } from './browser'; +import { DEFAULT_BROWSER_CONFIG } from './browser'; + +/** + * Main unified configuration structure. + * Stored in ~/.ccs/config.yaml + */ +export interface UnifiedConfig { + /** Config version */ + version: number; + /** Flag indicating setup wizard has been completed */ + setup_completed?: boolean; + /** Default profile name to use when none specified */ + default?: string; + /** Account-based profiles (isolated Claude instances) */ + accounts: Record; + /** API-based profiles (env var injection) */ + profiles: Record; + /** CLIProxy configuration */ + cliproxy: CLIProxyConfig; + /** OpenAI-compatible local proxy configuration */ + proxy?: OpenAICompatProxyConfig; + /** CCS-owned structured logging configuration */ + logging?: LoggingConfig; + /** User preferences */ + preferences: PreferencesConfig; + /** WebSearch configuration */ + websearch?: WebSearchConfig; + /** Global environment variables for all non-Claude subscription profiles */ + global_env?: GlobalEnvConfig; + /** Cross-profile continuity inheritance mapping */ + continuity?: ContinuityConfig; + /** Copilot API configuration (GitHub Copilot proxy) */ + copilot?: CopilotConfig; + /** Cursor IDE configuration (Cursor proxy daemon) */ + cursor?: CursorConfig; + /** CLIProxy server configuration for remote/local mode */ + cliproxy_server?: CliproxyServerConfig; + /** Quota management configuration (v7+) */ + quota_management?: QuotaManagementConfig; + /** Thinking/reasoning budget configuration (v8+) */ + thinking?: ThinkingConfig; + /** Official Channels runtime auto-enable preferences (v11+) */ + channels?: OfficialChannelsConfig; + /** Dashboard authentication configuration (optional) */ + dashboard_auth?: DashboardAuthConfig; + /** Browser automation configuration */ + browser?: BrowserConfig; + /** Image analysis configuration (vision via CLIProxy) */ + image_analysis?: ImageAnalysisConfig; +} + +/** + * Create an empty unified config with defaults. + */ +export function createEmptyUnifiedConfig(): UnifiedConfig { + return { + version: UNIFIED_CONFIG_VERSION, + default: undefined, + accounts: {}, + profiles: {}, + cliproxy: { + backend: 'original', + oauth_accounts: {}, + providers: [...CLIPROXY_SUPPORTED_PROVIDERS], + variants: {}, + logging: { + enabled: false, + request_log: false, + }, + safety: { ...DEFAULT_CLIPROXY_SAFETY_CONFIG }, + auto_sync: true, + routing: { + strategy: 'round-robin', + session_affinity: false, + session_affinity_ttl: '1h', + }, + }, + proxy: { + port: DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.port, + profile_ports: { ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.profile_ports }, + routing: { + ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.routing, + }, + }, + logging: { ...DEFAULT_LOGGING_CONFIG }, + preferences: { + theme: 'system', + telemetry: false, + auto_update: true, + }, + websearch: { + enabled: true, + providers: { + exa: { + enabled: false, + max_results: 5, + }, + tavily: { + enabled: false, + max_results: 5, + }, + brave: { + enabled: false, + max_results: 5, + }, + searxng: { + enabled: false, + url: '', + max_results: 5, + }, + duckduckgo: { + enabled: true, + max_results: 5, + }, + gemini: { + enabled: false, + model: 'gemini-2.5-flash', + timeout: 55, + }, + opencode: { + enabled: false, + model: 'opencode/grok-code', + timeout: 90, + }, + grok: { + enabled: false, + timeout: 55, + }, + }, + }, + global_env: { + enabled: true, + env: { ...DEFAULT_GLOBAL_ENV }, + }, + copilot: { ...DEFAULT_COPILOT_CONFIG }, + cursor: { ...DEFAULT_CURSOR_CONFIG }, + cliproxy_server: { ...DEFAULT_CLIPROXY_SERVER_CONFIG }, + quota_management: { ...DEFAULT_QUOTA_MANAGEMENT_CONFIG }, + thinking: { ...DEFAULT_THINKING_CONFIG }, + channels: { ...DEFAULT_OFFICIAL_CHANNELS_CONFIG }, + dashboard_auth: { ...DEFAULT_DASHBOARD_AUTH_CONFIG }, + browser: { + claude: { ...DEFAULT_BROWSER_CONFIG.claude }, + codex: { ...DEFAULT_BROWSER_CONFIG.codex }, + }, + image_analysis: { ...DEFAULT_IMAGE_ANALYSIS_CONFIG }, + }; +} + +/** + * Type guard for UnifiedConfig. + * Relaxed validation: accepts configs with version >= 1 and any subset of sections. + * Missing sections will be filled with defaults during merge. + */ +export function isUnifiedConfig(obj: unknown): obj is UnifiedConfig { + if (typeof obj !== 'object' || obj === null) return false; + const config = obj as Record; + // Only require version to be a number >= 1 (allow future versions) + // Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig + return typeof config.version === 'number' && config.version >= 1; +} diff --git a/src/config/schemas/version.ts b/src/config/schemas/version.ts new file mode 100644 index 00000000..98979314 --- /dev/null +++ b/src/config/schemas/version.ts @@ -0,0 +1,23 @@ +/** + * Unified config version constant. + * + * Central source of truth for the current config schema version. + * Incremented whenever new sections are added to config.yaml. + */ + +/** + * Unified config version. + * Version 2 = YAML unified format + * Version 3 = WebSearch config with model configuration for Gemini/OpenCode + * Version 4 = Copilot API integration (GitHub Copilot proxy) + * Version 5 = Remote proxy configuration (connect to remote CLIProxyAPI) + * Version 6 = Customizable auth tokens (API key and management secret) + * Version 7 = Quota management for hybrid auto+manual account control + * Version 8 = Thinking/reasoning budget configuration + * Version 9 = Real WebSearch backends (DuckDuckGo/Brave) with legacy CLI fallback + * Version 10 = Exa + Tavily WebSearch backends + * Version 11 = Discord Channels runtime auto-enable preferences + * Version 12 = Official Channels multi-provider support (Telegram, Discord, iMessage) + * Version 13 = Browser automation defaults to safe manual/off exposure + */ +export const UNIFIED_CONFIG_VERSION = 13; diff --git a/src/config/schemas/websearch.ts b/src/config/schemas/websearch.ts new file mode 100644 index 00000000..d2714c41 --- /dev/null +++ b/src/config/schemas/websearch.ts @@ -0,0 +1,148 @@ +/** + * WebSearch backend configuration types. + * + * Covers all supported search backends: + * - API-backed: Exa, Tavily, Brave + * - Self-hosted: SearXNG + * - Zero-setup: DuckDuckGo + * - Legacy CLI fallbacks: Gemini, Grok, OpenCode + */ + +/** + * DuckDuckGo WebSearch configuration. + */ +export interface DuckDuckGoWebSearchConfig { + /** Enable DuckDuckGo HTML search fallback (default: true) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Brave WebSearch configuration. + */ +export interface BraveWebSearchConfig { + /** Enable Brave Search when BRAVE_API_KEY is available (default: false) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Exa WebSearch configuration. + */ +export interface ExaWebSearchConfig { + /** Enable Exa Search when EXA_API_KEY is available (default: false) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Tavily WebSearch configuration. + */ +export interface TavilyWebSearchConfig { + /** Enable Tavily Search when TAVILY_API_KEY is available (default: false) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * SearXNG WebSearch configuration. + */ +export interface SearxngWebSearchConfig { + /** Enable SearXNG JSON search backend (default: false) */ + enabled?: boolean; + /** Base SearXNG URL, e.g. https://search.example.com (default: '') */ + url?: string; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Gemini CLI WebSearch configuration. + */ +export interface GeminiWebSearchConfig { + /** Enable Gemini CLI legacy fallback (default: false) */ + enabled?: boolean; + /** Model to use (default: gemini-2.5-flash) */ + model?: string; + /** Timeout in seconds (default: 55) */ + timeout?: number; +} + +/** + * Grok CLI WebSearch configuration. + */ +export interface GrokWebSearchConfig { + /** Enable Grok CLI legacy fallback (default: false - requires GROK_API_KEY) */ + enabled?: boolean; + /** Timeout in seconds (default: 55) */ + timeout?: number; +} + +/** + * OpenCode CLI WebSearch configuration. + */ +export interface OpenCodeWebSearchConfig { + /** Enable OpenCode CLI legacy fallback (default: false) */ + enabled?: boolean; + /** Model to use (default: opencode/grok-code) */ + model?: string; + /** Timeout in seconds (default: 60) */ + timeout?: number; +} + +/** + * WebSearch providers configuration. + * Uses deterministic search backends first, with optional legacy CLI fallback. + */ +export interface WebSearchProvidersConfig { + /** Exa Search API - API-backed search with strong relevance and content extraction */ + exa?: ExaWebSearchConfig; + /** Tavily Search API - API-backed search optimized for agent/tool usage */ + tavily?: TavilyWebSearchConfig; + /** Brave Search API - higher quality results when BRAVE_API_KEY is set */ + brave?: BraveWebSearchConfig; + /** SearXNG JSON search - self-hosted or public instance backend */ + searxng?: SearxngWebSearchConfig; + /** DuckDuckGo HTML search - zero setup default backend */ + duckduckgo?: DuckDuckGoWebSearchConfig; + /** Gemini CLI - optional legacy LLM fallback */ + gemini?: GeminiWebSearchConfig; + /** Grok CLI - optional legacy LLM fallback */ + grok?: GrokWebSearchConfig; + /** OpenCode - optional legacy LLM fallback */ + opencode?: OpenCodeWebSearchConfig; +} + +/** + * WebSearch configuration. + * Uses deterministic local backends for third-party profiles. + * Legacy AI CLI fallbacks remain available for compatibility only. + */ +export interface WebSearchConfig { + /** Master switch - enable/disable WebSearch (default: true) */ + enabled?: boolean; + /** Individual provider configurations */ + providers?: WebSearchProvidersConfig; + // Legacy fields (deprecated, kept for backwards compatibility) + /** @deprecated Use providers.gemini instead */ + gemini?: { + enabled?: boolean; + timeout?: number; + }; + /** @deprecated Unused */ + mode?: 'sequential' | 'parallel'; + /** @deprecated Unused */ + provider?: 'auto' | 'web-search-prime' | 'brave' | 'tavily'; + /** @deprecated Unused */ + fallback?: boolean; + /** @deprecated Unused */ + webSearchPrimeUrl?: string; + /** @deprecated Unused */ + selectedProviders?: string[]; + /** @deprecated Unused */ + customMcp?: unknown[]; +} diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index ab4eca6c..ec5eef2f 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -7,1122 +7,8 @@ * - *.settings.json (env vars) * * Into a single config.yaml structure. - */ - -import type { TargetType } from '../targets/target-adapter'; -import type { CLIProxyProvider, CliproxyRoutingStrategy } from '../cliproxy/types'; -import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities'; - -/** - * Unified config version. - * Version 2 = YAML unified format - * Version 3 = WebSearch config with model configuration for Gemini/OpenCode - * Version 4 = Copilot API integration (GitHub Copilot proxy) - * Version 5 = Remote proxy configuration (connect to remote CLIProxyAPI) - * Version 6 = Customizable auth tokens (API key and management secret) - * Version 7 = Quota management for hybrid auto+manual account control - * Version 8 = Thinking/reasoning budget configuration - * Version 9 = Real WebSearch backends (DuckDuckGo/Brave) with legacy CLI fallback - * Version 10 = Exa + Tavily WebSearch backends - * Version 11 = Discord Channels runtime auto-enable preferences - * Version 12 = Official Channels multi-provider support (Telegram, Discord, iMessage) - * Version 13 = Browser automation defaults to safe manual/off exposure - */ -export const UNIFIED_CONFIG_VERSION = 13; - -/** - * Supported CLIProxy providers. - * Derived from CLIPROXY_PROVIDER_IDS — single source of truth in provider-capabilities.ts. - */ -export const CLIPROXY_SUPPORTED_PROVIDERS = CLIPROXY_PROVIDER_IDS; - -/** - * Account configuration (formerly in profiles.json). - * Represents an isolated Claude instance via CLAUDE_CONFIG_DIR. - */ -export interface AccountConfig { - /** ISO timestamp when account was created */ - created: string; - /** ISO timestamp of last usage, null if never used */ - last_used: string | null; - /** Context mode for project workspace data */ - context_mode?: 'isolated' | 'shared'; - /** Context-sharing group when context_mode='shared' */ - context_group?: string; - /** Shared continuity depth when context_mode='shared' */ - continuity_mode?: 'standard' | 'deeper'; - /** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */ - bare?: boolean; -} - -/** - * API-based profile configuration. - * Injects environment variables for alternative providers (GLM, Kimi, etc.). * - * Settings are stored in separate *.settings.json files (matching Claude's pattern) - * to allow users to edit them directly without touching config.yaml. + * Types have been reorganized into src/config/schemas/ for maintainability. + * This file re-exports everything for backward compatibility. */ -export interface ProfileConfig { - /** Profile type - currently only 'api' */ - type: 'api'; - /** Path to settings file (e.g., "~/.ccs/glm.settings.json") */ - settings: string; - /** Target CLI to use for this profile (default: 'claude') */ - target?: TargetType; -} - -/** - * CLIProxy OAuth account nickname mapping. - * Maps user-friendly nicknames to email addresses. - */ -export type OAuthAccounts = Record; - -/** - * CLIProxy variant configuration. - * User-defined variants of built-in OAuth providers. - * - * Settings are stored in separate *.settings.json files (matching Claude's pattern) - * to allow users to edit them directly without touching config.yaml. - */ -export interface CLIProxyVariantConfig { - /** Base provider to use */ - provider: CLIProxyProvider; - /** Account nickname (references oauth_accounts) */ - account?: string; - /** Path to settings file (e.g., "~/.ccs/gemini-custom.settings.json") */ - settings?: string; - /** Unique port for variant isolation (8318-8417) */ - port?: number; - /** Per-variant auth override (optional) */ - auth?: CLIProxyAuthConfig; - /** Target CLI to use for this variant (default: 'claude') */ - target?: TargetType; -} - -/** - * Per-tier provider+model mapping for composite variants. - */ -export interface CompositeTierConfig { - /** Provider for this tier */ - provider: CLIProxyProvider; - /** Model ID to use for this tier */ - model: string; - /** Account nickname (optional, references oauth_accounts) */ - account?: string; - /** Fallback provider+model if primary fails */ - fallback?: { - provider: CLIProxyProvider; - model: string; - account?: string; - }; - /** Per-tier thinking budget override (e.g. 'xhigh', 'medium', 'off') */ - thinking?: string; -} - -/** - * Composite variant configuration. - * Mixes different providers per Claude tier (opus, sonnet, haiku) in a single profile. - * Uses CLIProxyAPI root endpoints (/v1/messages) for model-based routing - * instead of provider-specific endpoints (/api/provider/{provider}). - */ -export interface CompositeVariantConfig { - /** Discriminator for composite type */ - type: 'composite'; - /** Which tier ANTHROPIC_MODEL equals (default must be one of the three) */ - default_tier: 'opus' | 'sonnet' | 'haiku'; - /** Per-tier provider+model mapping */ - tiers: { - opus: CompositeTierConfig; - sonnet: CompositeTierConfig; - haiku: CompositeTierConfig; - }; - /** Path to settings file */ - settings?: string; - /** Shared port for the composite profile */ - port?: number; - /** Per-variant auth override (optional) */ - auth?: CLIProxyAuthConfig; - /** Target CLI to use for this composite variant (default: 'claude') */ - target?: TargetType; -} - -/** - * CLIProxy authentication configuration. - * Allows customization of API key and management secret for CLIProxyAPI. - */ -export interface CLIProxyAuthConfig { - /** API key for CCS-managed requests (default: 'ccs-internal-managed') */ - api_key?: string; - /** Management secret for Control Panel login (default: 'ccs') */ - management_secret?: string; -} - -/** - * CLIProxy logging configuration. - * Controls whether CLIProxyAPI writes logs to disk. - * Logs can grow to several GB if left enabled. - */ -export interface CLIProxyLoggingConfig { - /** Enable logging to file (default: false to prevent disk bloat) */ - enabled?: boolean; - /** Enable request logging for debugging (default: false) */ - request_log?: boolean; -} - -/** - * CLIProxy safety configuration. - * Controls high-risk flow safeguards for supported providers. - */ -export interface CLIProxySafetyConfig { - /** Allow skipping AGY responsibility checks and Gemini dashboard typed acknowledgement */ - antigravity_ack_bypass?: boolean; -} - -/** - * Default CLIProxy safety configuration. - */ -export const DEFAULT_CLIPROXY_SAFETY_CONFIG: CLIProxySafetyConfig = { - antigravity_ack_bypass: false, -}; - -/** - * Token refresh configuration. - * Manages background token refresh worker settings. - */ -export interface TokenRefreshSettings { - /** Enable background token refresh (default: false) */ - enabled?: boolean; - /** Refresh check interval in minutes (default: 30) */ - interval_minutes?: number; - /** Preemptive refresh time in minutes (default: 45) */ - preemptive_minutes?: number; - /** Maximum retry attempts per token (default: 3) */ - max_retries?: number; - /** Enable verbose logging (default: false) */ - verbose?: boolean; -} - -export interface CLIProxyRoutingConfig { - /** Credential selection strategy when multiple accounts match */ - strategy?: CliproxyRoutingStrategy; - /** Keep one conversation pinned to the same account when possible */ - session_affinity?: boolean; - /** Go-style duration for session-affinity binding retention */ - session_affinity_ttl?: string; -} - -/** - * CLIProxy configuration section. - */ -export interface CLIProxyConfig { - /** Backend selection: 'original' or 'plus' (default: 'original') */ - backend?: 'original' | 'plus'; - /** Nickname to email mapping for OAuth accounts */ - oauth_accounts: OAuthAccounts; - /** Built-in providers (read-only, for reference) */ - providers: readonly string[]; - /** User-defined provider variants (single-provider or composite) */ - variants: Record; - /** Logging configuration (disabled by default) */ - logging?: CLIProxyLoggingConfig; - /** Safety controls for high-risk provider flows */ - safety?: CLIProxySafetyConfig; - /** Kiro: disable incognito browser mode (use normal browser to save credentials) */ - kiro_no_incognito?: boolean; - /** Global auth configuration for CLIProxyAPI */ - auth?: CLIProxyAuthConfig; - /** Background token refresh worker settings */ - token_refresh?: TokenRefreshSettings; - /** Auto-sync API profiles to local CLIProxy config on settings change (default: true) */ - auto_sync?: boolean; - /** Routing strategy for multi-account CLIProxy selection */ - routing?: CLIProxyRoutingConfig; -} - -export type LoggingLevel = 'error' | 'warn' | 'info' | 'debug'; - -/** - * CCS-owned structured logging configuration. - * Separate from cliproxy.logging, which controls CLIProxy runtime files. - */ -export interface LoggingConfig { - /** Enable CCS-owned structured runtime logging */ - enabled: boolean; - /** Minimum level written to disk */ - level: LoggingLevel; - /** Rotate current log when it reaches this size in MB */ - rotate_mb: number; - /** Keep archived segments for this many days */ - retain_days: number; - /** Redact sensitive values before persistence */ - redact: boolean; - /** In-memory recent event buffer size for dashboard reads */ - live_buffer_size: number; -} - -export const DEFAULT_LOGGING_CONFIG: LoggingConfig = { - enabled: true, - level: 'info', - rotate_mb: 10, - retain_days: 7, - redact: true, - live_buffer_size: 250, -}; - -/** - * User preferences. - */ -export interface PreferencesConfig { - /** UI theme preference */ - theme?: 'light' | 'dark' | 'system'; - /** Enable anonymous telemetry */ - telemetry?: boolean; - /** Enable automatic update checks */ - auto_update?: boolean; -} - -/** - * DuckDuckGo WebSearch configuration. - */ -export interface DuckDuckGoWebSearchConfig { - /** Enable DuckDuckGo HTML search fallback (default: true) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Brave WebSearch configuration. - */ -export interface BraveWebSearchConfig { - /** Enable Brave Search when BRAVE_API_KEY is available (default: false) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Exa WebSearch configuration. - */ -export interface ExaWebSearchConfig { - /** Enable Exa Search when EXA_API_KEY is available (default: false) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Tavily WebSearch configuration. - */ -export interface TavilyWebSearchConfig { - /** Enable Tavily Search when TAVILY_API_KEY is available (default: false) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * SearXNG WebSearch configuration. - */ -export interface SearxngWebSearchConfig { - /** Enable SearXNG JSON search backend (default: false) */ - enabled?: boolean; - /** Base SearXNG URL, e.g. https://search.example.com (default: '') */ - url?: string; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Gemini CLI WebSearch configuration. - */ -export interface GeminiWebSearchConfig { - /** Enable Gemini CLI legacy fallback (default: false) */ - enabled?: boolean; - /** Model to use (default: gemini-2.5-flash) */ - model?: string; - /** Timeout in seconds (default: 55) */ - timeout?: number; -} - -/** - * Grok CLI WebSearch configuration. - */ -export interface GrokWebSearchConfig { - /** Enable Grok CLI legacy fallback (default: false - requires GROK_API_KEY) */ - enabled?: boolean; - /** Timeout in seconds (default: 55) */ - timeout?: number; -} - -/** - * OpenCode CLI WebSearch configuration. - */ -export interface OpenCodeWebSearchConfig { - /** Enable OpenCode CLI legacy fallback (default: false) */ - enabled?: boolean; - /** Model to use (default: opencode/grok-code) */ - model?: string; - /** Timeout in seconds (default: 60) */ - timeout?: number; -} - -/** - * WebSearch providers configuration. - * Uses deterministic search backends first, with optional legacy CLI fallback. - */ -export interface WebSearchProvidersConfig { - /** Exa Search API - API-backed search with strong relevance and content extraction */ - exa?: ExaWebSearchConfig; - /** Tavily Search API - API-backed search optimized for agent/tool usage */ - tavily?: TavilyWebSearchConfig; - /** Brave Search API - higher quality results when BRAVE_API_KEY is set */ - brave?: BraveWebSearchConfig; - /** SearXNG JSON search - self-hosted or public instance backend */ - searxng?: SearxngWebSearchConfig; - /** DuckDuckGo HTML search - zero setup default backend */ - duckduckgo?: DuckDuckGoWebSearchConfig; - /** Gemini CLI - optional legacy LLM fallback */ - gemini?: GeminiWebSearchConfig; - /** Grok CLI - optional legacy LLM fallback */ - grok?: GrokWebSearchConfig; - /** OpenCode - optional legacy LLM fallback */ - opencode?: OpenCodeWebSearchConfig; -} - -/** - * Copilot API account type. - */ -export type CopilotAccountType = 'individual' | 'business' | 'enterprise'; - -/** - * Copilot API configuration. - * Enables GitHub Copilot subscription usage via copilot-api proxy. - * Strictly opt-in - disabled by default. - * - * !! DISCLAIMER - USE AT YOUR OWN RISK !! - * This uses an UNOFFICIAL reverse-engineered API. - * Excessive usage may trigger GitHub account restrictions. - * CCS provides NO WARRANTY and accepts NO RESPONSIBILITY for any consequences. - */ -export interface CopilotConfig { - /** Enable Copilot integration (default: false) - must be explicitly enabled */ - enabled: boolean; - /** Auto-start copilot-api daemon when using profile (default: false) */ - auto_start: boolean; - /** Port for copilot-api proxy (default: 4141) */ - port: number; - /** GitHub Copilot account type (default: individual) */ - account_type: CopilotAccountType; - /** Rate limit in seconds between requests (null = no limit) */ - rate_limit: number | null; - /** Wait instead of error when rate limit is hit (default: true) */ - wait_on_limit: boolean; - /** Default model ID (e.g., claude-sonnet-4.5) */ - model: string; - /** Model mapping for Claude tiers - maps opus/sonnet/haiku to specific models */ - opus_model?: string; - sonnet_model?: string; - haiku_model?: string; -} - -/** - * Cursor IDE integration configuration. - * Enables Cursor IDE usage via cursor proxy daemon. - */ -export interface CursorConfig { - /** Enable Cursor integration (default: false) */ - enabled: boolean; - /** Port for cursor proxy daemon (default: 20129) */ - port: number; - /** Auto-start daemon when CCS starts (default: false) */ - auto_start: boolean; - /** Enable ghost mode to disable telemetry (default: true) */ - ghost_mode: boolean; - /** Default model ID used by Cursor integration */ - model: string; - /** Optional tier mapping for Claude-compatible model routing */ - opus_model?: string; - /** Optional tier mapping for Claude-compatible model routing */ - sonnet_model?: string; - /** Optional tier mapping for Claude-compatible model routing */ - haiku_model?: string; -} - -/** - * Remote proxy configuration. - * Connect to a remote CLIProxyAPI instance instead of spawning local binary. - */ -export interface ProxyRemoteConfig { - /** Enable remote proxy mode (default: false = local mode) */ - enabled: boolean; - /** Remote proxy hostname or IP (empty = not configured) */ - host: string; - /** - * Remote proxy port. - * Optional - defaults based on protocol: - * - HTTPS: 443 - * - HTTP: 8317 - * When empty/undefined, uses protocol default. - */ - port?: number; - /** Protocol for remote connection */ - protocol: 'http' | 'https'; - /** Auth token for remote proxy API endpoints (optional, sent as header) */ - auth_token: string; - /** - * Management key for remote proxy management API endpoints. - * CLIProxyAPI uses separate authentication for management endpoints - * (/v0/management/*) via 'secret-key' config. - * If not set, falls back to auth_token for backwards compatibility. - */ - management_key?: string; - /** Connection timeout in milliseconds (default: 2000) */ - timeout?: number; - /** Enable auto-sync profiles to remote on settings change (default: false) */ - auto_sync?: boolean; -} - -/** - * Fallback configuration when remote proxy is unreachable. - */ -export interface ProxyFallbackConfig { - /** Enable fallback to local proxy (default: true) */ - enabled: boolean; - /** Auto-start local proxy without prompting (default: false = prompt user) */ - auto_start: boolean; -} - -/** - * Local proxy configuration. - */ -export interface ProxyLocalConfig { - /** Local proxy port (default: 8317) */ - port: number; - /** Auto-start local binary (default: true) */ - auto_start: boolean; -} - -export interface OpenAICompatProxyRoutingConfig { - default?: string; - background?: string; - think?: string; - longContext?: string; - webSearch?: string; - longContextThreshold?: number; -} - -export interface OpenAICompatProxyConfig { - /** Default local port for OpenAI-compatible proxy instances */ - port?: number; - /** Optional profile-scoped local port overrides */ - profile_ports?: Record; - routing?: OpenAICompatProxyRoutingConfig; -} - -/** - * CLIProxy server configuration section. - * Controls whether CCS uses local or remote CLIProxyAPI instance. - */ -export interface CliproxyServerConfig { - /** Remote proxy settings */ - remote: ProxyRemoteConfig; - /** Fallback behavior when remote is unreachable */ - fallback: ProxyFallbackConfig; - /** Local proxy settings */ - local: ProxyLocalConfig; -} - -/** - * Global environment variables configuration. - * These env vars are injected into ALL non-Claude subscription profiles. - * Useful for disabling telemetry, bug commands, error reporting, etc. - */ -export interface GlobalEnvConfig { - /** Enable global env injection (default: true) */ - enabled: boolean; - /** Environment variables to inject */ - env: Record; -} - -/** - * Cross-profile continuity inheritance configuration. - * Maps execution profile names to source account profiles for CLAUDE_CONFIG_DIR reuse. - */ -export interface ContinuityConfig { - /** Profile name -> source account profile name */ - inherit_from_account?: Record; -} - -/** - * Default global env vars for third-party profiles. - * These disable Claude Code telemetry/reporting since we're using proxy. - */ -export const DEFAULT_GLOBAL_ENV: Record = { - DISABLE_BUG_COMMAND: '1', - DISABLE_ERROR_REPORTING: '1', - DISABLE_TELEMETRY: '1', -}; - -/** - * WebSearch configuration. - * Uses deterministic local backends for third-party profiles. - * Legacy AI CLI fallbacks remain available for compatibility only. - */ -export interface WebSearchConfig { - /** Master switch - enable/disable WebSearch (default: true) */ - enabled?: boolean; - /** Individual provider configurations */ - providers?: WebSearchProvidersConfig; - // Legacy fields (deprecated, kept for backwards compatibility) - /** @deprecated Use providers.gemini instead */ - gemini?: { - enabled?: boolean; - timeout?: number; - }; - /** @deprecated Unused */ - mode?: 'sequential' | 'parallel'; - /** @deprecated Unused */ - provider?: 'auto' | 'web-search-prime' | 'brave' | 'tavily'; - /** @deprecated Unused */ - fallback?: boolean; - /** @deprecated Unused */ - webSearchPrimeUrl?: string; - /** @deprecated Unused */ - selectedProviders?: string[]; - /** @deprecated Unused */ - customMcp?: unknown[]; -} - -// ============================================================================ -// QUOTA MANAGEMENT CONFIGURATION (v7+) -// ============================================================================ - -/** - * Auto quota management configuration. - * Controls automatic failover behavior. - */ -export interface AutoQuotaConfig { - /** Enable pre-flight quota check before requests (default: true) */ - preflight_check: boolean; - /** Quota percentage below which account is "exhausted" (default: 5) */ - exhaustion_threshold: number; - /** Tier priority for failover, highest to lowest (default: ['paid']) */ - tier_priority: string[]; - /** Minutes to skip exhausted account before retry (default: 5) */ - cooldown_minutes: number; -} - -/** - * Runtime quota monitor configuration. - * Controls adaptive polling during active sessions. - */ -export interface RuntimeMonitorConfig { - /** Enable runtime monitoring during sessions (default: true) */ - enabled: boolean; - /** Poll interval in seconds when quota > warn_threshold (default: 300) */ - normal_interval_seconds: number; - /** Poll interval in seconds when quota <= warn_threshold (default: 60) */ - critical_interval_seconds: number; - /** Quota percentage that triggers fast polling + warning (default: 20) */ - warn_threshold: number; - /** Quota percentage that triggers cooldown + switch (default: 5) */ - exhaustion_threshold: number; - /** Minutes to cooldown exhausted account (default: 5) */ - cooldown_minutes: number; -} - -/** - * Manual quota management configuration. - * User-controlled overrides for account selection. - */ -export interface ManualQuotaConfig { - /** User-paused accounts (stored in accounts.json) */ - paused_accounts: string[]; - /** Force use of specific account (overrides auto-selection) */ - forced_default: string | null; - /** Lock to specific tier only */ - tier_lock: string | null; -} - -/** - * Quota management mode. - * - auto: Fully automatic failover based on quota - * - manual: User controls everything, no auto-switching - * - hybrid: Auto-failover with user overrides (default) - */ -export type QuotaManagementMode = 'auto' | 'manual' | 'hybrid'; - -/** - * Quota management configuration section. - * Controls hybrid auto+manual account selection for multi-account setups. - */ -export interface QuotaManagementConfig { - /** Management mode (default: hybrid) */ - mode: QuotaManagementMode; - /** Auto mode settings */ - auto: AutoQuotaConfig; - /** Manual mode settings */ - manual: ManualQuotaConfig; - /** Runtime monitor settings */ - runtime_monitor: RuntimeMonitorConfig; -} - -/** - * Default auto quota configuration. - */ -export const DEFAULT_AUTO_QUOTA_CONFIG: AutoQuotaConfig = { - preflight_check: true, - exhaustion_threshold: 5, - tier_priority: ['ultra', 'pro', 'free'], - cooldown_minutes: 5, -}; - -/** - * Default manual quota configuration. - */ -export const DEFAULT_MANUAL_QUOTA_CONFIG: ManualQuotaConfig = { - paused_accounts: [], - forced_default: null, - tier_lock: null, -}; - -/** - * Default runtime monitor configuration. - */ -export const DEFAULT_RUNTIME_MONITOR_CONFIG: RuntimeMonitorConfig = { - enabled: true, - normal_interval_seconds: 300, - critical_interval_seconds: 60, - warn_threshold: 20, - exhaustion_threshold: 5, - cooldown_minutes: 5, -}; - -/** - * Default quota management configuration. - */ -export const DEFAULT_QUOTA_MANAGEMENT_CONFIG: QuotaManagementConfig = { - mode: 'hybrid', - auto: { ...DEFAULT_AUTO_QUOTA_CONFIG }, - manual: { ...DEFAULT_MANUAL_QUOTA_CONFIG }, - runtime_monitor: { ...DEFAULT_RUNTIME_MONITOR_CONFIG }, -}; - -// ============================================================================ -// THINKING CONFIGURATION (v8+) -// ============================================================================ - -/** - * Thinking mode for auto/manual/off control. - * - auto: Apply tier-based defaults (opus→high, sonnet→medium, haiku→low) - * - off: Disable thinking entirely - * - manual: Use explicit override value - */ -export type ThinkingMode = 'auto' | 'off' | 'manual'; - -/** - * Tier-to-thinking level defaults. - * Maps Claude tier names to thinking level names. - */ -export interface ThinkingTierDefaults { - /** Thinking level for opus tier (default: 'high') */ - opus: string; - /** Thinking level for sonnet tier (default: 'medium') */ - sonnet: string; - /** Thinking level for haiku tier (default: 'low') */ - haiku: string; -} - -/** - * Thinking configuration section. - * Controls thinking/reasoning budget injection for CLIProxy providers. - */ -export interface ThinkingConfig { - /** Thinking mode (default: 'auto') */ - mode: ThinkingMode; - /** Manual override value (level name or budget number) */ - override?: string | number; - /** Tier-to-level mapping */ - tier_defaults: ThinkingTierDefaults; - /** Per-provider overrides (e.g., { gemini: { opus: 'high' } }) */ - provider_overrides?: Record>; - /** Show warning when values are clamped (default: true) */ - show_warnings?: boolean; -} - -/** - * Default thinking tier defaults. - */ -export const DEFAULT_THINKING_TIER_DEFAULTS: ThinkingTierDefaults = { - opus: 'high', - sonnet: 'medium', - haiku: 'low', -}; - -/** - * Default thinking configuration. - */ -export const DEFAULT_THINKING_CONFIG: ThinkingConfig = { - mode: 'auto', - tier_defaults: { ...DEFAULT_THINKING_TIER_DEFAULTS }, - show_warnings: true, -}; - -/** - * Supported Anthropic official channel IDs. - */ -export type OfficialChannelId = 'telegram' | 'discord' | 'imessage'; - -/** - * Official Channels configuration. - * Controls runtime-only injection of Anthropic's official channel plugins. - */ -export interface OfficialChannelsConfig { - /** Selected official channels to auto-enable for compatible sessions */ - selected: OfficialChannelId[]; - /** Also add --dangerously-skip-permissions when auto-enable is active */ - unattended: boolean; -} - -/** - * Default Official Channels configuration. - * Disabled by default because the feature requires explicit user setup. - */ -export const DEFAULT_OFFICIAL_CHANNELS_CONFIG: OfficialChannelsConfig = { - selected: [], - unattended: false, -}; - -/** - * Dashboard authentication configuration. - * Optional login protection for CCS dashboard. - * Disabled by default for backward compatibility. - */ -export interface DashboardAuthConfig { - /** Enable dashboard authentication (default: false) */ - enabled: boolean; - /** Username for dashboard login */ - username: string; - /** Bcrypt-hashed password (use: npx bcrypt-cli hash 'password') */ - password_hash: string; - /** Session timeout in hours (default: 24) */ - session_timeout_hours?: number; -} - -/** - * Default dashboard auth configuration. - * Disabled by default - must be explicitly enabled. - */ -export const DEFAULT_DASHBOARD_AUTH_CONFIG: DashboardAuthConfig = { - enabled: false, - username: '', - password_hash: '', - session_timeout_hours: 24, -}; - -/** - * Browser automation configuration. - * Controls Claude browser attach and Codex browser tooling. - */ -export type BrowserToolPolicy = 'auto' | 'manual'; -export type BrowserEvalMode = 'disabled' | 'readonly' | 'readwrite'; - -export interface BrowserClaudeConfig { - /** Enable Claude browser attach (default: false) */ - enabled: boolean; - /** Control whether Claude browser attach is exposed automatically or only via --browser */ - policy: BrowserToolPolicy; - /** Chrome user-data directory used for attach mode */ - user_data_dir: string; - /** DevTools port used for attach mode (default: 9222) */ - devtools_port: number; - /** Eval access mode exposed through browser settings/status surfaces */ - eval_mode?: BrowserEvalMode; -} - -export interface BrowserCodexConfig { - /** Enable Codex browser tooling injection (default: false) */ - enabled: boolean; - /** Control whether Codex browser tooling is exposed automatically or only via --browser */ - policy: BrowserToolPolicy; - /** Eval access mode exposed through browser settings/status surfaces */ - eval_mode?: BrowserEvalMode; -} - -export interface BrowserConfig { - claude: BrowserClaudeConfig; - codex: BrowserCodexConfig; -} - -export const DEFAULT_BROWSER_CONFIG: BrowserConfig = { - claude: { - enabled: false, - policy: 'manual', - user_data_dir: '', - devtools_port: 9222, - eval_mode: 'readonly', - }, - codex: { - enabled: false, - policy: 'manual', - eval_mode: 'readonly', - }, -}; - -/** - * Image analysis configuration. - * Routes image/PDF files through CLIProxy for vision analysis. - */ -export interface ImageAnalysisConfig { - /** Enable image analysis via CLIProxy (default: true) */ - enabled: boolean; - /** Timeout in seconds (default: 60) */ - timeout: number; - /** Provider-to-model mapping for vision analysis */ - provider_models: Record; - /** Fallback backend used when a profile does not resolve to a provider-specific backend */ - fallback_backend?: string; - /** Explicit profile-name-to-backend overrides for settings/custom aliases */ - profile_backends?: Record; -} - -/** - * Default image analysis configuration. - * Enabled by default for CLIProxy providers with vision support. - */ -export const DEFAULT_IMAGE_ANALYSIS_CONFIG: ImageAnalysisConfig = { - enabled: true, - timeout: 60, - provider_models: { - agy: 'gemini-3-1-flash-preview', - gemini: 'gemini-3-flash-preview', - codex: 'gpt-5.1-codex-mini', - kiro: 'kiro-claude-haiku-4-5', - ghcp: 'claude-haiku-4.5', - claude: 'claude-haiku-4-5-20251001', - // 'vision-model' is a generic placeholder - users can override via config.yaml - qwen: 'vision-model', - iflow: 'qwen3-vl-plus', - kimi: 'vision-model', - }, - fallback_backend: 'gemini', - profile_backends: {}, -}; - -/** - * Main unified configuration structure. - * Stored in ~/.ccs/config.yaml - */ -export interface UnifiedConfig { - /** Config version (7 for quota management) */ - version: number; - /** Flag indicating setup wizard has been completed */ - setup_completed?: boolean; - /** Default profile name to use when none specified */ - default?: string; - /** Account-based profiles (isolated Claude instances) */ - accounts: Record; - /** API-based profiles (env var injection) */ - profiles: Record; - /** CLIProxy configuration */ - cliproxy: CLIProxyConfig; - /** OpenAI-compatible local proxy configuration */ - proxy?: OpenAICompatProxyConfig; - /** CCS-owned structured logging configuration */ - logging?: LoggingConfig; - /** User preferences */ - preferences: PreferencesConfig; - /** WebSearch configuration */ - websearch?: WebSearchConfig; - /** Global environment variables for all non-Claude subscription profiles */ - global_env?: GlobalEnvConfig; - /** Cross-profile continuity inheritance mapping */ - continuity?: ContinuityConfig; - /** Copilot API configuration (GitHub Copilot proxy) */ - copilot?: CopilotConfig; - /** Cursor IDE configuration (Cursor proxy daemon) */ - cursor?: CursorConfig; - /** CLIProxy server configuration for remote/local mode */ - cliproxy_server?: CliproxyServerConfig; - /** Quota management configuration (v7+) */ - quota_management?: QuotaManagementConfig; - /** Thinking/reasoning budget configuration (v8+) */ - thinking?: ThinkingConfig; - /** Discord Channels runtime auto-enable preferences (v11+) */ - channels?: OfficialChannelsConfig; - /** Dashboard authentication configuration (optional) */ - dashboard_auth?: DashboardAuthConfig; - /** Browser automation configuration */ - browser?: BrowserConfig; - /** Image analysis configuration (vision via CLIProxy) */ - image_analysis?: ImageAnalysisConfig; -} - -/** - * Default Copilot configuration. - * Strictly opt-in - disabled by default. - * Uses gpt-4.1 as default model (free tier compatible). - */ -export const DEFAULT_COPILOT_CONFIG: CopilotConfig = { - enabled: false, - auto_start: false, - port: 4141, - account_type: 'individual', - rate_limit: null, - wait_on_limit: true, - model: 'gpt-4.1', // Free tier compatible -}; - -/** - * Default Cursor configuration. - * Disabled by default, ghost mode enabled for privacy. - */ -export const DEFAULT_CURSOR_CONFIG: CursorConfig = { - enabled: false, - port: 20129, - auto_start: false, - ghost_mode: true, - model: 'gpt-5.3-codex', -}; - -/** - * Default CLIProxy server configuration. - * Local mode by default - remote must be explicitly enabled. - * Port is optional for remote - defaults based on protocol. - */ -export const DEFAULT_CLIPROXY_SERVER_CONFIG: CliproxyServerConfig = { - remote: { - enabled: false, - host: '', - // port is intentionally omitted - will use protocol default (443 for HTTPS, 8317 for HTTP) - protocol: 'http', - auth_token: '', - }, - fallback: { - enabled: true, - auto_start: false, - }, - local: { - port: 8317, - auto_start: true, - }, -}; - -export const DEFAULT_OPENAI_COMPAT_PROXY_CONFIG: OpenAICompatProxyConfig = { - profile_ports: {}, - routing: { - longContextThreshold: 60_000, - }, -}; - -/** - * Create an empty unified config with defaults. - */ -export function createEmptyUnifiedConfig(): UnifiedConfig { - return { - version: UNIFIED_CONFIG_VERSION, - default: undefined, - accounts: {}, - profiles: {}, - cliproxy: { - backend: 'original', - oauth_accounts: {}, - providers: [...CLIPROXY_SUPPORTED_PROVIDERS], - variants: {}, - logging: { - enabled: false, - request_log: false, - }, - safety: { ...DEFAULT_CLIPROXY_SAFETY_CONFIG }, - auto_sync: true, - routing: { - strategy: 'round-robin', - session_affinity: false, - session_affinity_ttl: '1h', - }, - }, - proxy: { - port: DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.port, - profile_ports: { ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.profile_ports }, - routing: { - ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.routing, - }, - }, - logging: { ...DEFAULT_LOGGING_CONFIG }, - preferences: { - theme: 'system', - telemetry: false, - auto_update: true, - }, - websearch: { - enabled: true, - providers: { - exa: { - enabled: false, - max_results: 5, - }, - tavily: { - enabled: false, - max_results: 5, - }, - brave: { - enabled: false, - max_results: 5, - }, - searxng: { - enabled: false, - url: '', - max_results: 5, - }, - duckduckgo: { - enabled: true, - max_results: 5, - }, - gemini: { - enabled: false, - model: 'gemini-2.5-flash', - timeout: 55, - }, - opencode: { - enabled: false, - model: 'opencode/grok-code', - timeout: 90, - }, - grok: { - enabled: false, - timeout: 55, - }, - }, - }, - global_env: { - enabled: true, - env: { ...DEFAULT_GLOBAL_ENV }, - }, - copilot: { ...DEFAULT_COPILOT_CONFIG }, - cursor: { ...DEFAULT_CURSOR_CONFIG }, - cliproxy_server: { ...DEFAULT_CLIPROXY_SERVER_CONFIG }, - quota_management: { ...DEFAULT_QUOTA_MANAGEMENT_CONFIG }, - thinking: { ...DEFAULT_THINKING_CONFIG }, - channels: { ...DEFAULT_OFFICIAL_CHANNELS_CONFIG }, - dashboard_auth: { ...DEFAULT_DASHBOARD_AUTH_CONFIG }, - browser: { - claude: { ...DEFAULT_BROWSER_CONFIG.claude }, - codex: { ...DEFAULT_BROWSER_CONFIG.codex }, - }, - image_analysis: { ...DEFAULT_IMAGE_ANALYSIS_CONFIG }, - }; -} - -/** - * Type guard for UnifiedConfig. - * Relaxed validation: accepts configs with version >= 1 and any subset of sections. - * Missing sections will be filled with defaults during merge. - */ -export function isUnifiedConfig(obj: unknown): obj is UnifiedConfig { - if (typeof obj !== 'object' || obj === null) return false; - const config = obj as Record; - // Only require version to be a number >= 1 (allow future versions) - // Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig - return typeof config.version === 'number' && config.version >= 1; -} +export * from './schemas/index'; From b7aea78512948a49f7069bd4d67eafb2e53ae49c Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 14:07:32 -0400 Subject: [PATCH 06/12] Revert "Revert "refactor(config): reorganize unified-config-types into schemas directory"" This reverts commit 06bce198eb8b9c6508e7b5fc3339cbda54faf608. --- .../__tests__/schemas-reexport.test.ts | 198 +++ src/config/schemas/auth.ts | 109 ++ src/config/schemas/browser.ts | 71 ++ src/config/schemas/channels.ts | 32 + src/config/schemas/cliproxy.ts | 151 +++ src/config/schemas/copilot-cursor.ts | 93 ++ src/config/schemas/index.ts | 112 ++ src/config/schemas/logging.ts | 53 + src/config/schemas/providers.ts | 30 + src/config/schemas/proxy-server.ts | 193 +++ src/config/schemas/quota.ts | 121 ++ src/config/schemas/thinking.ts | 66 + src/config/schemas/unified-config.ts | 200 +++ src/config/schemas/version.ts | 23 + src/config/schemas/websearch.ts | 148 +++ src/config/unified-config-types.ts | 1120 +---------------- 16 files changed, 1603 insertions(+), 1117 deletions(-) create mode 100644 src/config/schemas/__tests__/schemas-reexport.test.ts create mode 100644 src/config/schemas/auth.ts create mode 100644 src/config/schemas/browser.ts create mode 100644 src/config/schemas/channels.ts create mode 100644 src/config/schemas/cliproxy.ts create mode 100644 src/config/schemas/copilot-cursor.ts create mode 100644 src/config/schemas/index.ts create mode 100644 src/config/schemas/logging.ts create mode 100644 src/config/schemas/providers.ts create mode 100644 src/config/schemas/proxy-server.ts create mode 100644 src/config/schemas/quota.ts create mode 100644 src/config/schemas/thinking.ts create mode 100644 src/config/schemas/unified-config.ts create mode 100644 src/config/schemas/version.ts create mode 100644 src/config/schemas/websearch.ts diff --git a/src/config/schemas/__tests__/schemas-reexport.test.ts b/src/config/schemas/__tests__/schemas-reexport.test.ts new file mode 100644 index 00000000..5bdda5d9 --- /dev/null +++ b/src/config/schemas/__tests__/schemas-reexport.test.ts @@ -0,0 +1,198 @@ +/** + * Tests: config schemas re-export backward compatibility. + * + * Verifies that every type, interface, constant, and function originally + * exported from unified-config-types.ts is still accessible via both + * the barrel file and the schemas/index barrel. + */ + +import { describe, it, expect } from 'bun:test'; + +// Import from the backward-compatible barrel (this is what all existing code uses) +import * as barrel from '../../unified-config-types'; + +// Import from the new schemas barrel (this is what the barrel delegates to) +import * as schemas from '../index'; + +// --------------------------------------------------------------------------- +// Type-level checks (compile-time, not runtime) +// --------------------------------------------------------------------------- + +// Verify key interfaces are accessible as types +import type { + UnifiedConfig, + AccountConfig, + ProfileConfig, + OAuthAccounts, + CLIProxyAuthConfig, + TokenRefreshSettings, + DashboardAuthConfig, + CLIProxyVariantConfig, + CompositeTierConfig, + CompositeVariantConfig, + CLIProxyLoggingConfig, + CLIProxySafetyConfig, + CLIProxyRoutingConfig, + CLIProxyConfig, + AutoQuotaConfig, + RuntimeMonitorConfig, + ManualQuotaConfig, + QuotaManagementMode, + QuotaManagementConfig, + ThinkingMode, + ThinkingTierDefaults, + ThinkingConfig, + OfficialChannelId, + OfficialChannelsConfig, + DuckDuckGoWebSearchConfig, + BraveWebSearchConfig, + ExaWebSearchConfig, + TavilyWebSearchConfig, + SearxngWebSearchConfig, + GeminiWebSearchConfig, + GrokWebSearchConfig, + OpenCodeWebSearchConfig, + WebSearchProvidersConfig, + WebSearchConfig, + BrowserToolPolicy, + BrowserEvalMode, + BrowserClaudeConfig, + BrowserCodexConfig, + BrowserConfig, + LoggingLevel, + LoggingConfig, + PreferencesConfig, + CopilotAccountType, + CopilotConfig, + CursorConfig, + ProxyRemoteConfig, + ProxyFallbackConfig, + ProxyLocalConfig, + OpenAICompatProxyRoutingConfig, + OpenAICompatProxyConfig, + CliproxyServerConfig, + GlobalEnvConfig, + ContinuityConfig, + ImageAnalysisConfig, +} from '../../unified-config-types'; + +describe('config schemas backward compatibility', () => { + // ------------------------------------------------------------------------- + // Constants + // ------------------------------------------------------------------------- + it('re-exports UNIFIED_CONFIG_VERSION', () => { + expect(barrel.UNIFIED_CONFIG_VERSION).toBe(13); + expect(schemas.UNIFIED_CONFIG_VERSION).toBe(13); + }); + + it('re-exports CLIPROXY_SUPPORTED_PROVIDERS', () => { + expect(Array.isArray(barrel.CLIPROXY_SUPPORTED_PROVIDERS)).toBe(true); + expect(barrel.CLIPROXY_SUPPORTED_PROVIDERS.length).toBeGreaterThan(0); + expect(barrel.CLIPROXY_SUPPORTED_PROVIDERS).toEqual(schemas.CLIPROXY_SUPPORTED_PROVIDERS); + }); + + // ------------------------------------------------------------------------- + // Default constants + // ------------------------------------------------------------------------- + const defaultConstants = [ + 'DEFAULT_CLIPROXY_SAFETY_CONFIG', + 'DEFAULT_LOGGING_CONFIG', + 'DEFAULT_OFFICIAL_CHANNELS_CONFIG', + 'DEFAULT_BROWSER_CONFIG', + 'DEFAULT_DASHBOARD_AUTH_CONFIG', + 'DEFAULT_AUTO_QUOTA_CONFIG', + 'DEFAULT_MANUAL_QUOTA_CONFIG', + 'DEFAULT_RUNTIME_MONITOR_CONFIG', + 'DEFAULT_QUOTA_MANAGEMENT_CONFIG', + 'DEFAULT_THINKING_TIER_DEFAULTS', + 'DEFAULT_THINKING_CONFIG', + 'DEFAULT_COPILOT_CONFIG', + 'DEFAULT_CURSOR_CONFIG', + 'DEFAULT_CLIPROXY_SERVER_CONFIG', + 'DEFAULT_OPENAI_COMPAT_PROXY_CONFIG', + 'DEFAULT_GLOBAL_ENV', + 'DEFAULT_IMAGE_ANALYSIS_CONFIG', + ] as const; + + for (const name of defaultConstants) { + it(`re-exports ${name}`, () => { + expect(barrel[name]).toBeDefined(); + expect(barrel[name]).toEqual(schemas[name]); + }); + } + + // ------------------------------------------------------------------------- + // Functions + // ------------------------------------------------------------------------- + it('re-exports createEmptyUnifiedConfig', () => { + expect(typeof barrel.createEmptyUnifiedConfig).toBe('function'); + expect(typeof schemas.createEmptyUnifiedConfig).toBe('function'); + + const config = barrel.createEmptyUnifiedConfig(); + expect(config.version).toBe(13); + expect(config.accounts).toEqual({}); + expect(config.profiles).toEqual({}); + expect(config.cliproxy).toBeDefined(); + expect(config.cliproxy.oauth_accounts).toEqual({}); + expect(config.cliproxy.variants).toEqual({}); + expect(config.logging).toBeDefined(); + expect(config.preferences).toBeDefined(); + expect(config.browser).toBeDefined(); + expect(config.image_analysis).toBeDefined(); + expect(config.quota_management).toBeDefined(); + expect(config.thinking).toBeDefined(); + expect(config.channels).toBeDefined(); + expect(config.dashboard_auth).toBeDefined(); + expect(config.copilot).toBeDefined(); + expect(config.cursor).toBeDefined(); + expect(config.cliproxy_server).toBeDefined(); + expect(config.websearch).toBeDefined(); + }); + + it('re-exports isUnifiedConfig', () => { + expect(typeof barrel.isUnifiedConfig).toBe('function'); + expect(typeof schemas.isUnifiedConfig).toBe('function'); + + expect(barrel.isUnifiedConfig({ version: 13 })).toBe(true); + expect(barrel.isUnifiedConfig(null)).toBe(false); + expect(barrel.isUnifiedConfig({})).toBe(false); + expect(barrel.isUnifiedConfig({ version: 0 })).toBe(false); + expect(barrel.isUnifiedConfig({ version: 1 })).toBe(true); + expect(barrel.isUnifiedConfig('not an object')).toBe(false); + }); + + // ------------------------------------------------------------------------- + // Barrel has all expected runtime exports (type-only exports are verified + // at compile time via the import type block above — they are erased at + // runtime and cannot be checked with the `in` operator). + // ------------------------------------------------------------------------- + const expectedRuntimeExports = [ + 'UNIFIED_CONFIG_VERSION', + 'CLIPROXY_SUPPORTED_PROVIDERS', + 'createEmptyUnifiedConfig', + 'isUnifiedConfig', + 'DEFAULT_CLIPROXY_SAFETY_CONFIG', + 'DEFAULT_LOGGING_CONFIG', + 'DEFAULT_OFFICIAL_CHANNELS_CONFIG', + 'DEFAULT_BROWSER_CONFIG', + 'DEFAULT_DASHBOARD_AUTH_CONFIG', + 'DEFAULT_AUTO_QUOTA_CONFIG', + 'DEFAULT_MANUAL_QUOTA_CONFIG', + 'DEFAULT_RUNTIME_MONITOR_CONFIG', + 'DEFAULT_QUOTA_MANAGEMENT_CONFIG', + 'DEFAULT_THINKING_TIER_DEFAULTS', + 'DEFAULT_THINKING_CONFIG', + 'DEFAULT_COPILOT_CONFIG', + 'DEFAULT_CURSOR_CONFIG', + 'DEFAULT_CLIPROXY_SERVER_CONFIG', + 'DEFAULT_OPENAI_COMPAT_PROXY_CONFIG', + 'DEFAULT_GLOBAL_ENV', + 'DEFAULT_IMAGE_ANALYSIS_CONFIG', + ] as const; + + for (const name of expectedRuntimeExports) { + it(`barrel exports "${name}"`, () => { + expect(name in barrel).toBe(true); + }); + } +}); diff --git a/src/config/schemas/auth.ts b/src/config/schemas/auth.ts new file mode 100644 index 00000000..4056ecf8 --- /dev/null +++ b/src/config/schemas/auth.ts @@ -0,0 +1,109 @@ +/** + * Account, profile, and authentication config types. + * + * Covers: + * - AccountConfig: isolated Claude instances via CLAUDE_CONFIG_DIR + * - ProfileConfig: API-based profiles (env var injection) + * - OAuthAccounts: CLIProxy nickname-to-email mapping + * - CLIProxyAuthConfig: API key and management secret customization + * - TokenRefreshSettings: background token refresh worker config + * - DashboardAuthConfig: dashboard login protection + */ + +import type { TargetType } from '../../targets/target-adapter'; + +/** + * Account configuration (formerly in profiles.json). + * Represents an isolated Claude instance via CLAUDE_CONFIG_DIR. + */ +export interface AccountConfig { + /** ISO timestamp when account was created */ + created: string; + /** ISO timestamp of last usage, null if never used */ + last_used: string | null; + /** Context mode for project workspace data */ + context_mode?: 'isolated' | 'shared'; + /** Context-sharing group when context_mode='shared' */ + context_group?: string; + /** Shared continuity depth when context_mode='shared' */ + continuity_mode?: 'standard' | 'deeper'; + /** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */ + bare?: boolean; +} + +/** + * API-based profile configuration. + * Injects environment variables for alternative providers (GLM, Kimi, etc.). + * + * Settings are stored in separate *.settings.json files (matching Claude's pattern) + * to allow users to edit them directly without touching config.yaml. + */ +export interface ProfileConfig { + /** Profile type - currently only 'api' */ + type: 'api'; + /** Path to settings file (e.g., "~/.ccs/glm.settings.json") */ + settings: string; + /** Target CLI to use for this profile (default: 'claude') */ + target?: TargetType; +} + +/** + * CLIProxy OAuth account nickname mapping. + * Maps user-friendly nicknames to email addresses. + */ +export type OAuthAccounts = Record; + +/** + * CLIProxy authentication configuration. + * Allows customization of API key and management secret for CLIProxyAPI. + */ +export interface CLIProxyAuthConfig { + /** API key for CCS-managed requests (default: 'ccs-internal-managed') */ + api_key?: string; + /** Management secret for Control Panel login (default: 'ccs') */ + management_secret?: string; +} + +/** + * Token refresh configuration. + * Manages background token refresh worker settings. + */ +export interface TokenRefreshSettings { + /** Enable background token refresh (default: false) */ + enabled?: boolean; + /** Refresh check interval in minutes (default: 30) */ + interval_minutes?: number; + /** Preemptive refresh time in minutes (default: 45) */ + preemptive_minutes?: number; + /** Maximum retry attempts per token (default: 3) */ + max_retries?: number; + /** Enable verbose logging (default: false) */ + verbose?: boolean; +} + +/** + * Dashboard authentication configuration. + * Optional login protection for CCS dashboard. + * Disabled by default for backward compatibility. + */ +export interface DashboardAuthConfig { + /** Enable dashboard authentication (default: false) */ + enabled: boolean; + /** Username for dashboard login */ + username: string; + /** Bcrypt-hashed password (use: npx bcrypt-cli hash 'password') */ + password_hash: string; + /** Session timeout in hours (default: 24) */ + session_timeout_hours?: number; +} + +/** + * Default dashboard auth configuration. + * Disabled by default - must be explicitly enabled. + */ +export const DEFAULT_DASHBOARD_AUTH_CONFIG: DashboardAuthConfig = { + enabled: false, + username: '', + password_hash: '', + session_timeout_hours: 24, +}; diff --git a/src/config/schemas/browser.ts b/src/config/schemas/browser.ts new file mode 100644 index 00000000..f4facb4b --- /dev/null +++ b/src/config/schemas/browser.ts @@ -0,0 +1,71 @@ +/** + * Browser automation configuration types and defaults. + * + * Controls Claude browser attach and Codex browser tooling. + * Version 13+ feature. + */ + +/** + * Browser tool exposure policy. + */ +export type BrowserToolPolicy = 'auto' | 'manual'; + +/** + * Browser eval access mode. + */ +export type BrowserEvalMode = 'disabled' | 'readonly' | 'readwrite'; + +/** + * Claude browser attach configuration. + */ +export interface BrowserClaudeConfig { + /** Enable Claude browser attach (default: false) */ + enabled: boolean; + /** Control whether Claude browser attach is exposed automatically or only via --browser */ + policy: BrowserToolPolicy; + /** Chrome user-data directory used for attach mode */ + user_data_dir: string; + /** DevTools port used for attach mode (default: 9222) */ + devtools_port: number; + /** Eval access mode exposed through browser settings/status surfaces */ + eval_mode?: BrowserEvalMode; +} + +/** + * Codex browser tooling configuration. + */ +export interface BrowserCodexConfig { + /** Enable Codex browser tooling injection (default: false) */ + enabled: boolean; + /** Control whether Codex browser tooling is exposed automatically or only via --browser */ + policy: BrowserToolPolicy; + /** Eval access mode exposed through browser settings/status surfaces */ + eval_mode?: BrowserEvalMode; +} + +/** + * Browser automation configuration. + * Controls Claude browser attach and Codex browser tooling. + */ +export interface BrowserConfig { + claude: BrowserClaudeConfig; + codex: BrowserCodexConfig; +} + +/** + * Default browser configuration. + */ +export const DEFAULT_BROWSER_CONFIG: BrowserConfig = { + claude: { + enabled: false, + policy: 'manual', + user_data_dir: '', + devtools_port: 9222, + eval_mode: 'readonly', + }, + codex: { + enabled: false, + policy: 'manual', + eval_mode: 'readonly', + }, +}; diff --git a/src/config/schemas/channels.ts b/src/config/schemas/channels.ts new file mode 100644 index 00000000..f8d5274c --- /dev/null +++ b/src/config/schemas/channels.ts @@ -0,0 +1,32 @@ +/** + * Official Channels configuration types and defaults. + * + * Controls runtime-only injection of Anthropic's official channel plugins + * (Telegram, Discord, iMessage). + * Version 12+ feature. + */ + +/** + * Supported Anthropic official channel IDs. + */ +export type OfficialChannelId = 'telegram' | 'discord' | 'imessage'; + +/** + * Official Channels configuration. + * Controls runtime-only injection of Anthropic's official channel plugins. + */ +export interface OfficialChannelsConfig { + /** Selected official channels to auto-enable for compatible sessions */ + selected: OfficialChannelId[]; + /** Also add --dangerously-skip-permissions when auto-enable is active */ + unattended: boolean; +} + +/** + * Default Official Channels configuration. + * Disabled by default because the feature requires explicit user setup. + */ +export const DEFAULT_OFFICIAL_CHANNELS_CONFIG: OfficialChannelsConfig = { + selected: [], + unattended: false, +}; diff --git a/src/config/schemas/cliproxy.ts b/src/config/schemas/cliproxy.ts new file mode 100644 index 00000000..1765335f --- /dev/null +++ b/src/config/schemas/cliproxy.ts @@ -0,0 +1,151 @@ +/** + * CLIProxy configuration types and defaults. + * + * Covers provider/variant/routing/safety/logging configuration + * for the CLIProxy integration layer. + */ + +import type { TargetType } from '../../targets/target-adapter'; +import type { CLIProxyProvider, CliproxyRoutingStrategy } from '../../cliproxy/types'; +import { CLIPROXY_PROVIDER_IDS } from '../../cliproxy/provider-capabilities'; +import type { OAuthAccounts, CLIProxyAuthConfig, TokenRefreshSettings } from './auth'; + +/** + * Supported CLIProxy providers. + * Derived from CLIPROXY_PROVIDER_IDS — single source of truth in provider-capabilities.ts. + */ +export { CLIPROXY_PROVIDER_IDS as CLIPROXY_SUPPORTED_PROVIDERS }; + +/** + * CLIProxy variant configuration. + * User-defined variants of built-in OAuth providers. + * + * Settings are stored in separate *.settings.json files (matching Claude's pattern) + * to allow users to edit them directly without touching config.yaml. + */ +export interface CLIProxyVariantConfig { + /** Base provider to use */ + provider: CLIProxyProvider; + /** Account nickname (references oauth_accounts) */ + account?: string; + /** Path to settings file (e.g., "~/.ccs/gemini-custom.settings.json") */ + settings?: string; + /** Unique port for variant isolation (8318-8417) */ + port?: number; + /** Per-variant auth override (optional) */ + auth?: CLIProxyAuthConfig; + /** Target CLI to use for this variant (default: 'claude') */ + target?: TargetType; +} + +/** + * Per-tier provider+model mapping for composite variants. + */ +export interface CompositeTierConfig { + /** Provider for this tier */ + provider: CLIProxyProvider; + /** Model ID to use for this tier */ + model: string; + /** Account nickname (optional, references oauth_accounts) */ + account?: string; + /** Fallback provider+model if primary fails */ + fallback?: { + provider: CLIProxyProvider; + model: string; + account?: string; + }; + /** Per-tier thinking budget override (e.g. 'xhigh', 'medium', 'off') */ + thinking?: string; +} + +/** + * Composite variant configuration. + * Mixes different providers per Claude tier (opus, sonnet, haiku) in a single profile. + * Uses CLIProxyAPI root endpoints (/v1/messages) for model-based routing + * instead of provider-specific endpoints (/api/provider/{provider}). + */ +export interface CompositeVariantConfig { + /** Discriminator for composite type */ + type: 'composite'; + /** Which tier ANTHROPIC_MODEL equals (default must be one of the three) */ + default_tier: 'opus' | 'sonnet' | 'haiku'; + /** Per-tier provider+model mapping */ + tiers: { + opus: CompositeTierConfig; + sonnet: CompositeTierConfig; + haiku: CompositeTierConfig; + }; + /** Path to settings file */ + settings?: string; + /** Shared port for the composite profile */ + port?: number; + /** Per-variant auth override (optional) */ + auth?: CLIProxyAuthConfig; + /** Target CLI to use for this composite variant (default: 'claude') */ + target?: TargetType; +} + +/** + * CLIProxy logging configuration. + * Controls whether CLIProxyAPI writes logs to disk. + * Logs can grow to several GB if left enabled. + */ +export interface CLIProxyLoggingConfig { + /** Enable logging to file (default: false to prevent disk bloat) */ + enabled?: boolean; + /** Enable request logging for debugging (default: false) */ + request_log?: boolean; +} + +/** + * CLIProxy safety configuration. + * Controls high-risk flow safeguards for supported providers. + */ +export interface CLIProxySafetyConfig { + /** Allow skipping AGY responsibility checks and Gemini dashboard typed acknowledgement */ + antigravity_ack_bypass?: boolean; +} + +/** + * Default CLIProxy safety configuration. + */ +export const DEFAULT_CLIPROXY_SAFETY_CONFIG: CLIProxySafetyConfig = { + antigravity_ack_bypass: false, +}; + +export interface CLIProxyRoutingConfig { + /** Credential selection strategy when multiple accounts match */ + strategy?: CliproxyRoutingStrategy; + /** Keep one conversation pinned to the same account when possible */ + session_affinity?: boolean; + /** Go-style duration for session-affinity binding retention */ + session_affinity_ttl?: string; +} + +/** + * CLIProxy configuration section. + */ +export interface CLIProxyConfig { + /** Backend selection: 'original' or 'plus' (default: 'original') */ + backend?: 'original' | 'plus'; + /** Nickname to email mapping for OAuth accounts */ + oauth_accounts: OAuthAccounts; + /** Built-in providers (read-only, for reference) */ + providers: readonly string[]; + /** User-defined provider variants (single-provider or composite) */ + variants: Record; + /** Logging configuration (disabled by default) */ + logging?: CLIProxyLoggingConfig; + /** Safety controls for high-risk provider flows */ + safety?: CLIProxySafetyConfig; + /** Kiro: disable incognito browser mode (use normal browser to save credentials) */ + kiro_no_incognito?: boolean; + /** Global auth configuration for CLIProxyAPI */ + auth?: CLIProxyAuthConfig; + /** Background token refresh worker settings */ + token_refresh?: TokenRefreshSettings; + /** Auto-sync API profiles to local CLIProxy config on settings change (default: true) */ + auto_sync?: boolean; + /** Routing strategy for multi-account CLIProxy selection */ + routing?: CLIProxyRoutingConfig; +} diff --git a/src/config/schemas/copilot-cursor.ts b/src/config/schemas/copilot-cursor.ts new file mode 100644 index 00000000..e0b33fbb --- /dev/null +++ b/src/config/schemas/copilot-cursor.ts @@ -0,0 +1,93 @@ +/** + * Copilot and Cursor IDE integration configuration types and defaults. + * + * Covers: + * - CopilotConfig: GitHub Copilot proxy integration (strictly opt-in) + * - CursorConfig: Cursor IDE proxy daemon + */ + +/** + * Copilot API account type. + */ +export type CopilotAccountType = 'individual' | 'business' | 'enterprise'; + +/** + * Copilot API configuration. + * Enables GitHub Copilot subscription usage via copilot-api proxy. + * Strictly opt-in - disabled by default. + * + * !! DISCLAIMER - USE AT YOUR OWN RISK !! + * This uses an UNOFFICIAL reverse-engineered API. + * Excessive usage may trigger GitHub account restrictions. + * CCS provides NO WARRANTY and accepts NO RESPONSIBILITY for any consequences. + */ +export interface CopilotConfig { + /** Enable Copilot integration (default: false) - must be explicitly enabled */ + enabled: boolean; + /** Auto-start copilot-api daemon when using profile (default: false) */ + auto_start: boolean; + /** Port for copilot-api proxy (default: 4141) */ + port: number; + /** GitHub Copilot account type (default: individual) */ + account_type: CopilotAccountType; + /** Rate limit in seconds between requests (null = no limit) */ + rate_limit: number | null; + /** Wait instead of error when rate limit is hit (default: true) */ + wait_on_limit: boolean; + /** Default model ID (e.g., claude-sonnet-4.5) */ + model: string; + /** Model mapping for Claude tiers - maps opus/sonnet/haiku to specific models */ + opus_model?: string; + sonnet_model?: string; + haiku_model?: string; +} + +/** + * Cursor IDE integration configuration. + * Enables Cursor IDE usage via cursor proxy daemon. + */ +export interface CursorConfig { + /** Enable Cursor integration (default: false) */ + enabled: boolean; + /** Port for cursor proxy daemon (default: 20129) */ + port: number; + /** Auto-start daemon when CCS starts (default: false) */ + auto_start: boolean; + /** Enable ghost mode to disable telemetry (default: true) */ + ghost_mode: boolean; + /** Default model ID used by Cursor integration */ + model: string; + /** Optional tier mapping for Claude-compatible model routing */ + opus_model?: string; + /** Optional tier mapping for Claude-compatible model routing */ + sonnet_model?: string; + /** Optional tier mapping for Claude-compatible model routing */ + haiku_model?: string; +} + +/** + * Default Copilot configuration. + * Strictly opt-in - disabled by default. + * Uses gpt-4.1 as default model (free tier compatible). + */ +export const DEFAULT_COPILOT_CONFIG: CopilotConfig = { + enabled: false, + auto_start: false, + port: 4141, + account_type: 'individual', + rate_limit: null, + wait_on_limit: true, + model: 'gpt-4.1', +}; + +/** + * Default Cursor configuration. + * Disabled by default, ghost mode enabled for privacy. + */ +export const DEFAULT_CURSOR_CONFIG: CursorConfig = { + enabled: false, + port: 20129, + auto_start: false, + ghost_mode: true, + model: 'gpt-5.3-codex', +}; diff --git a/src/config/schemas/index.ts b/src/config/schemas/index.ts new file mode 100644 index 00000000..ff0313f3 --- /dev/null +++ b/src/config/schemas/index.ts @@ -0,0 +1,112 @@ +/** + * Config schema barrel re-exports. + * + * All types, interfaces, constants, and functions originally in + * unified-config-types.ts are re-exported here for backward compatibility. + * Each module is responsible for a focused domain of the config schema. + */ + +// Version constant +export { UNIFIED_CONFIG_VERSION } from './version'; + +// Account, profile, OAuth, auth types +export type { + AccountConfig, + ProfileConfig, + OAuthAccounts, + CLIProxyAuthConfig, + TokenRefreshSettings, + DashboardAuthConfig, +} from './auth'; +export { DEFAULT_DASHBOARD_AUTH_CONFIG } from './auth'; + +// CLIProxy provider, variant, routing, safety, logging types +export { CLIPROXY_SUPPORTED_PROVIDERS, DEFAULT_CLIPROXY_SAFETY_CONFIG } from './cliproxy'; +export type { + CLIProxyVariantConfig, + CompositeTierConfig, + CompositeVariantConfig, + CLIProxyLoggingConfig, + CLIProxySafetyConfig, + CLIProxyRoutingConfig, + CLIProxyConfig, +} from './cliproxy'; + +// Quota management types and defaults +export { + DEFAULT_AUTO_QUOTA_CONFIG, + DEFAULT_MANUAL_QUOTA_CONFIG, + DEFAULT_RUNTIME_MONITOR_CONFIG, + DEFAULT_QUOTA_MANAGEMENT_CONFIG, +} from './quota'; +export type { + AutoQuotaConfig, + RuntimeMonitorConfig, + ManualQuotaConfig, + QuotaManagementMode, + QuotaManagementConfig, +} from './quota'; + +// Thinking/reasoning budget types and defaults +export { DEFAULT_THINKING_TIER_DEFAULTS, DEFAULT_THINKING_CONFIG } from './thinking'; +export type { ThinkingMode, ThinkingTierDefaults, ThinkingConfig } from './thinking'; + +// Official channels types and defaults +export { DEFAULT_OFFICIAL_CHANNELS_CONFIG } from './channels'; +export type { OfficialChannelId, OfficialChannelsConfig } from './channels'; + +// WebSearch backend types +export type { + DuckDuckGoWebSearchConfig, + BraveWebSearchConfig, + ExaWebSearchConfig, + TavilyWebSearchConfig, + SearxngWebSearchConfig, + GeminiWebSearchConfig, + GrokWebSearchConfig, + OpenCodeWebSearchConfig, + WebSearchProvidersConfig, + WebSearchConfig, +} from './websearch'; + +// Browser automation types and defaults +export { DEFAULT_BROWSER_CONFIG } from './browser'; +export type { + BrowserToolPolicy, + BrowserEvalMode, + BrowserClaudeConfig, + BrowserCodexConfig, + BrowserConfig, +} from './browser'; + +// Logging and preferences types and defaults +export { DEFAULT_LOGGING_CONFIG } from './logging'; +export type { LoggingLevel, LoggingConfig, PreferencesConfig } from './logging'; + +// Provider integration types and defaults +export { + DEFAULT_GLOBAL_ENV, + DEFAULT_COPILOT_CONFIG, + DEFAULT_CURSOR_CONFIG, + DEFAULT_CLIPROXY_SERVER_CONFIG, + DEFAULT_OPENAI_COMPAT_PROXY_CONFIG, + DEFAULT_IMAGE_ANALYSIS_CONFIG, +} from './providers'; +export type { + CopilotAccountType, + CopilotConfig, + CursorConfig, + ProxyRemoteConfig, + ProxyFallbackConfig, + ProxyLocalConfig, + OpenAICompatProxyRoutingConfig, + OpenAICompatProxyConfig, + CliproxyServerConfig, + GlobalEnvConfig, + ContinuityConfig, + ImageAnalysisConfig, +} from './providers'; + +// Main unified config interface, factory, and type guard +export { createEmptyUnifiedConfig, isUnifiedConfig } from './unified-config'; +export type { UnifiedConfig } from './unified-config'; diff --git a/src/config/schemas/logging.ts b/src/config/schemas/logging.ts new file mode 100644 index 00000000..05fe3061 --- /dev/null +++ b/src/config/schemas/logging.ts @@ -0,0 +1,53 @@ +/** + * Logging and preferences configuration types and defaults. + * + * Covers: + * - LoggingConfig: CCS-owned structured runtime logging + * - LoggingLevel: log severity levels + * - PreferencesConfig: user preferences (theme, telemetry, auto-update) + */ + +export type LoggingLevel = 'error' | 'warn' | 'info' | 'debug'; + +/** + * CCS-owned structured logging configuration. + * Separate from cliproxy.logging, which controls CLIProxy runtime files. + */ +export interface LoggingConfig { + /** Enable CCS-owned structured runtime logging */ + enabled: boolean; + /** Minimum level written to disk */ + level: LoggingLevel; + /** Rotate current log when it reaches this size in MB */ + rotate_mb: number; + /** Keep archived segments for this many days */ + retain_days: number; + /** Redact sensitive values before persistence */ + redact: boolean; + /** In-memory recent event buffer size for dashboard reads */ + live_buffer_size: number; +} + +/** + * Default logging configuration. + */ +export const DEFAULT_LOGGING_CONFIG: LoggingConfig = { + enabled: true, + level: 'info', + rotate_mb: 10, + retain_days: 7, + redact: true, + live_buffer_size: 250, +}; + +/** + * User preferences. + */ +export interface PreferencesConfig { + /** UI theme preference */ + theme?: 'light' | 'dark' | 'system'; + /** Enable anonymous telemetry */ + telemetry?: boolean; + /** Enable automatic update checks */ + auto_update?: boolean; +} diff --git a/src/config/schemas/providers.ts b/src/config/schemas/providers.ts new file mode 100644 index 00000000..78a6dc8f --- /dev/null +++ b/src/config/schemas/providers.ts @@ -0,0 +1,30 @@ +/** + * Provider integration configuration types and defaults. + * + * Re-exports from focused sub-modules for backward compatibility. + * Actual definitions live in: + * - copilot-cursor.ts: CopilotConfig, CursorConfig + defaults + * - proxy-server.ts: CliproxyServerConfig, OpenAICompatProxyConfig, + * GlobalEnvConfig, ContinuityConfig, ImageAnalysisConfig + defaults + */ + +export type { CopilotAccountType, CopilotConfig, CursorConfig } from './copilot-cursor'; +export { DEFAULT_COPILOT_CONFIG, DEFAULT_CURSOR_CONFIG } from './copilot-cursor'; + +export type { + ProxyRemoteConfig, + ProxyFallbackConfig, + ProxyLocalConfig, + OpenAICompatProxyRoutingConfig, + OpenAICompatProxyConfig, + CliproxyServerConfig, + GlobalEnvConfig, + ContinuityConfig, + ImageAnalysisConfig, +} from './proxy-server'; +export { + DEFAULT_CLIPROXY_SERVER_CONFIG, + DEFAULT_OPENAI_COMPAT_PROXY_CONFIG, + DEFAULT_GLOBAL_ENV, + DEFAULT_IMAGE_ANALYSIS_CONFIG, +} from './proxy-server'; diff --git a/src/config/schemas/proxy-server.ts b/src/config/schemas/proxy-server.ts new file mode 100644 index 00000000..745ebab3 --- /dev/null +++ b/src/config/schemas/proxy-server.ts @@ -0,0 +1,193 @@ +/** + * Proxy server, global env, continuity, and image analysis types and defaults. + * + * Covers: + * - CliproxyServerConfig: remote/local CLIProxy server mode + * - OpenAICompatProxyConfig: OpenAI-compatible local proxy + * - GlobalEnvConfig: global environment variable injection + * - ContinuityConfig: cross-profile continuity inheritance + * - ImageAnalysisConfig: vision analysis via CLIProxy + */ + +/** + * Remote proxy configuration. + * Connect to a remote CLIProxyAPI instance instead of spawning local binary. + */ +export interface ProxyRemoteConfig { + /** Enable remote proxy mode (default: false = local mode) */ + enabled: boolean; + /** Remote proxy hostname or IP (empty = not configured) */ + host: string; + /** + * Remote proxy port. + * Optional - defaults based on protocol: + * - HTTPS: 443 + * - HTTP: 8317 + * When empty/undefined, uses protocol default. + */ + port?: number; + /** Protocol for remote connection */ + protocol: 'http' | 'https'; + /** Auth token for remote proxy API endpoints (optional, sent as header) */ + auth_token: string; + /** + * Management key for remote proxy management API endpoints. + * CLIProxyAPI uses separate authentication for management endpoints + * (/v0/management/*) via 'secret-key' config. + * If not set, falls back to auth_token for backwards compatibility. + */ + management_key?: string; + /** Connection timeout in milliseconds (default: 2000) */ + timeout?: number; + /** Enable auto-sync profiles to remote on settings change (default: false) */ + auto_sync?: boolean; +} + +/** + * Fallback configuration when remote proxy is unreachable. + */ +export interface ProxyFallbackConfig { + /** Enable fallback to local proxy (default: true) */ + enabled: boolean; + /** Auto-start local proxy without prompting (default: false = prompt user) */ + auto_start: boolean; +} + +/** + * Local proxy configuration. + */ +export interface ProxyLocalConfig { + /** Local proxy port (default: 8317) */ + port: number; + /** Auto-start local binary (default: true) */ + auto_start: boolean; +} + +export interface OpenAICompatProxyRoutingConfig { + default?: string; + background?: string; + think?: string; + longContext?: string; + webSearch?: string; + longContextThreshold?: number; +} + +export interface OpenAICompatProxyConfig { + /** Default local port for OpenAI-compatible proxy instances */ + port?: number; + /** Optional profile-scoped local port overrides */ + profile_ports?: Record; + routing?: OpenAICompatProxyRoutingConfig; +} + +/** + * CLIProxy server configuration section. + * Controls whether CCS uses local or remote CLIProxyAPI instance. + */ +export interface CliproxyServerConfig { + /** Remote proxy settings */ + remote: ProxyRemoteConfig; + /** Fallback behavior when remote is unreachable */ + fallback: ProxyFallbackConfig; + /** Local proxy settings */ + local: ProxyLocalConfig; +} + +/** + * Global environment variables configuration. + * These env vars are injected into ALL non-Claude subscription profiles. + * Useful for disabling telemetry, bug commands, error reporting, etc. + */ +export interface GlobalEnvConfig { + /** Enable global env injection (default: true) */ + enabled: boolean; + /** Environment variables to inject */ + env: Record; +} + +/** + * Cross-profile continuity inheritance configuration. + * Maps execution profile names to source account profiles for CLAUDE_CONFIG_DIR reuse. + */ +export interface ContinuityConfig { + /** Profile name -> source account profile name */ + inherit_from_account?: Record; +} + +/** + * Default global env vars for third-party profiles. + * These disable Claude Code telemetry/reporting since we're using proxy. + */ +export const DEFAULT_GLOBAL_ENV: Record = { + DISABLE_BUG_COMMAND: '1', + DISABLE_ERROR_REPORTING: '1', + DISABLE_TELEMETRY: '1', +}; + +/** + * Default CLIProxy server configuration. + * Local mode by default - remote must be explicitly enabled. + * Port is optional for remote - defaults based on protocol. + */ +export const DEFAULT_CLIPROXY_SERVER_CONFIG: CliproxyServerConfig = { + remote: { + enabled: false, + host: '', + protocol: 'http', + auth_token: '', + }, + fallback: { + enabled: true, + auto_start: false, + }, + local: { + port: 8317, + auto_start: true, + }, +}; + +export const DEFAULT_OPENAI_COMPAT_PROXY_CONFIG: OpenAICompatProxyConfig = { + profile_ports: {}, + routing: { + longContextThreshold: 60_000, + }, +}; + +/** + * Image analysis configuration. + * Routes image/PDF files through CLIProxy for vision analysis. + */ +export interface ImageAnalysisConfig { + /** Enable image analysis via CLIProxy (default: true) */ + enabled: boolean; + /** Timeout in seconds (default: 60) */ + timeout: number; + /** Provider-to-model mapping for vision analysis */ + provider_models: Record; + /** Fallback backend used when a profile does not resolve to a provider-specific backend */ + fallback_backend?: string; + /** Explicit profile-name-to-backend overrides for settings/custom aliases */ + profile_backends?: Record; +} + +/** + * Default image analysis configuration. + * Enabled by default for CLIProxy providers with vision support. + */ +export const DEFAULT_IMAGE_ANALYSIS_CONFIG: ImageAnalysisConfig = { + enabled: true, + timeout: 60, + provider_models: { + agy: 'gemini-3-1-flash-preview', + gemini: 'gemini-3-flash-preview', + codex: 'gpt-5.1-codex-mini', + kiro: 'kiro-claude-haiku-4-5', + ghcp: 'claude-haiku-4.5', + claude: 'claude-haiku-4.5-20251001', + qwen: 'vision-model', + iflow: 'qwen3-vl-plus', + kimi: 'vision-model', + }, + fallback_backend: 'gemini', + profile_backends: {}, +}; diff --git a/src/config/schemas/quota.ts b/src/config/schemas/quota.ts new file mode 100644 index 00000000..0d380694 --- /dev/null +++ b/src/config/schemas/quota.ts @@ -0,0 +1,121 @@ +/** + * Quota management configuration types and defaults. + * + * Controls hybrid auto+manual account selection for multi-account setups. + * Version 7+ feature. + */ + +// ============================================================================ +// QUOTA MANAGEMENT CONFIGURATION (v7+) +// ============================================================================ + +/** + * Auto quota management configuration. + * Controls automatic failover behavior. + */ +export interface AutoQuotaConfig { + /** Enable pre-flight quota check before requests (default: true) */ + preflight_check: boolean; + /** Quota percentage below which account is "exhausted" (default: 5) */ + exhaustion_threshold: number; + /** Tier priority for failover, highest to lowest (default: ['paid']) */ + tier_priority: string[]; + /** Minutes to skip exhausted account before retry (default: 5) */ + cooldown_minutes: number; +} + +/** + * Runtime quota monitor configuration. + * Controls adaptive polling during active sessions. + */ +export interface RuntimeMonitorConfig { + /** Enable runtime monitoring during sessions (default: true) */ + enabled: boolean; + /** Poll interval in seconds when quota > warn_threshold (default: 300) */ + normal_interval_seconds: number; + /** Poll interval in seconds when quota <= warn_threshold (default: 60) */ + critical_interval_seconds: number; + /** Quota percentage that triggers fast polling + warning (default: 20) */ + warn_threshold: number; + /** Quota percentage that triggers cooldown + switch (default: 5) */ + exhaustion_threshold: number; + /** Minutes to cooldown exhausted account (default: 5) */ + cooldown_minutes: number; +} + +/** + * Manual quota management configuration. + * User-controlled overrides for account selection. + */ +export interface ManualQuotaConfig { + /** User-paused accounts (stored in accounts.json) */ + paused_accounts: string[]; + /** Force use of specific account (overrides auto-selection) */ + forced_default: string | null; + /** Lock to specific tier only */ + tier_lock: string | null; +} + +/** + * Quota management mode. + * - auto: Fully automatic failover based on quota + * - manual: User controls everything, no auto-switching + * - hybrid: Auto-failover with user overrides (default) + */ +export type QuotaManagementMode = 'auto' | 'manual' | 'hybrid'; + +/** + * Quota management configuration section. + * Controls hybrid auto+manual account selection for multi-account setups. + */ +export interface QuotaManagementConfig { + /** Management mode (default: hybrid) */ + mode: QuotaManagementMode; + /** Auto mode settings */ + auto: AutoQuotaConfig; + /** Manual mode settings */ + manual: ManualQuotaConfig; + /** Runtime monitor settings */ + runtime_monitor: RuntimeMonitorConfig; +} + +/** + * Default auto quota configuration. + */ +export const DEFAULT_AUTO_QUOTA_CONFIG: AutoQuotaConfig = { + preflight_check: true, + exhaustion_threshold: 5, + tier_priority: ['ultra', 'pro', 'free'], + cooldown_minutes: 5, +}; + +/** + * Default manual quota configuration. + */ +export const DEFAULT_MANUAL_QUOTA_CONFIG: ManualQuotaConfig = { + paused_accounts: [], + forced_default: null, + tier_lock: null, +}; + +/** + * Default runtime monitor configuration. + */ +export const DEFAULT_RUNTIME_MONITOR_CONFIG: RuntimeMonitorConfig = { + enabled: true, + normal_interval_seconds: 300, + critical_interval_seconds: 60, + warn_threshold: 20, + exhaustion_threshold: 5, + cooldown_minutes: 5, +}; + +/** + * Default quota management configuration. + */ +export const DEFAULT_QUOTA_MANAGEMENT_CONFIG: QuotaManagementConfig = { + mode: 'hybrid', + auto: { ...DEFAULT_AUTO_QUOTA_CONFIG }, + manual: { ...DEFAULT_MANUAL_QUOTA_CONFIG }, + runtime_monitor: { ...DEFAULT_RUNTIME_MONITOR_CONFIG }, +}; diff --git a/src/config/schemas/thinking.ts b/src/config/schemas/thinking.ts new file mode 100644 index 00000000..81951ad2 --- /dev/null +++ b/src/config/schemas/thinking.ts @@ -0,0 +1,66 @@ +/** + * Thinking/reasoning budget configuration types and defaults. + * + * Controls thinking budget injection for CLIProxy providers. + * Version 8+ feature. + */ + +// ============================================================================ +// THINKING CONFIGURATION (v8+) +// ============================================================================ + +/** + * Thinking mode for auto/manual/off control. + * - auto: Apply tier-based defaults (opus→high, sonnet→medium, haiku→low) + * - off: Disable thinking entirely + * - manual: Use explicit override value + */ +export type ThinkingMode = 'auto' | 'off' | 'manual'; + +/** + * Tier-to-thinking level defaults. + * Maps Claude tier names to thinking level names. + */ +export interface ThinkingTierDefaults { + /** Thinking level for opus tier (default: 'high') */ + opus: string; + /** Thinking level for sonnet tier (default: 'medium') */ + sonnet: string; + /** Thinking level for haiku tier (default: 'low') */ + haiku: string; +} + +/** + * Thinking configuration section. + * Controls thinking/reasoning budget injection for CLIProxy providers. + */ +export interface ThinkingConfig { + /** Thinking mode (default: 'auto') */ + mode: ThinkingMode; + /** Manual override value (level name or budget number) */ + override?: string | number; + /** Tier-to-level mapping */ + tier_defaults: ThinkingTierDefaults; + /** Per-provider overrides (e.g., { gemini: { opus: 'high' } }) */ + provider_overrides?: Record>; + /** Show warning when values are clamped (default: true) */ + show_warnings?: boolean; +} + +/** + * Default thinking tier defaults. + */ +export const DEFAULT_THINKING_TIER_DEFAULTS: ThinkingTierDefaults = { + opus: 'high', + sonnet: 'medium', + haiku: 'low', +}; + +/** + * Default thinking configuration. + */ +export const DEFAULT_THINKING_CONFIG: ThinkingConfig = { + mode: 'auto', + tier_defaults: { ...DEFAULT_THINKING_TIER_DEFAULTS }, + show_warnings: true, +}; diff --git a/src/config/schemas/unified-config.ts b/src/config/schemas/unified-config.ts new file mode 100644 index 00000000..78726c46 --- /dev/null +++ b/src/config/schemas/unified-config.ts @@ -0,0 +1,200 @@ +/** + * Main unified configuration interface, factory, and type guard. + * + * The UnifiedConfig type is the root of the entire config.yaml schema. + * This file imports all section types from their respective schema modules. + */ + +import type { AccountConfig, ProfileConfig, DashboardAuthConfig } from './auth'; +import { DEFAULT_DASHBOARD_AUTH_CONFIG } from './auth'; +import type { CLIProxyConfig } from './cliproxy'; +import { CLIPROXY_SUPPORTED_PROVIDERS, DEFAULT_CLIPROXY_SAFETY_CONFIG } from './cliproxy'; +import type { LoggingConfig, PreferencesConfig } from './logging'; +import { DEFAULT_LOGGING_CONFIG } from './logging'; +import type { WebSearchConfig } from './websearch'; +import type { + GlobalEnvConfig, + ContinuityConfig, + CopilotConfig, + CursorConfig, + CliproxyServerConfig, + OpenAICompatProxyConfig, + ImageAnalysisConfig, +} from './providers'; +import { + DEFAULT_COPILOT_CONFIG, + DEFAULT_CURSOR_CONFIG, + DEFAULT_CLIPROXY_SERVER_CONFIG, + DEFAULT_OPENAI_COMPAT_PROXY_CONFIG, + DEFAULT_IMAGE_ANALYSIS_CONFIG, + DEFAULT_GLOBAL_ENV, +} from './providers'; +import { UNIFIED_CONFIG_VERSION } from './version'; +import type { QuotaManagementConfig } from './quota'; +import { DEFAULT_QUOTA_MANAGEMENT_CONFIG } from './quota'; +import type { ThinkingConfig } from './thinking'; +import { DEFAULT_THINKING_CONFIG } from './thinking'; +import type { OfficialChannelsConfig } from './channels'; +import { DEFAULT_OFFICIAL_CHANNELS_CONFIG } from './channels'; +import type { BrowserConfig } from './browser'; +import { DEFAULT_BROWSER_CONFIG } from './browser'; + +/** + * Main unified configuration structure. + * Stored in ~/.ccs/config.yaml + */ +export interface UnifiedConfig { + /** Config version */ + version: number; + /** Flag indicating setup wizard has been completed */ + setup_completed?: boolean; + /** Default profile name to use when none specified */ + default?: string; + /** Account-based profiles (isolated Claude instances) */ + accounts: Record; + /** API-based profiles (env var injection) */ + profiles: Record; + /** CLIProxy configuration */ + cliproxy: CLIProxyConfig; + /** OpenAI-compatible local proxy configuration */ + proxy?: OpenAICompatProxyConfig; + /** CCS-owned structured logging configuration */ + logging?: LoggingConfig; + /** User preferences */ + preferences: PreferencesConfig; + /** WebSearch configuration */ + websearch?: WebSearchConfig; + /** Global environment variables for all non-Claude subscription profiles */ + global_env?: GlobalEnvConfig; + /** Cross-profile continuity inheritance mapping */ + continuity?: ContinuityConfig; + /** Copilot API configuration (GitHub Copilot proxy) */ + copilot?: CopilotConfig; + /** Cursor IDE configuration (Cursor proxy daemon) */ + cursor?: CursorConfig; + /** CLIProxy server configuration for remote/local mode */ + cliproxy_server?: CliproxyServerConfig; + /** Quota management configuration (v7+) */ + quota_management?: QuotaManagementConfig; + /** Thinking/reasoning budget configuration (v8+) */ + thinking?: ThinkingConfig; + /** Official Channels runtime auto-enable preferences (v11+) */ + channels?: OfficialChannelsConfig; + /** Dashboard authentication configuration (optional) */ + dashboard_auth?: DashboardAuthConfig; + /** Browser automation configuration */ + browser?: BrowserConfig; + /** Image analysis configuration (vision via CLIProxy) */ + image_analysis?: ImageAnalysisConfig; +} + +/** + * Create an empty unified config with defaults. + */ +export function createEmptyUnifiedConfig(): UnifiedConfig { + return { + version: UNIFIED_CONFIG_VERSION, + default: undefined, + accounts: {}, + profiles: {}, + cliproxy: { + backend: 'original', + oauth_accounts: {}, + providers: [...CLIPROXY_SUPPORTED_PROVIDERS], + variants: {}, + logging: { + enabled: false, + request_log: false, + }, + safety: { ...DEFAULT_CLIPROXY_SAFETY_CONFIG }, + auto_sync: true, + routing: { + strategy: 'round-robin', + session_affinity: false, + session_affinity_ttl: '1h', + }, + }, + proxy: { + port: DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.port, + profile_ports: { ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.profile_ports }, + routing: { + ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.routing, + }, + }, + logging: { ...DEFAULT_LOGGING_CONFIG }, + preferences: { + theme: 'system', + telemetry: false, + auto_update: true, + }, + websearch: { + enabled: true, + providers: { + exa: { + enabled: false, + max_results: 5, + }, + tavily: { + enabled: false, + max_results: 5, + }, + brave: { + enabled: false, + max_results: 5, + }, + searxng: { + enabled: false, + url: '', + max_results: 5, + }, + duckduckgo: { + enabled: true, + max_results: 5, + }, + gemini: { + enabled: false, + model: 'gemini-2.5-flash', + timeout: 55, + }, + opencode: { + enabled: false, + model: 'opencode/grok-code', + timeout: 90, + }, + grok: { + enabled: false, + timeout: 55, + }, + }, + }, + global_env: { + enabled: true, + env: { ...DEFAULT_GLOBAL_ENV }, + }, + copilot: { ...DEFAULT_COPILOT_CONFIG }, + cursor: { ...DEFAULT_CURSOR_CONFIG }, + cliproxy_server: { ...DEFAULT_CLIPROXY_SERVER_CONFIG }, + quota_management: { ...DEFAULT_QUOTA_MANAGEMENT_CONFIG }, + thinking: { ...DEFAULT_THINKING_CONFIG }, + channels: { ...DEFAULT_OFFICIAL_CHANNELS_CONFIG }, + dashboard_auth: { ...DEFAULT_DASHBOARD_AUTH_CONFIG }, + browser: { + claude: { ...DEFAULT_BROWSER_CONFIG.claude }, + codex: { ...DEFAULT_BROWSER_CONFIG.codex }, + }, + image_analysis: { ...DEFAULT_IMAGE_ANALYSIS_CONFIG }, + }; +} + +/** + * Type guard for UnifiedConfig. + * Relaxed validation: accepts configs with version >= 1 and any subset of sections. + * Missing sections will be filled with defaults during merge. + */ +export function isUnifiedConfig(obj: unknown): obj is UnifiedConfig { + if (typeof obj !== 'object' || obj === null) return false; + const config = obj as Record; + // Only require version to be a number >= 1 (allow future versions) + // Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig + return typeof config.version === 'number' && config.version >= 1; +} diff --git a/src/config/schemas/version.ts b/src/config/schemas/version.ts new file mode 100644 index 00000000..98979314 --- /dev/null +++ b/src/config/schemas/version.ts @@ -0,0 +1,23 @@ +/** + * Unified config version constant. + * + * Central source of truth for the current config schema version. + * Incremented whenever new sections are added to config.yaml. + */ + +/** + * Unified config version. + * Version 2 = YAML unified format + * Version 3 = WebSearch config with model configuration for Gemini/OpenCode + * Version 4 = Copilot API integration (GitHub Copilot proxy) + * Version 5 = Remote proxy configuration (connect to remote CLIProxyAPI) + * Version 6 = Customizable auth tokens (API key and management secret) + * Version 7 = Quota management for hybrid auto+manual account control + * Version 8 = Thinking/reasoning budget configuration + * Version 9 = Real WebSearch backends (DuckDuckGo/Brave) with legacy CLI fallback + * Version 10 = Exa + Tavily WebSearch backends + * Version 11 = Discord Channels runtime auto-enable preferences + * Version 12 = Official Channels multi-provider support (Telegram, Discord, iMessage) + * Version 13 = Browser automation defaults to safe manual/off exposure + */ +export const UNIFIED_CONFIG_VERSION = 13; diff --git a/src/config/schemas/websearch.ts b/src/config/schemas/websearch.ts new file mode 100644 index 00000000..d2714c41 --- /dev/null +++ b/src/config/schemas/websearch.ts @@ -0,0 +1,148 @@ +/** + * WebSearch backend configuration types. + * + * Covers all supported search backends: + * - API-backed: Exa, Tavily, Brave + * - Self-hosted: SearXNG + * - Zero-setup: DuckDuckGo + * - Legacy CLI fallbacks: Gemini, Grok, OpenCode + */ + +/** + * DuckDuckGo WebSearch configuration. + */ +export interface DuckDuckGoWebSearchConfig { + /** Enable DuckDuckGo HTML search fallback (default: true) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Brave WebSearch configuration. + */ +export interface BraveWebSearchConfig { + /** Enable Brave Search when BRAVE_API_KEY is available (default: false) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Exa WebSearch configuration. + */ +export interface ExaWebSearchConfig { + /** Enable Exa Search when EXA_API_KEY is available (default: false) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Tavily WebSearch configuration. + */ +export interface TavilyWebSearchConfig { + /** Enable Tavily Search when TAVILY_API_KEY is available (default: false) */ + enabled?: boolean; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * SearXNG WebSearch configuration. + */ +export interface SearxngWebSearchConfig { + /** Enable SearXNG JSON search backend (default: false) */ + enabled?: boolean; + /** Base SearXNG URL, e.g. https://search.example.com (default: '') */ + url?: string; + /** Number of results to fetch (default: 5) */ + max_results?: number; +} + +/** + * Gemini CLI WebSearch configuration. + */ +export interface GeminiWebSearchConfig { + /** Enable Gemini CLI legacy fallback (default: false) */ + enabled?: boolean; + /** Model to use (default: gemini-2.5-flash) */ + model?: string; + /** Timeout in seconds (default: 55) */ + timeout?: number; +} + +/** + * Grok CLI WebSearch configuration. + */ +export interface GrokWebSearchConfig { + /** Enable Grok CLI legacy fallback (default: false - requires GROK_API_KEY) */ + enabled?: boolean; + /** Timeout in seconds (default: 55) */ + timeout?: number; +} + +/** + * OpenCode CLI WebSearch configuration. + */ +export interface OpenCodeWebSearchConfig { + /** Enable OpenCode CLI legacy fallback (default: false) */ + enabled?: boolean; + /** Model to use (default: opencode/grok-code) */ + model?: string; + /** Timeout in seconds (default: 60) */ + timeout?: number; +} + +/** + * WebSearch providers configuration. + * Uses deterministic search backends first, with optional legacy CLI fallback. + */ +export interface WebSearchProvidersConfig { + /** Exa Search API - API-backed search with strong relevance and content extraction */ + exa?: ExaWebSearchConfig; + /** Tavily Search API - API-backed search optimized for agent/tool usage */ + tavily?: TavilyWebSearchConfig; + /** Brave Search API - higher quality results when BRAVE_API_KEY is set */ + brave?: BraveWebSearchConfig; + /** SearXNG JSON search - self-hosted or public instance backend */ + searxng?: SearxngWebSearchConfig; + /** DuckDuckGo HTML search - zero setup default backend */ + duckduckgo?: DuckDuckGoWebSearchConfig; + /** Gemini CLI - optional legacy LLM fallback */ + gemini?: GeminiWebSearchConfig; + /** Grok CLI - optional legacy LLM fallback */ + grok?: GrokWebSearchConfig; + /** OpenCode - optional legacy LLM fallback */ + opencode?: OpenCodeWebSearchConfig; +} + +/** + * WebSearch configuration. + * Uses deterministic local backends for third-party profiles. + * Legacy AI CLI fallbacks remain available for compatibility only. + */ +export interface WebSearchConfig { + /** Master switch - enable/disable WebSearch (default: true) */ + enabled?: boolean; + /** Individual provider configurations */ + providers?: WebSearchProvidersConfig; + // Legacy fields (deprecated, kept for backwards compatibility) + /** @deprecated Use providers.gemini instead */ + gemini?: { + enabled?: boolean; + timeout?: number; + }; + /** @deprecated Unused */ + mode?: 'sequential' | 'parallel'; + /** @deprecated Unused */ + provider?: 'auto' | 'web-search-prime' | 'brave' | 'tavily'; + /** @deprecated Unused */ + fallback?: boolean; + /** @deprecated Unused */ + webSearchPrimeUrl?: string; + /** @deprecated Unused */ + selectedProviders?: string[]; + /** @deprecated Unused */ + customMcp?: unknown[]; +} diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index ab4eca6c..ec5eef2f 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -7,1122 +7,8 @@ * - *.settings.json (env vars) * * Into a single config.yaml structure. - */ - -import type { TargetType } from '../targets/target-adapter'; -import type { CLIProxyProvider, CliproxyRoutingStrategy } from '../cliproxy/types'; -import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities'; - -/** - * Unified config version. - * Version 2 = YAML unified format - * Version 3 = WebSearch config with model configuration for Gemini/OpenCode - * Version 4 = Copilot API integration (GitHub Copilot proxy) - * Version 5 = Remote proxy configuration (connect to remote CLIProxyAPI) - * Version 6 = Customizable auth tokens (API key and management secret) - * Version 7 = Quota management for hybrid auto+manual account control - * Version 8 = Thinking/reasoning budget configuration - * Version 9 = Real WebSearch backends (DuckDuckGo/Brave) with legacy CLI fallback - * Version 10 = Exa + Tavily WebSearch backends - * Version 11 = Discord Channels runtime auto-enable preferences - * Version 12 = Official Channels multi-provider support (Telegram, Discord, iMessage) - * Version 13 = Browser automation defaults to safe manual/off exposure - */ -export const UNIFIED_CONFIG_VERSION = 13; - -/** - * Supported CLIProxy providers. - * Derived from CLIPROXY_PROVIDER_IDS — single source of truth in provider-capabilities.ts. - */ -export const CLIPROXY_SUPPORTED_PROVIDERS = CLIPROXY_PROVIDER_IDS; - -/** - * Account configuration (formerly in profiles.json). - * Represents an isolated Claude instance via CLAUDE_CONFIG_DIR. - */ -export interface AccountConfig { - /** ISO timestamp when account was created */ - created: string; - /** ISO timestamp of last usage, null if never used */ - last_used: string | null; - /** Context mode for project workspace data */ - context_mode?: 'isolated' | 'shared'; - /** Context-sharing group when context_mode='shared' */ - context_group?: string; - /** Shared continuity depth when context_mode='shared' */ - continuity_mode?: 'standard' | 'deeper'; - /** Bare profile: no shared symlinks (commands, skills, agents, settings.json) */ - bare?: boolean; -} - -/** - * API-based profile configuration. - * Injects environment variables for alternative providers (GLM, Kimi, etc.). * - * Settings are stored in separate *.settings.json files (matching Claude's pattern) - * to allow users to edit them directly without touching config.yaml. + * Types have been reorganized into src/config/schemas/ for maintainability. + * This file re-exports everything for backward compatibility. */ -export interface ProfileConfig { - /** Profile type - currently only 'api' */ - type: 'api'; - /** Path to settings file (e.g., "~/.ccs/glm.settings.json") */ - settings: string; - /** Target CLI to use for this profile (default: 'claude') */ - target?: TargetType; -} - -/** - * CLIProxy OAuth account nickname mapping. - * Maps user-friendly nicknames to email addresses. - */ -export type OAuthAccounts = Record; - -/** - * CLIProxy variant configuration. - * User-defined variants of built-in OAuth providers. - * - * Settings are stored in separate *.settings.json files (matching Claude's pattern) - * to allow users to edit them directly without touching config.yaml. - */ -export interface CLIProxyVariantConfig { - /** Base provider to use */ - provider: CLIProxyProvider; - /** Account nickname (references oauth_accounts) */ - account?: string; - /** Path to settings file (e.g., "~/.ccs/gemini-custom.settings.json") */ - settings?: string; - /** Unique port for variant isolation (8318-8417) */ - port?: number; - /** Per-variant auth override (optional) */ - auth?: CLIProxyAuthConfig; - /** Target CLI to use for this variant (default: 'claude') */ - target?: TargetType; -} - -/** - * Per-tier provider+model mapping for composite variants. - */ -export interface CompositeTierConfig { - /** Provider for this tier */ - provider: CLIProxyProvider; - /** Model ID to use for this tier */ - model: string; - /** Account nickname (optional, references oauth_accounts) */ - account?: string; - /** Fallback provider+model if primary fails */ - fallback?: { - provider: CLIProxyProvider; - model: string; - account?: string; - }; - /** Per-tier thinking budget override (e.g. 'xhigh', 'medium', 'off') */ - thinking?: string; -} - -/** - * Composite variant configuration. - * Mixes different providers per Claude tier (opus, sonnet, haiku) in a single profile. - * Uses CLIProxyAPI root endpoints (/v1/messages) for model-based routing - * instead of provider-specific endpoints (/api/provider/{provider}). - */ -export interface CompositeVariantConfig { - /** Discriminator for composite type */ - type: 'composite'; - /** Which tier ANTHROPIC_MODEL equals (default must be one of the three) */ - default_tier: 'opus' | 'sonnet' | 'haiku'; - /** Per-tier provider+model mapping */ - tiers: { - opus: CompositeTierConfig; - sonnet: CompositeTierConfig; - haiku: CompositeTierConfig; - }; - /** Path to settings file */ - settings?: string; - /** Shared port for the composite profile */ - port?: number; - /** Per-variant auth override (optional) */ - auth?: CLIProxyAuthConfig; - /** Target CLI to use for this composite variant (default: 'claude') */ - target?: TargetType; -} - -/** - * CLIProxy authentication configuration. - * Allows customization of API key and management secret for CLIProxyAPI. - */ -export interface CLIProxyAuthConfig { - /** API key for CCS-managed requests (default: 'ccs-internal-managed') */ - api_key?: string; - /** Management secret for Control Panel login (default: 'ccs') */ - management_secret?: string; -} - -/** - * CLIProxy logging configuration. - * Controls whether CLIProxyAPI writes logs to disk. - * Logs can grow to several GB if left enabled. - */ -export interface CLIProxyLoggingConfig { - /** Enable logging to file (default: false to prevent disk bloat) */ - enabled?: boolean; - /** Enable request logging for debugging (default: false) */ - request_log?: boolean; -} - -/** - * CLIProxy safety configuration. - * Controls high-risk flow safeguards for supported providers. - */ -export interface CLIProxySafetyConfig { - /** Allow skipping AGY responsibility checks and Gemini dashboard typed acknowledgement */ - antigravity_ack_bypass?: boolean; -} - -/** - * Default CLIProxy safety configuration. - */ -export const DEFAULT_CLIPROXY_SAFETY_CONFIG: CLIProxySafetyConfig = { - antigravity_ack_bypass: false, -}; - -/** - * Token refresh configuration. - * Manages background token refresh worker settings. - */ -export interface TokenRefreshSettings { - /** Enable background token refresh (default: false) */ - enabled?: boolean; - /** Refresh check interval in minutes (default: 30) */ - interval_minutes?: number; - /** Preemptive refresh time in minutes (default: 45) */ - preemptive_minutes?: number; - /** Maximum retry attempts per token (default: 3) */ - max_retries?: number; - /** Enable verbose logging (default: false) */ - verbose?: boolean; -} - -export interface CLIProxyRoutingConfig { - /** Credential selection strategy when multiple accounts match */ - strategy?: CliproxyRoutingStrategy; - /** Keep one conversation pinned to the same account when possible */ - session_affinity?: boolean; - /** Go-style duration for session-affinity binding retention */ - session_affinity_ttl?: string; -} - -/** - * CLIProxy configuration section. - */ -export interface CLIProxyConfig { - /** Backend selection: 'original' or 'plus' (default: 'original') */ - backend?: 'original' | 'plus'; - /** Nickname to email mapping for OAuth accounts */ - oauth_accounts: OAuthAccounts; - /** Built-in providers (read-only, for reference) */ - providers: readonly string[]; - /** User-defined provider variants (single-provider or composite) */ - variants: Record; - /** Logging configuration (disabled by default) */ - logging?: CLIProxyLoggingConfig; - /** Safety controls for high-risk provider flows */ - safety?: CLIProxySafetyConfig; - /** Kiro: disable incognito browser mode (use normal browser to save credentials) */ - kiro_no_incognito?: boolean; - /** Global auth configuration for CLIProxyAPI */ - auth?: CLIProxyAuthConfig; - /** Background token refresh worker settings */ - token_refresh?: TokenRefreshSettings; - /** Auto-sync API profiles to local CLIProxy config on settings change (default: true) */ - auto_sync?: boolean; - /** Routing strategy for multi-account CLIProxy selection */ - routing?: CLIProxyRoutingConfig; -} - -export type LoggingLevel = 'error' | 'warn' | 'info' | 'debug'; - -/** - * CCS-owned structured logging configuration. - * Separate from cliproxy.logging, which controls CLIProxy runtime files. - */ -export interface LoggingConfig { - /** Enable CCS-owned structured runtime logging */ - enabled: boolean; - /** Minimum level written to disk */ - level: LoggingLevel; - /** Rotate current log when it reaches this size in MB */ - rotate_mb: number; - /** Keep archived segments for this many days */ - retain_days: number; - /** Redact sensitive values before persistence */ - redact: boolean; - /** In-memory recent event buffer size for dashboard reads */ - live_buffer_size: number; -} - -export const DEFAULT_LOGGING_CONFIG: LoggingConfig = { - enabled: true, - level: 'info', - rotate_mb: 10, - retain_days: 7, - redact: true, - live_buffer_size: 250, -}; - -/** - * User preferences. - */ -export interface PreferencesConfig { - /** UI theme preference */ - theme?: 'light' | 'dark' | 'system'; - /** Enable anonymous telemetry */ - telemetry?: boolean; - /** Enable automatic update checks */ - auto_update?: boolean; -} - -/** - * DuckDuckGo WebSearch configuration. - */ -export interface DuckDuckGoWebSearchConfig { - /** Enable DuckDuckGo HTML search fallback (default: true) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Brave WebSearch configuration. - */ -export interface BraveWebSearchConfig { - /** Enable Brave Search when BRAVE_API_KEY is available (default: false) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Exa WebSearch configuration. - */ -export interface ExaWebSearchConfig { - /** Enable Exa Search when EXA_API_KEY is available (default: false) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Tavily WebSearch configuration. - */ -export interface TavilyWebSearchConfig { - /** Enable Tavily Search when TAVILY_API_KEY is available (default: false) */ - enabled?: boolean; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * SearXNG WebSearch configuration. - */ -export interface SearxngWebSearchConfig { - /** Enable SearXNG JSON search backend (default: false) */ - enabled?: boolean; - /** Base SearXNG URL, e.g. https://search.example.com (default: '') */ - url?: string; - /** Number of results to fetch (default: 5) */ - max_results?: number; -} - -/** - * Gemini CLI WebSearch configuration. - */ -export interface GeminiWebSearchConfig { - /** Enable Gemini CLI legacy fallback (default: false) */ - enabled?: boolean; - /** Model to use (default: gemini-2.5-flash) */ - model?: string; - /** Timeout in seconds (default: 55) */ - timeout?: number; -} - -/** - * Grok CLI WebSearch configuration. - */ -export interface GrokWebSearchConfig { - /** Enable Grok CLI legacy fallback (default: false - requires GROK_API_KEY) */ - enabled?: boolean; - /** Timeout in seconds (default: 55) */ - timeout?: number; -} - -/** - * OpenCode CLI WebSearch configuration. - */ -export interface OpenCodeWebSearchConfig { - /** Enable OpenCode CLI legacy fallback (default: false) */ - enabled?: boolean; - /** Model to use (default: opencode/grok-code) */ - model?: string; - /** Timeout in seconds (default: 60) */ - timeout?: number; -} - -/** - * WebSearch providers configuration. - * Uses deterministic search backends first, with optional legacy CLI fallback. - */ -export interface WebSearchProvidersConfig { - /** Exa Search API - API-backed search with strong relevance and content extraction */ - exa?: ExaWebSearchConfig; - /** Tavily Search API - API-backed search optimized for agent/tool usage */ - tavily?: TavilyWebSearchConfig; - /** Brave Search API - higher quality results when BRAVE_API_KEY is set */ - brave?: BraveWebSearchConfig; - /** SearXNG JSON search - self-hosted or public instance backend */ - searxng?: SearxngWebSearchConfig; - /** DuckDuckGo HTML search - zero setup default backend */ - duckduckgo?: DuckDuckGoWebSearchConfig; - /** Gemini CLI - optional legacy LLM fallback */ - gemini?: GeminiWebSearchConfig; - /** Grok CLI - optional legacy LLM fallback */ - grok?: GrokWebSearchConfig; - /** OpenCode - optional legacy LLM fallback */ - opencode?: OpenCodeWebSearchConfig; -} - -/** - * Copilot API account type. - */ -export type CopilotAccountType = 'individual' | 'business' | 'enterprise'; - -/** - * Copilot API configuration. - * Enables GitHub Copilot subscription usage via copilot-api proxy. - * Strictly opt-in - disabled by default. - * - * !! DISCLAIMER - USE AT YOUR OWN RISK !! - * This uses an UNOFFICIAL reverse-engineered API. - * Excessive usage may trigger GitHub account restrictions. - * CCS provides NO WARRANTY and accepts NO RESPONSIBILITY for any consequences. - */ -export interface CopilotConfig { - /** Enable Copilot integration (default: false) - must be explicitly enabled */ - enabled: boolean; - /** Auto-start copilot-api daemon when using profile (default: false) */ - auto_start: boolean; - /** Port for copilot-api proxy (default: 4141) */ - port: number; - /** GitHub Copilot account type (default: individual) */ - account_type: CopilotAccountType; - /** Rate limit in seconds between requests (null = no limit) */ - rate_limit: number | null; - /** Wait instead of error when rate limit is hit (default: true) */ - wait_on_limit: boolean; - /** Default model ID (e.g., claude-sonnet-4.5) */ - model: string; - /** Model mapping for Claude tiers - maps opus/sonnet/haiku to specific models */ - opus_model?: string; - sonnet_model?: string; - haiku_model?: string; -} - -/** - * Cursor IDE integration configuration. - * Enables Cursor IDE usage via cursor proxy daemon. - */ -export interface CursorConfig { - /** Enable Cursor integration (default: false) */ - enabled: boolean; - /** Port for cursor proxy daemon (default: 20129) */ - port: number; - /** Auto-start daemon when CCS starts (default: false) */ - auto_start: boolean; - /** Enable ghost mode to disable telemetry (default: true) */ - ghost_mode: boolean; - /** Default model ID used by Cursor integration */ - model: string; - /** Optional tier mapping for Claude-compatible model routing */ - opus_model?: string; - /** Optional tier mapping for Claude-compatible model routing */ - sonnet_model?: string; - /** Optional tier mapping for Claude-compatible model routing */ - haiku_model?: string; -} - -/** - * Remote proxy configuration. - * Connect to a remote CLIProxyAPI instance instead of spawning local binary. - */ -export interface ProxyRemoteConfig { - /** Enable remote proxy mode (default: false = local mode) */ - enabled: boolean; - /** Remote proxy hostname or IP (empty = not configured) */ - host: string; - /** - * Remote proxy port. - * Optional - defaults based on protocol: - * - HTTPS: 443 - * - HTTP: 8317 - * When empty/undefined, uses protocol default. - */ - port?: number; - /** Protocol for remote connection */ - protocol: 'http' | 'https'; - /** Auth token for remote proxy API endpoints (optional, sent as header) */ - auth_token: string; - /** - * Management key for remote proxy management API endpoints. - * CLIProxyAPI uses separate authentication for management endpoints - * (/v0/management/*) via 'secret-key' config. - * If not set, falls back to auth_token for backwards compatibility. - */ - management_key?: string; - /** Connection timeout in milliseconds (default: 2000) */ - timeout?: number; - /** Enable auto-sync profiles to remote on settings change (default: false) */ - auto_sync?: boolean; -} - -/** - * Fallback configuration when remote proxy is unreachable. - */ -export interface ProxyFallbackConfig { - /** Enable fallback to local proxy (default: true) */ - enabled: boolean; - /** Auto-start local proxy without prompting (default: false = prompt user) */ - auto_start: boolean; -} - -/** - * Local proxy configuration. - */ -export interface ProxyLocalConfig { - /** Local proxy port (default: 8317) */ - port: number; - /** Auto-start local binary (default: true) */ - auto_start: boolean; -} - -export interface OpenAICompatProxyRoutingConfig { - default?: string; - background?: string; - think?: string; - longContext?: string; - webSearch?: string; - longContextThreshold?: number; -} - -export interface OpenAICompatProxyConfig { - /** Default local port for OpenAI-compatible proxy instances */ - port?: number; - /** Optional profile-scoped local port overrides */ - profile_ports?: Record; - routing?: OpenAICompatProxyRoutingConfig; -} - -/** - * CLIProxy server configuration section. - * Controls whether CCS uses local or remote CLIProxyAPI instance. - */ -export interface CliproxyServerConfig { - /** Remote proxy settings */ - remote: ProxyRemoteConfig; - /** Fallback behavior when remote is unreachable */ - fallback: ProxyFallbackConfig; - /** Local proxy settings */ - local: ProxyLocalConfig; -} - -/** - * Global environment variables configuration. - * These env vars are injected into ALL non-Claude subscription profiles. - * Useful for disabling telemetry, bug commands, error reporting, etc. - */ -export interface GlobalEnvConfig { - /** Enable global env injection (default: true) */ - enabled: boolean; - /** Environment variables to inject */ - env: Record; -} - -/** - * Cross-profile continuity inheritance configuration. - * Maps execution profile names to source account profiles for CLAUDE_CONFIG_DIR reuse. - */ -export interface ContinuityConfig { - /** Profile name -> source account profile name */ - inherit_from_account?: Record; -} - -/** - * Default global env vars for third-party profiles. - * These disable Claude Code telemetry/reporting since we're using proxy. - */ -export const DEFAULT_GLOBAL_ENV: Record = { - DISABLE_BUG_COMMAND: '1', - DISABLE_ERROR_REPORTING: '1', - DISABLE_TELEMETRY: '1', -}; - -/** - * WebSearch configuration. - * Uses deterministic local backends for third-party profiles. - * Legacy AI CLI fallbacks remain available for compatibility only. - */ -export interface WebSearchConfig { - /** Master switch - enable/disable WebSearch (default: true) */ - enabled?: boolean; - /** Individual provider configurations */ - providers?: WebSearchProvidersConfig; - // Legacy fields (deprecated, kept for backwards compatibility) - /** @deprecated Use providers.gemini instead */ - gemini?: { - enabled?: boolean; - timeout?: number; - }; - /** @deprecated Unused */ - mode?: 'sequential' | 'parallel'; - /** @deprecated Unused */ - provider?: 'auto' | 'web-search-prime' | 'brave' | 'tavily'; - /** @deprecated Unused */ - fallback?: boolean; - /** @deprecated Unused */ - webSearchPrimeUrl?: string; - /** @deprecated Unused */ - selectedProviders?: string[]; - /** @deprecated Unused */ - customMcp?: unknown[]; -} - -// ============================================================================ -// QUOTA MANAGEMENT CONFIGURATION (v7+) -// ============================================================================ - -/** - * Auto quota management configuration. - * Controls automatic failover behavior. - */ -export interface AutoQuotaConfig { - /** Enable pre-flight quota check before requests (default: true) */ - preflight_check: boolean; - /** Quota percentage below which account is "exhausted" (default: 5) */ - exhaustion_threshold: number; - /** Tier priority for failover, highest to lowest (default: ['paid']) */ - tier_priority: string[]; - /** Minutes to skip exhausted account before retry (default: 5) */ - cooldown_minutes: number; -} - -/** - * Runtime quota monitor configuration. - * Controls adaptive polling during active sessions. - */ -export interface RuntimeMonitorConfig { - /** Enable runtime monitoring during sessions (default: true) */ - enabled: boolean; - /** Poll interval in seconds when quota > warn_threshold (default: 300) */ - normal_interval_seconds: number; - /** Poll interval in seconds when quota <= warn_threshold (default: 60) */ - critical_interval_seconds: number; - /** Quota percentage that triggers fast polling + warning (default: 20) */ - warn_threshold: number; - /** Quota percentage that triggers cooldown + switch (default: 5) */ - exhaustion_threshold: number; - /** Minutes to cooldown exhausted account (default: 5) */ - cooldown_minutes: number; -} - -/** - * Manual quota management configuration. - * User-controlled overrides for account selection. - */ -export interface ManualQuotaConfig { - /** User-paused accounts (stored in accounts.json) */ - paused_accounts: string[]; - /** Force use of specific account (overrides auto-selection) */ - forced_default: string | null; - /** Lock to specific tier only */ - tier_lock: string | null; -} - -/** - * Quota management mode. - * - auto: Fully automatic failover based on quota - * - manual: User controls everything, no auto-switching - * - hybrid: Auto-failover with user overrides (default) - */ -export type QuotaManagementMode = 'auto' | 'manual' | 'hybrid'; - -/** - * Quota management configuration section. - * Controls hybrid auto+manual account selection for multi-account setups. - */ -export interface QuotaManagementConfig { - /** Management mode (default: hybrid) */ - mode: QuotaManagementMode; - /** Auto mode settings */ - auto: AutoQuotaConfig; - /** Manual mode settings */ - manual: ManualQuotaConfig; - /** Runtime monitor settings */ - runtime_monitor: RuntimeMonitorConfig; -} - -/** - * Default auto quota configuration. - */ -export const DEFAULT_AUTO_QUOTA_CONFIG: AutoQuotaConfig = { - preflight_check: true, - exhaustion_threshold: 5, - tier_priority: ['ultra', 'pro', 'free'], - cooldown_minutes: 5, -}; - -/** - * Default manual quota configuration. - */ -export const DEFAULT_MANUAL_QUOTA_CONFIG: ManualQuotaConfig = { - paused_accounts: [], - forced_default: null, - tier_lock: null, -}; - -/** - * Default runtime monitor configuration. - */ -export const DEFAULT_RUNTIME_MONITOR_CONFIG: RuntimeMonitorConfig = { - enabled: true, - normal_interval_seconds: 300, - critical_interval_seconds: 60, - warn_threshold: 20, - exhaustion_threshold: 5, - cooldown_minutes: 5, -}; - -/** - * Default quota management configuration. - */ -export const DEFAULT_QUOTA_MANAGEMENT_CONFIG: QuotaManagementConfig = { - mode: 'hybrid', - auto: { ...DEFAULT_AUTO_QUOTA_CONFIG }, - manual: { ...DEFAULT_MANUAL_QUOTA_CONFIG }, - runtime_monitor: { ...DEFAULT_RUNTIME_MONITOR_CONFIG }, -}; - -// ============================================================================ -// THINKING CONFIGURATION (v8+) -// ============================================================================ - -/** - * Thinking mode for auto/manual/off control. - * - auto: Apply tier-based defaults (opus→high, sonnet→medium, haiku→low) - * - off: Disable thinking entirely - * - manual: Use explicit override value - */ -export type ThinkingMode = 'auto' | 'off' | 'manual'; - -/** - * Tier-to-thinking level defaults. - * Maps Claude tier names to thinking level names. - */ -export interface ThinkingTierDefaults { - /** Thinking level for opus tier (default: 'high') */ - opus: string; - /** Thinking level for sonnet tier (default: 'medium') */ - sonnet: string; - /** Thinking level for haiku tier (default: 'low') */ - haiku: string; -} - -/** - * Thinking configuration section. - * Controls thinking/reasoning budget injection for CLIProxy providers. - */ -export interface ThinkingConfig { - /** Thinking mode (default: 'auto') */ - mode: ThinkingMode; - /** Manual override value (level name or budget number) */ - override?: string | number; - /** Tier-to-level mapping */ - tier_defaults: ThinkingTierDefaults; - /** Per-provider overrides (e.g., { gemini: { opus: 'high' } }) */ - provider_overrides?: Record>; - /** Show warning when values are clamped (default: true) */ - show_warnings?: boolean; -} - -/** - * Default thinking tier defaults. - */ -export const DEFAULT_THINKING_TIER_DEFAULTS: ThinkingTierDefaults = { - opus: 'high', - sonnet: 'medium', - haiku: 'low', -}; - -/** - * Default thinking configuration. - */ -export const DEFAULT_THINKING_CONFIG: ThinkingConfig = { - mode: 'auto', - tier_defaults: { ...DEFAULT_THINKING_TIER_DEFAULTS }, - show_warnings: true, -}; - -/** - * Supported Anthropic official channel IDs. - */ -export type OfficialChannelId = 'telegram' | 'discord' | 'imessage'; - -/** - * Official Channels configuration. - * Controls runtime-only injection of Anthropic's official channel plugins. - */ -export interface OfficialChannelsConfig { - /** Selected official channels to auto-enable for compatible sessions */ - selected: OfficialChannelId[]; - /** Also add --dangerously-skip-permissions when auto-enable is active */ - unattended: boolean; -} - -/** - * Default Official Channels configuration. - * Disabled by default because the feature requires explicit user setup. - */ -export const DEFAULT_OFFICIAL_CHANNELS_CONFIG: OfficialChannelsConfig = { - selected: [], - unattended: false, -}; - -/** - * Dashboard authentication configuration. - * Optional login protection for CCS dashboard. - * Disabled by default for backward compatibility. - */ -export interface DashboardAuthConfig { - /** Enable dashboard authentication (default: false) */ - enabled: boolean; - /** Username for dashboard login */ - username: string; - /** Bcrypt-hashed password (use: npx bcrypt-cli hash 'password') */ - password_hash: string; - /** Session timeout in hours (default: 24) */ - session_timeout_hours?: number; -} - -/** - * Default dashboard auth configuration. - * Disabled by default - must be explicitly enabled. - */ -export const DEFAULT_DASHBOARD_AUTH_CONFIG: DashboardAuthConfig = { - enabled: false, - username: '', - password_hash: '', - session_timeout_hours: 24, -}; - -/** - * Browser automation configuration. - * Controls Claude browser attach and Codex browser tooling. - */ -export type BrowserToolPolicy = 'auto' | 'manual'; -export type BrowserEvalMode = 'disabled' | 'readonly' | 'readwrite'; - -export interface BrowserClaudeConfig { - /** Enable Claude browser attach (default: false) */ - enabled: boolean; - /** Control whether Claude browser attach is exposed automatically or only via --browser */ - policy: BrowserToolPolicy; - /** Chrome user-data directory used for attach mode */ - user_data_dir: string; - /** DevTools port used for attach mode (default: 9222) */ - devtools_port: number; - /** Eval access mode exposed through browser settings/status surfaces */ - eval_mode?: BrowserEvalMode; -} - -export interface BrowserCodexConfig { - /** Enable Codex browser tooling injection (default: false) */ - enabled: boolean; - /** Control whether Codex browser tooling is exposed automatically or only via --browser */ - policy: BrowserToolPolicy; - /** Eval access mode exposed through browser settings/status surfaces */ - eval_mode?: BrowserEvalMode; -} - -export interface BrowserConfig { - claude: BrowserClaudeConfig; - codex: BrowserCodexConfig; -} - -export const DEFAULT_BROWSER_CONFIG: BrowserConfig = { - claude: { - enabled: false, - policy: 'manual', - user_data_dir: '', - devtools_port: 9222, - eval_mode: 'readonly', - }, - codex: { - enabled: false, - policy: 'manual', - eval_mode: 'readonly', - }, -}; - -/** - * Image analysis configuration. - * Routes image/PDF files through CLIProxy for vision analysis. - */ -export interface ImageAnalysisConfig { - /** Enable image analysis via CLIProxy (default: true) */ - enabled: boolean; - /** Timeout in seconds (default: 60) */ - timeout: number; - /** Provider-to-model mapping for vision analysis */ - provider_models: Record; - /** Fallback backend used when a profile does not resolve to a provider-specific backend */ - fallback_backend?: string; - /** Explicit profile-name-to-backend overrides for settings/custom aliases */ - profile_backends?: Record; -} - -/** - * Default image analysis configuration. - * Enabled by default for CLIProxy providers with vision support. - */ -export const DEFAULT_IMAGE_ANALYSIS_CONFIG: ImageAnalysisConfig = { - enabled: true, - timeout: 60, - provider_models: { - agy: 'gemini-3-1-flash-preview', - gemini: 'gemini-3-flash-preview', - codex: 'gpt-5.1-codex-mini', - kiro: 'kiro-claude-haiku-4-5', - ghcp: 'claude-haiku-4.5', - claude: 'claude-haiku-4-5-20251001', - // 'vision-model' is a generic placeholder - users can override via config.yaml - qwen: 'vision-model', - iflow: 'qwen3-vl-plus', - kimi: 'vision-model', - }, - fallback_backend: 'gemini', - profile_backends: {}, -}; - -/** - * Main unified configuration structure. - * Stored in ~/.ccs/config.yaml - */ -export interface UnifiedConfig { - /** Config version (7 for quota management) */ - version: number; - /** Flag indicating setup wizard has been completed */ - setup_completed?: boolean; - /** Default profile name to use when none specified */ - default?: string; - /** Account-based profiles (isolated Claude instances) */ - accounts: Record; - /** API-based profiles (env var injection) */ - profiles: Record; - /** CLIProxy configuration */ - cliproxy: CLIProxyConfig; - /** OpenAI-compatible local proxy configuration */ - proxy?: OpenAICompatProxyConfig; - /** CCS-owned structured logging configuration */ - logging?: LoggingConfig; - /** User preferences */ - preferences: PreferencesConfig; - /** WebSearch configuration */ - websearch?: WebSearchConfig; - /** Global environment variables for all non-Claude subscription profiles */ - global_env?: GlobalEnvConfig; - /** Cross-profile continuity inheritance mapping */ - continuity?: ContinuityConfig; - /** Copilot API configuration (GitHub Copilot proxy) */ - copilot?: CopilotConfig; - /** Cursor IDE configuration (Cursor proxy daemon) */ - cursor?: CursorConfig; - /** CLIProxy server configuration for remote/local mode */ - cliproxy_server?: CliproxyServerConfig; - /** Quota management configuration (v7+) */ - quota_management?: QuotaManagementConfig; - /** Thinking/reasoning budget configuration (v8+) */ - thinking?: ThinkingConfig; - /** Discord Channels runtime auto-enable preferences (v11+) */ - channels?: OfficialChannelsConfig; - /** Dashboard authentication configuration (optional) */ - dashboard_auth?: DashboardAuthConfig; - /** Browser automation configuration */ - browser?: BrowserConfig; - /** Image analysis configuration (vision via CLIProxy) */ - image_analysis?: ImageAnalysisConfig; -} - -/** - * Default Copilot configuration. - * Strictly opt-in - disabled by default. - * Uses gpt-4.1 as default model (free tier compatible). - */ -export const DEFAULT_COPILOT_CONFIG: CopilotConfig = { - enabled: false, - auto_start: false, - port: 4141, - account_type: 'individual', - rate_limit: null, - wait_on_limit: true, - model: 'gpt-4.1', // Free tier compatible -}; - -/** - * Default Cursor configuration. - * Disabled by default, ghost mode enabled for privacy. - */ -export const DEFAULT_CURSOR_CONFIG: CursorConfig = { - enabled: false, - port: 20129, - auto_start: false, - ghost_mode: true, - model: 'gpt-5.3-codex', -}; - -/** - * Default CLIProxy server configuration. - * Local mode by default - remote must be explicitly enabled. - * Port is optional for remote - defaults based on protocol. - */ -export const DEFAULT_CLIPROXY_SERVER_CONFIG: CliproxyServerConfig = { - remote: { - enabled: false, - host: '', - // port is intentionally omitted - will use protocol default (443 for HTTPS, 8317 for HTTP) - protocol: 'http', - auth_token: '', - }, - fallback: { - enabled: true, - auto_start: false, - }, - local: { - port: 8317, - auto_start: true, - }, -}; - -export const DEFAULT_OPENAI_COMPAT_PROXY_CONFIG: OpenAICompatProxyConfig = { - profile_ports: {}, - routing: { - longContextThreshold: 60_000, - }, -}; - -/** - * Create an empty unified config with defaults. - */ -export function createEmptyUnifiedConfig(): UnifiedConfig { - return { - version: UNIFIED_CONFIG_VERSION, - default: undefined, - accounts: {}, - profiles: {}, - cliproxy: { - backend: 'original', - oauth_accounts: {}, - providers: [...CLIPROXY_SUPPORTED_PROVIDERS], - variants: {}, - logging: { - enabled: false, - request_log: false, - }, - safety: { ...DEFAULT_CLIPROXY_SAFETY_CONFIG }, - auto_sync: true, - routing: { - strategy: 'round-robin', - session_affinity: false, - session_affinity_ttl: '1h', - }, - }, - proxy: { - port: DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.port, - profile_ports: { ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.profile_ports }, - routing: { - ...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.routing, - }, - }, - logging: { ...DEFAULT_LOGGING_CONFIG }, - preferences: { - theme: 'system', - telemetry: false, - auto_update: true, - }, - websearch: { - enabled: true, - providers: { - exa: { - enabled: false, - max_results: 5, - }, - tavily: { - enabled: false, - max_results: 5, - }, - brave: { - enabled: false, - max_results: 5, - }, - searxng: { - enabled: false, - url: '', - max_results: 5, - }, - duckduckgo: { - enabled: true, - max_results: 5, - }, - gemini: { - enabled: false, - model: 'gemini-2.5-flash', - timeout: 55, - }, - opencode: { - enabled: false, - model: 'opencode/grok-code', - timeout: 90, - }, - grok: { - enabled: false, - timeout: 55, - }, - }, - }, - global_env: { - enabled: true, - env: { ...DEFAULT_GLOBAL_ENV }, - }, - copilot: { ...DEFAULT_COPILOT_CONFIG }, - cursor: { ...DEFAULT_CURSOR_CONFIG }, - cliproxy_server: { ...DEFAULT_CLIPROXY_SERVER_CONFIG }, - quota_management: { ...DEFAULT_QUOTA_MANAGEMENT_CONFIG }, - thinking: { ...DEFAULT_THINKING_CONFIG }, - channels: { ...DEFAULT_OFFICIAL_CHANNELS_CONFIG }, - dashboard_auth: { ...DEFAULT_DASHBOARD_AUTH_CONFIG }, - browser: { - claude: { ...DEFAULT_BROWSER_CONFIG.claude }, - codex: { ...DEFAULT_BROWSER_CONFIG.codex }, - }, - image_analysis: { ...DEFAULT_IMAGE_ANALYSIS_CONFIG }, - }; -} - -/** - * Type guard for UnifiedConfig. - * Relaxed validation: accepts configs with version >= 1 and any subset of sections. - * Missing sections will be filled with defaults during merge. - */ -export function isUnifiedConfig(obj: unknown): obj is UnifiedConfig { - if (typeof obj !== 'object' || obj === null) return false; - const config = obj as Record; - // Only require version to be a number >= 1 (allow future versions) - // Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig - return typeof config.version === 'number' && config.version >= 1; -} +export * from './schemas/index'; From 9bb1bdbad960140db177e1cec6a30ea56b97263e Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 14:37:28 -0400 Subject: [PATCH 07/12] =?UTF-8?q?fix:=20address=20red-team=20review=20find?= =?UTF-8?q?ings=20=E2=80=94=20cache=20aliasing,=20jitter=20cap,=20cause=20?= =?UTF-8?q?shadowing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config-loader-facade: use structuredClone() to prevent cache aliasing - retry-strategy: re-cap delay after jitter to enforce maxDelayMs boundary - retry-strategy: wire retryAfter from RetryableError into delay computation - retry-strategy: guard against negative maxRetries - error-types: rename RetryableError.cause to originalError to avoid shadowing Error.cause - Tests updated for all fixes --- .../__tests__/config-loader-facade.test.ts | 18 +++++++---- src/config/config-loader-facade.ts | 16 ++++++---- src/errors/__tests__/error-types.test.ts | 8 ++--- src/errors/error-types.ts | 2 +- src/utils/__tests__/retry-strategy.test.ts | 32 +++++++++++++++++-- src/utils/retry-strategy.ts | 17 ++++++++-- 6 files changed, 70 insertions(+), 23 deletions(-) diff --git a/src/config/__tests__/config-loader-facade.test.ts b/src/config/__tests__/config-loader-facade.test.ts index f780bcc8..aff186a5 100644 --- a/src/config/__tests__/config-loader-facade.test.ts +++ b/src/config/__tests__/config-loader-facade.test.ts @@ -116,14 +116,15 @@ describe('config-loader-facade', () => { }); describe('memoization', () => { - it('getCachedConfig returns same object on repeated calls', async () => { + it('getCachedConfig returns equivalent config on repeated calls', async () => { const facade = await importFacade(); const first = facade.getCachedConfig(); const second = facade.getCachedConfig(); - // Same reference (cached, not re-read) - expect(first).toBe(second); + // Deep copies — different references but same content (no re-read from disk) + expect(first).not.toBe(second); + expect(first.version).toBe(second.version); }); it('invalidateConfigCache forces re-read on next getCachedConfig', async () => { @@ -139,16 +140,21 @@ describe('config-loader-facade', () => { expect(first.version).toBe(second.version); }); - it('saveConfig updates cache and does not invalidate', async () => { + it('saveConfig persists to disk and updates cache', async () => { const facade = await importFacade(); const config = facade.getCachedConfig(); config.default = 'test-profile'; facade.saveConfig(config); + // Verify disk content reflects the save + const configPath = path.join(tempHome, '.ccs', 'config.yaml'); + const diskContent = fs.readFileSync(configPath, 'utf8'); + const diskConfig = yaml.load(diskContent) as Record; + expect(diskConfig.default).toBe('test-profile'); + const cached = facade.getCachedConfig(); - // Cache should hold the just-saved config (no re-read) - expect(cached).toBe(config); + // Cache holds a copy of the saved config expect(cached.default).toBe('test-profile'); }); diff --git a/src/config/config-loader-facade.ts b/src/config/config-loader-facade.ts index 40b526fc..b3fee310 100644 --- a/src/config/config-loader-facade.ts +++ b/src/config/config-loader-facade.ts @@ -62,7 +62,11 @@ let _configCache: UnifiedConfig | null = null; /** * Get the unified config with in-memory caching. - * First call reads from disk; subsequent calls return the cached object. + * First call reads from disk; subsequent calls return a deep copy. + * Returns a copy to prevent callers from silently mutating the cache. + * + * NOTE: `loadOrCreateUnifiedConfig` (re-exported above) is NOT cached. + * Use `getCachedConfig()` for cached reads, or the direct import for uncached. * * Call invalidateConfigCache() or use mutateConfig()/updateConfig() * to force a re-read from disk. @@ -71,7 +75,7 @@ export function getCachedConfig(): UnifiedConfig { if (!_configCache) { _configCache = _loadOrCreateUnifiedConfig(); } - return _configCache; + return structuredClone(_configCache); } /** @@ -83,17 +87,17 @@ export function invalidateConfigCache(): void { } /** - * Save config to disk and update the cache to the given object. - * Does NOT invalidate — the provided config IS the new cache value. + * Save config to disk and update the cache. + * Stores a deep copy to break the reference alias. */ export function saveConfig(config: UnifiedConfig): void { _saveUnifiedConfig(config); - _configCache = config; + _configCache = structuredClone(config); } /** * Atomically mutate config (read-modify-write with lock) and invalidate cache. - * After mutation, the next getCachedConfig() call will re-read from disk. + * Invalidated AFTER _mutateUnifiedConfig returns — if that throws, cache stays valid. */ export function mutateConfig(mutator: (config: UnifiedConfig) => void): UnifiedConfig { const result = _mutateUnifiedConfig(mutator); diff --git a/src/errors/__tests__/error-types.test.ts b/src/errors/__tests__/error-types.test.ts index 0a28b240..b17298d6 100644 --- a/src/errors/__tests__/error-types.test.ts +++ b/src/errors/__tests__/error-types.test.ts @@ -29,10 +29,10 @@ describe('RetryableError', () => { expect(err.message).toBe('something went wrong'); }); - it('accepts an optional cause', () => { - const cause = new Error('original'); - const err = new RetryableError('wrapped', cause); - expect(err.cause).toBe(cause); + it('accepts an optional originalError', () => { + const original = new Error('original'); + const err = new RetryableError('wrapped', original); + expect(err.originalError).toBe(original); }); it('accepts an optional retryAfter (ms)', () => { diff --git a/src/errors/error-types.ts b/src/errors/error-types.ts index c0b8a9b2..2c288426 100644 --- a/src/errors/error-types.ts +++ b/src/errors/error-types.ts @@ -176,7 +176,7 @@ export class ValidationError extends CCSError { export class RetryableError extends CCSError { constructor( message: string, - public readonly cause?: Error, + public readonly originalError?: Error, public readonly retryAfter?: number // ms until next attempt ) { super(message, ExitCode.GENERAL_ERROR, true); diff --git a/src/utils/__tests__/retry-strategy.test.ts b/src/utils/__tests__/retry-strategy.test.ts index e22693ef..372babf5 100644 --- a/src/utils/__tests__/retry-strategy.test.ts +++ b/src/utils/__tests__/retry-strategy.test.ts @@ -95,11 +95,11 @@ describe('withRetry', () => { maxDelayMs: 200, }); - // Verify setTimeout was called with delay <= maxDelayMs (200ms) + jitter buffer - // Jitter adds 0-20% of delay, so max possible is 240ms + // Verify setTimeout was called with delay <= maxDelayMs (200ms) + // Jitter is capped, so delay must never exceed 200ms for (const call of sleepSpy.mock.calls) { const delay = call[1] as number; - expect(delay).toBeLessThanOrEqual(250); // allow small jitter overhead + expect(delay).toBeLessThanOrEqual(200); } sleepSpy.mockRestore(); }); @@ -158,4 +158,30 @@ describe('withRetry', () => { await expect(withRetry(fn, { maxRetries: 0, baseDelayMs: 1 })).rejects.toThrow('fail'); expect(fn).toHaveBeenCalledTimes(1); }); + + it('throws on negative maxRetries', async () => { + const fn = mock(() => Promise.resolve('ok')); + await expect(withRetry(fn, { maxRetries: -1, baseDelayMs: 1 })).rejects.toThrow( + 'maxRetries must be >= 0' + ); + expect(fn).not.toHaveBeenCalled(); + }); + + it('respects retryAfter from RetryableError', async () => { + const sleepSpy = spyOn(globalThis, 'setTimeout'); + let attempt = 0; + const fn = mock(() => { + attempt++; + if (attempt < 2) { + return Promise.reject(new RetryableError('rate limited', undefined, 500)); + } + return Promise.resolve('ok'); + }); + + await withRetry(fn, { maxRetries: 3, baseDelayMs: 10, maxDelayMs: 1000 }); + // retryAfter=500 should override the computed backoff (~10ms) since 500 > 10 + const delay = sleepSpy.mock.calls[0][1] as number; + expect(delay).toBeGreaterThanOrEqual(500); + sleepSpy.mockRestore(); + }); }); diff --git a/src/utils/retry-strategy.ts b/src/utils/retry-strategy.ts index 988f9216..6e778862 100644 --- a/src/utils/retry-strategy.ts +++ b/src/utils/retry-strategy.ts @@ -46,16 +46,22 @@ function defaultRetryableCheck(error: unknown): boolean { /** * Compute backoff delay: base * multiplier^attempt + jitter, capped at maxDelay. + * Jitter is applied before the final cap to ensure the result never exceeds maxDelayMs. */ function computeDelay( attempt: number, baseDelayMs: number, maxDelayMs: number, - multiplier: number + multiplier: number, + retryAfter?: number ): number { const exponentialDelay = Math.min(baseDelayMs * Math.pow(multiplier, attempt), maxDelayMs); const jitter = exponentialDelay * JITTER_RATIO * Math.random(); - return exponentialDelay + jitter; + const delay = Math.min(exponentialDelay + jitter, maxDelayMs); + if (retryAfter !== undefined && retryAfter > 0) { + return Math.max(delay, retryAfter); + } + return delay; } /** @@ -86,6 +92,10 @@ export async function withRetry(fn: () => Promise, options: RetryOptions): onRetry, } = options; + if (maxRetries < 0) { + throw new Error('withRetry: maxRetries must be >= 0'); + } + const isRetryable = retryableCheck ?? defaultRetryableCheck; let lastError: unknown; @@ -108,7 +118,8 @@ export async function withRetry(fn: () => Promise, options: RetryOptions): const err = error instanceof Error ? error : new Error(String(error)); onRetry?.(err, attempt + 1); - const delay = computeDelay(attempt, baseDelayMs, maxDelayMs, backoffMultiplier); + const retryAfter = error instanceof RetryableError ? error.retryAfter : undefined; + const delay = computeDelay(attempt, baseDelayMs, maxDelayMs, backoffMultiplier, retryAfter); await sleep(delay); } } From 18e865ea364de0f7c811d447bacaf644d98f10d2 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 14:59:44 -0400 Subject: [PATCH 08/12] =?UTF-8?q?fix:=20round=202=20red-team=20fixes=20?= =?UTF-8?q?=E2=80=94=20onRetry=20safety,=20validation,=20barrel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - retry-strategy: wrap onRetry in try/catch to prevent callback errors from aborting retries - retry-strategy: validate baseDelayMs >= 0 - retry-strategy: update JSDoc to clarify retryAfter/maxDelayMs interaction - errors/index.ts: add ValidationError to barrel - Tests: onRetry throw test, negative baseDelayMs test --- src/errors/index.ts | 1 + src/utils/__tests__/retry-strategy.test.ts | 27 ++++++++++++++++++++++ src/utils/retry-strategy.ts | 15 ++++++++---- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/errors/index.ts b/src/errors/index.ts index 45e89b7d..8d76762e 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -37,6 +37,7 @@ export { ProxyError, MigrationError, UserAbortError, + ValidationError, RetryableError, isCCSError, isRecoverableError, diff --git a/src/utils/__tests__/retry-strategy.test.ts b/src/utils/__tests__/retry-strategy.test.ts index 372babf5..58dd58e0 100644 --- a/src/utils/__tests__/retry-strategy.test.ts +++ b/src/utils/__tests__/retry-strategy.test.ts @@ -184,4 +184,31 @@ describe('withRetry', () => { expect(delay).toBeGreaterThanOrEqual(500); sleepSpy.mockRestore(); }); + + it('swallows onRetry callback errors and continues retrying', async () => { + let attempt = 0; + const fn = mock(() => { + attempt++; + if (attempt < 3) { + return Promise.reject(new RetryableError('fail')); + } + return Promise.resolve('ok'); + }); + const onRetry = mock(() => { + throw new Error('callback blew up'); + }); + + const result = await withRetry(fn, { maxRetries: 5, baseDelayMs: 1, onRetry }); + expect(result).toBe('ok'); + expect(fn).toHaveBeenCalledTimes(3); + expect(onRetry).toHaveBeenCalledTimes(2); + }); + + it('throws on negative baseDelayMs', async () => { + const fn = mock(() => Promise.resolve('ok')); + await expect(withRetry(fn, { maxRetries: 3, baseDelayMs: -1 })).rejects.toThrow( + 'baseDelayMs must be >= 0' + ); + expect(fn).not.toHaveBeenCalled(); + }); }); diff --git a/src/utils/retry-strategy.ts b/src/utils/retry-strategy.ts index 6e778862..50489b6b 100644 --- a/src/utils/retry-strategy.ts +++ b/src/utils/retry-strategy.ts @@ -16,13 +16,13 @@ export interface RetryOptions { maxRetries: number; /** Base delay in ms for the first retry (default: 1000) */ baseDelayMs: number; - /** Upper bound for the computed delay (default: 30000) */ + /** Upper bound for the computed backoff delay. Note: server-provided `retryAfter` (from RetryableError) takes precedence and may exceed this cap. */ maxDelayMs?: number; - /** Multiplier applied per attempt (default: 2) */ + /** Multiplier applied per attempt (default: 2). Values <1 produce degrowth. */ backoffMultiplier?: number; /** Override the default retryability check */ retryableCheck?: (error: unknown) => boolean; - /** Callback fired before each retry (not fired on initial call) */ + /** Callback fired before each retry. Errors thrown by this callback are swallowed to prevent aborting the retry loop. */ onRetry?: (error: Error, attempt: number) => void; } @@ -95,6 +95,9 @@ export async function withRetry(fn: () => Promise, options: RetryOptions): if (maxRetries < 0) { throw new Error('withRetry: maxRetries must be >= 0'); } + if (baseDelayMs < 0) { + throw new Error('withRetry: baseDelayMs must be >= 0'); + } const isRetryable = retryableCheck ?? defaultRetryableCheck; let lastError: unknown; @@ -116,7 +119,11 @@ export async function withRetry(fn: () => Promise, options: RetryOptions): } const err = error instanceof Error ? error : new Error(String(error)); - onRetry?.(err, attempt + 1); + try { + onRetry?.(err, attempt + 1); + } catch { + // Swallow callback errors — retry decision is already made + } const retryAfter = error instanceof RetryableError ? error.retryAfter : undefined; const delay = computeDelay(attempt, baseDelayMs, maxDelayMs, backoffMultiplier, retryAfter); From b8ed36e3705f9e6795a99864610727026be5fc52 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 15:16:07 -0400 Subject: [PATCH 09/12] =?UTF-8?q?fix:=20round=203=20red-team=20=E2=80=94?= =?UTF-8?q?=20test=20false-positive,=20dead=20code,=20edge-case=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix false-positive test: use CCSError with recoverable=false instead of plain Error (never exercised recoverable check) - Remove redundant retryableCheck ?? defaultRetryableCheck (destructuring already defaults) - Add test: retryAfter > maxDelayMs (server directive wins) - Add test: baseDelayMs=0 produces immediate retries - Add test: onRetry not called when maxRetries=0 --- src/utils/__tests__/retry-strategy.test.ts | 48 ++++++++++++++++++++-- src/utils/retry-strategy.ts | 2 +- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/utils/__tests__/retry-strategy.test.ts b/src/utils/__tests__/retry-strategy.test.ts index 58dd58e0..ad398b81 100644 --- a/src/utils/__tests__/retry-strategy.test.ts +++ b/src/utils/__tests__/retry-strategy.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, mock, spyOn } from 'bun:test'; import { withRetry, type RetryOptions } from '../retry-strategy'; -import { RetryableError } from '../../errors/error-types'; +import { CCSError, RetryableError } from '../../errors/error-types'; describe('withRetry', () => { it('returns the result on first success', async () => { @@ -38,8 +38,8 @@ describe('withRetry', () => { expect(fn).toHaveBeenCalledTimes(1); }); - it('does not retry errors with recoverable=false', async () => { - const fn = mock(() => Promise.reject(new Error('non-retryable'))); + it('does not retry CCSErrors with recoverable=false', async () => { + const fn = mock(() => Promise.reject(new CCSError('non-retryable', 1, false))); await expect(withRetry(fn, { maxRetries: 3, baseDelayMs: 1 })).rejects.toThrow('non-retryable'); expect(fn).toHaveBeenCalledTimes(1); }); @@ -211,4 +211,46 @@ describe('withRetry', () => { ); expect(fn).not.toHaveBeenCalled(); }); + + it('retryAfter can exceed maxDelayMs (server directive wins)', async () => { + const sleepSpy = spyOn(globalThis, 'setTimeout'); + let attempt = 0; + const fn = mock(() => { + attempt++; + if (attempt < 2) { + return Promise.reject(new RetryableError('rate limited', undefined, 500)); + } + return Promise.resolve('ok'); + }); + + await withRetry(fn, { maxRetries: 3, baseDelayMs: 10, maxDelayMs: 100 }); + // retryAfter=500 > maxDelayMs=100 — server directive takes precedence + const delay = sleepSpy.mock.calls[0][1] as number; + expect(delay).toBeGreaterThanOrEqual(500); + sleepSpy.mockRestore(); + }); + + it('baseDelayMs=0 produces immediate retries', async () => { + let attempt = 0; + const fn = mock(() => { + attempt++; + if (attempt < 3) { + return Promise.reject(new RetryableError('fail')); + } + return Promise.resolve('ok'); + }); + + const result = await withRetry(fn, { maxRetries: 5, baseDelayMs: 0 }); + expect(result).toBe('ok'); + expect(fn).toHaveBeenCalledTimes(3); + }); + + it('does not call onRetry when maxRetries=0', async () => { + const onRetry = mock(() => {}); + const fn = mock(() => Promise.reject(new RetryableError('fail'))); + + await expect(withRetry(fn, { maxRetries: 0, baseDelayMs: 1, onRetry })).rejects.toThrow('fail'); + expect(fn).toHaveBeenCalledTimes(1); + expect(onRetry).not.toHaveBeenCalled(); + }); }); diff --git a/src/utils/retry-strategy.ts b/src/utils/retry-strategy.ts index 50489b6b..33ebeee3 100644 --- a/src/utils/retry-strategy.ts +++ b/src/utils/retry-strategy.ts @@ -99,7 +99,7 @@ export async function withRetry(fn: () => Promise, options: RetryOptions): throw new Error('withRetry: baseDelayMs must be >= 0'); } - const isRetryable = retryableCheck ?? defaultRetryableCheck; + const isRetryable = retryableCheck; let lastError: unknown; for (let attempt = 0; attempt <= maxRetries; attempt++) { From 6d266fc7e89d7e7ddee3ec4235152e9a9bcaba0d Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 15:37:17 -0400 Subject: [PATCH 10/12] fix: remove raw write re-exports from facade (cache bypass) PR-Agent #1150 review flagged that re-exporting saveUnifiedConfig/mutateUnifiedConfig/updateUnifiedConfig allows callers to bypass the cache. Only export the cache-coherent wrappers (saveConfig/mutateConfig/updateConfig). Raw functions still available via direct import from unified-config-loader if needed. - Remove raw write re-exports from facade - Add test verifying raw writes are NOT exported - Add test verifying cache-coherent wrappers ARE exported --- .../__tests__/config-loader-facade.test.ts | 21 ++++++++++++++++--- src/config/config-loader-facade.ts | 14 +++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/config/__tests__/config-loader-facade.test.ts b/src/config/__tests__/config-loader-facade.test.ts index aff186a5..837ce030 100644 --- a/src/config/__tests__/config-loader-facade.test.ts +++ b/src/config/__tests__/config-loader-facade.test.ts @@ -63,9 +63,6 @@ describe('config-loader-facade', () => { expect(typeof facade.loadUnifiedConfig).toBe('function'); expect(typeof facade.loadOrCreateUnifiedConfig).toBe('function'); - expect(typeof facade.saveUnifiedConfig).toBe('function'); - expect(typeof facade.mutateUnifiedConfig).toBe('function'); - expect(typeof facade.updateUnifiedConfig).toBe('function'); }); it('should export all path/format utilities', async () => { @@ -115,6 +112,24 @@ describe('config-loader-facade', () => { }); }); + describe('cache coherence', () => { + it('should NOT export raw write functions that bypass cache', async () => { + const facade = (await importFacade()) as Record; + + expect(facade.saveUnifiedConfig).toBeUndefined(); + expect(facade.mutateUnifiedConfig).toBeUndefined(); + expect(facade.updateUnifiedConfig).toBeUndefined(); + }); + + it('should export cache-coherent write wrappers instead', async () => { + const facade = await importFacade(); + + expect(typeof facade.saveConfig).toBe('function'); + expect(typeof facade.mutateConfig).toBe('function'); + expect(typeof facade.updateConfig).toBe('function'); + }); + }); + describe('memoization', () => { it('getCachedConfig returns equivalent config on repeated calls', async () => { const facade = await importFacade(); diff --git a/src/config/config-loader-facade.ts b/src/config/config-loader-facade.ts index b3fee310..e6b17ad2 100644 --- a/src/config/config-loader-facade.ts +++ b/src/config/config-loader-facade.ts @@ -2,21 +2,23 @@ * Config Loader Facade * * Single import path for all config loading operations. - * Re-exports everything from unified-config-loader and config-manager, - * and adds memoization for loadOrCreateUnifiedConfig to reduce file I/O. + * Re-exports read-only functions from unified-config-loader and config-manager, + * and provides cache-coherent write wrappers that keep the memoization cache in sync. + * + * IMPORTANT: Raw write functions (saveUnifiedConfig, mutateUnifiedConfig, + * updateUnifiedConfig) are NOT re-exported here. Use the cache-coherent + * wrappers (saveConfig, mutateConfig, updateConfig) instead. If you need + * the raw functions, import directly from './unified-config-loader'. * * Usage: * import { getCachedConfig, saveConfig, mutateConfig } from '../config/config-loader-facade'; * import { getCcsDir, loadSettings } from '../config/config-loader-facade'; */ -// Re-export all functions from unified-config-loader +// Re-export read-only functions from unified-config-loader export { loadUnifiedConfig, loadOrCreateUnifiedConfig, - saveUnifiedConfig, - mutateUnifiedConfig, - updateUnifiedConfig, getConfigYamlPath, getConfigJsonPath, hasUnifiedConfig, From 2290a1dc5a575c0875524571c64ee24968851855 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 15:44:34 -0400 Subject: [PATCH 11/12] docs(config-facade): clarify cache coherence contract Document that uncached reads (loadOrCreateUnifiedConfig) bypass the cache and callers should use invalidateConfigCache() if they mix uncached reads with cached writes outside the facade. Resolves remaining PR-Agent concern from #1150 comment. --- src/config/config-loader-facade.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/config/config-loader-facade.ts b/src/config/config-loader-facade.ts index e6b17ad2..19188ba4 100644 --- a/src/config/config-loader-facade.ts +++ b/src/config/config-loader-facade.ts @@ -5,10 +5,14 @@ * Re-exports read-only functions from unified-config-loader and config-manager, * and provides cache-coherent write wrappers that keep the memoization cache in sync. * - * IMPORTANT: Raw write functions (saveUnifiedConfig, mutateUnifiedConfig, - * updateUnifiedConfig) are NOT re-exported here. Use the cache-coherent - * wrappers (saveConfig, mutateConfig, updateConfig) instead. If you need - * the raw functions, import directly from './unified-config-loader'. + * IMPORTANT: + * - Raw write functions (saveUnifiedConfig, mutateUnifiedConfig, + * updateUnifiedConfig) are NOT re-exported here. Use the cache-coherent + * wrappers (saveConfig, mutateConfig, updateConfig) instead. + * - `loadOrCreateUnifiedConfig` and `loadUnifiedConfig` are re-exported as + * uncached reads. If you need cached reads, use `getCachedConfig()`. + * If you use an uncached read followed by a write outside this facade, + * call `invalidateConfigCache()` to keep the cache coherent. * * Usage: * import { getCachedConfig, saveConfig, mutateConfig } from '../config/config-loader-facade'; From 1e5580a30ae4c2eef64975b688b13440cb2d3b07 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 15:56:50 -0400 Subject: [PATCH 12/12] fix(config-facade): mtime-based staleness detection for cache getCachedConfig() now checks config file mtime on each call. If external code writes via unified-config-loader directly, the facade detects the file change and re-reads from disk automatically. Resolves PR-Agent "Stale Cache" finding. --- .../__tests__/config-loader-facade.test.ts | 22 ++++++++++++++ src/config/config-loader-facade.ts | 29 ++++++++++++------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/config/__tests__/config-loader-facade.test.ts b/src/config/__tests__/config-loader-facade.test.ts index 837ce030..1a9f9f8b 100644 --- a/src/config/__tests__/config-loader-facade.test.ts +++ b/src/config/__tests__/config-loader-facade.test.ts @@ -208,5 +208,27 @@ describe('config-loader-facade', () => { expect(config.profiles).toBeDefined(); expect(config.cliproxy).toBeDefined(); }); + + it('auto-invalidates when config file is modified externally', async () => { + const facade = await importFacade(); + + // Prime the cache + const first = facade.getCachedConfig(); + expect(first.version).toBeDefined(); + + // Modify config file directly on disk (simulating external code + // bypassing the facade) + const configPath = path.join(tempHome, '.ccs', 'config.yaml'); + // Touch the file to update mtime (wait briefly for mtime resolution) + const content = fs.readFileSync(configPath, 'utf8'); + await new Promise((r) => setTimeout(r, 10)); + fs.writeFileSync(configPath, content + '# touched\n', 'utf8'); + + // getCachedConfig should detect the mtime change and re-read + const second = facade.getCachedConfig(); + expect(first).not.toBe(second); + // Content should still be valid (re-read from disk) + expect(second.version).toBeDefined(); + }); }); }); diff --git a/src/config/config-loader-facade.ts b/src/config/config-loader-facade.ts index 19188ba4..462efa37 100644 --- a/src/config/config-loader-facade.ts +++ b/src/config/config-loader-facade.ts @@ -52,34 +52,42 @@ export type { GeminiWebSearchInfo } from './unified-config-loader'; export { loadSettings, loadConfigSafe, readConfig, getCcsDir } from '../utils/config-manager'; // Internal imports for memoization wrappers +import { statSync } from 'fs'; import type { UnifiedConfig } from './unified-config-types'; import { loadOrCreateUnifiedConfig as _loadOrCreateUnifiedConfig, saveUnifiedConfig as _saveUnifiedConfig, mutateUnifiedConfig as _mutateUnifiedConfig, updateUnifiedConfig as _updateUnifiedConfig, + getConfigYamlPath as _getConfigYamlPath, } from './unified-config-loader'; // --------------------------------------------------------------------------- -// Memoization cache +// Memoization cache with mtime-based staleness detection // --------------------------------------------------------------------------- let _configCache: UnifiedConfig | null = null; +let _cacheMtimeMs: number = 0; + +function getConfigFileMtime(): number { + try { + return statSync(_getConfigYamlPath()).mtimeMs; + } catch { + return 0; + } +} /** * Get the unified config with in-memory caching. - * First call reads from disk; subsequent calls return a deep copy. - * Returns a copy to prevent callers from silently mutating the cache. - * - * NOTE: `loadOrCreateUnifiedConfig` (re-exported above) is NOT cached. - * Use `getCachedConfig()` for cached reads, or the direct import for uncached. - * - * Call invalidateConfigCache() or use mutateConfig()/updateConfig() - * to force a re-read from disk. + * Checks file mtime on each call — if the config file was modified + * externally (e.g. by code importing unified-config-loader directly), + * the cache is automatically invalidated and re-read from disk. */ export function getCachedConfig(): UnifiedConfig { - if (!_configCache) { + const currentMtime = getConfigFileMtime(); + if (!_configCache || currentMtime > _cacheMtimeMs) { _configCache = _loadOrCreateUnifiedConfig(); + _cacheMtimeMs = getConfigFileMtime(); } return structuredClone(_configCache); } @@ -90,6 +98,7 @@ export function getCachedConfig(): UnifiedConfig { */ export function invalidateConfigCache(): void { _configCache = null; + _cacheMtimeMs = 0; } /**