refactor: merge facade adoption from #1169 into executor extraction

This commit is contained in:
Tam Nhu Tran
2026-05-03 11:54:04 -04:00
136 changed files with 2035 additions and 1607 deletions
+8 -2
View File
@@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir, loadConfigSafe } from '../../utils/config-manager';
import { buildProxyUrl, getProxyTarget } from '../../cliproxy/proxy/proxy-target-resolver';
import { getEffectiveApiKey } from '../../cliproxy/auth/auth-token-manager';
import { getModelMappingFromConfig } from '../../cliproxy/config/base-config-loader';
@@ -11,7 +11,7 @@ import {
mapExternalProviderName,
} from '../../cliproxy/provider-capabilities';
import { extractProviderFromPathname } from '../../cliproxy/ai-providers/model-id-normalizer';
import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import type { TargetType } from '../../targets/target-adapter';
import type { Settings } from '../../types/config';
import type { CLIProxyProvider } from '../../cliproxy/types';
@@ -21,6 +21,12 @@ import type {
ModelMapping,
ResolvedCliproxyBridgeProfile,
} from './profile-types';
import {
getCcsDir,
isUnifiedMode,
loadConfigSafe,
loadOrCreateUnifiedConfig,
} from '../../config/config-loader-facade';
const DEFAULT_PROFILE_SUFFIX = '-api';
+1 -1
View File
@@ -5,7 +5,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir } from '../../utils/config-manager';
import { getCcsDir } from '../../config/config-loader-facade';
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/models';
function getCacheFile() {
@@ -9,12 +9,12 @@ import * as path from 'path';
import type { Config, Settings } from '../../types';
import type { TargetType } from '../../targets/target-adapter';
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
import { getCcsDir, getConfigPath, loadConfigSafe } from '../../utils/config-manager';
import { getConfigPath } from '../../utils/config-manager';
import { ensureWebSearchMcpOrThrow } from '../../utils/websearch-manager';
import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis';
import { isSensitiveKey } from '../../utils/sensitive-keys';
import { isReservedName } from '../../config/reserved-names';
import { isUnifiedMode, mutateUnifiedConfig } from '../../config/unified-config-loader';
import { validateApiName } from './validation-service';
import { listApiProfiles } from './profile-reader';
import { validateApiProfileSettingsPayload } from './profile-lifecycle-validation';
@@ -26,6 +26,12 @@ import type {
ImportApiProfileResult,
RegisterApiProfileOrphansResult,
} from './profile-types';
import {
getCcsDir,
isUnifiedMode,
loadConfigSafe,
mutateConfig,
} from '../../config/config-loader-facade';
const SETTINGS_FILE_SUFFIX = '.settings.json';
const REDACTED_TOKEN_SENTINEL = '__CCS_REDACTED__';
@@ -62,7 +68,7 @@ function writeJsonObjectAtomically(filePath: string, value: unknown): void {
function registerApiProfileInConfig(name: string, target: TargetType, force = false): void {
if (isUnifiedMode()) {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (config.profiles[name] && !force) {
throw new Error(`API profile already exists: ${name}`);
}
+6 -2
View File
@@ -6,14 +6,18 @@
*/
import * as fs from 'fs';
import { loadConfigSafe } from '../../utils/config-manager';
import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
import { expandPath } from '../../utils/helpers';
import type { TargetType } from '../../targets/target-adapter';
import { isPersistedTargetType } from '../../targets/target-metadata';
import type { Settings } from '../../types/config';
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
import { resolveCliproxyBridgeMetadata } from './cliproxy-profile-bridge';
import {
isUnifiedMode,
loadConfigSafe,
loadOrCreateUnifiedConfig,
} from '../../config/config-loader-facade';
function sanitizeTarget(target: unknown): TargetType {
if (isPersistedTargetType(target)) {
+11 -5
View File
@@ -4,10 +4,10 @@
*/
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir, getConfigPath, loadConfigSafe } from '../../utils/config-manager';
import { getConfigPath } from '../../utils/config-manager';
import { expandPath } from '../../utils/helpers';
import { validateApiName } from './validation-service';
import { mutateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
import { ensureWebSearchMcpOrThrow } from '../../utils/websearch-manager';
import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis';
import type { TargetType } from '../../targets/target-adapter';
@@ -31,6 +31,12 @@ import {
resolveCliproxyBridgeMetadata,
resolveCliproxyBridgeProfile,
} from './cliproxy-profile-bridge';
import {
getCcsDir,
isUnifiedMode,
loadConfigSafe,
mutateConfig,
} from '../../config/config-loader-facade';
/** Check if URL is an OpenRouter endpoint */
function isOpenRouterUrl(baseUrl: string): boolean {
@@ -232,7 +238,7 @@ function createApiProfileUnified(
throw error;
}
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
config.profiles[name] = {
type: 'api',
settings: `~/.ccs/${settingsFile}`,
@@ -343,7 +349,7 @@ export function updateApiProfileTarget(
): UpdateApiProfileTargetResult {
try {
if (isUnifiedMode()) {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (!config.profiles[name]) {
throw new Error(`API profile not found: ${name}`);
}
@@ -392,7 +398,7 @@ export function updateApiProfileTarget(
/** Remove API profile from unified config */
function removeApiProfileUnified(name: string): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
const profile = config.profiles[name];
if (!profile) {
+2 -1
View File
@@ -12,7 +12,7 @@ import {
getWindowsEscapedCommandShell,
stripClaudeCodeEnv,
} from '../../utils/shell-executor';
import { isUnifiedMode } from '../../config/unified-config-loader';
import { ProfileMetadata } from '../../types';
import {
resolveCreateAccountContext,
@@ -25,6 +25,7 @@ import { exitWithError } from '../../errors';
import { ExitCode } from '../../errors/exit-codes';
import { CommandContext, parseArgs } from './types';
import { stripAmbientProviderCredentials } from './create-command-env';
import { isUnifiedMode } from '../../config/config-loader-facade';
function sanitizeProfileNameForInstance(name: string): string {
return name.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
+2 -1
View File
@@ -5,10 +5,11 @@
*/
import { initUI, color, dim, ok, fail } from '../../utils/ui';
import { isUnifiedMode } from '../../config/unified-config-loader';
import { exitWithError } from '../../errors';
import { ExitCode } from '../../errors/exit-codes';
import { CommandContext, parseArgs } from './types';
import { isUnifiedMode } from '../../config/config-loader-facade';
/**
* Handle the default command (set default profile)
+2 -1
View File
@@ -8,10 +8,11 @@ import * as fs from 'fs';
import * as path from 'path';
import { initUI, color, ok, fail, info } from '../../utils/ui';
import { InteractivePrompt } from '../../utils/prompt';
import { isUnifiedMode } from '../../config/unified-config-loader';
import { exitWithError } from '../../errors';
import { ExitCode } from '../../errors/exit-codes';
import { CommandContext, parseArgs } from './types';
import { isUnifiedMode } from '../../config/config-loader-facade';
/**
* Handle the remove command
+6 -5
View File
@@ -1,15 +1,16 @@
import * as fs from 'fs';
import {
getConfigJsonPath,
getContinuityInheritanceMap,
isUnifiedMode,
} from '../config/unified-config-loader';
import { warn } from '../utils/ui';
import InstanceManager from '../management/instance-manager';
import ProfileRegistry from './profile-registry';
import { isAccountContextMetadata, resolveAccountContextPolicy } from './account-context';
import type { ProfileType } from '../types/profile';
import { getProfileLookupCandidates, resolveAliasToCanonical } from '../utils/profile-compat';
import {
getConfigJsonPath,
getContinuityInheritanceMap,
isUnifiedMode,
} from '../config/config-loader-facade';
export interface ProfileContinuityInheritanceInput {
profileName: string;
+7 -2
View File
@@ -21,8 +21,7 @@ import {
CompositeVariantConfig,
CompositeTierConfig,
} from '../config/unified-config-types';
import { loadUnifiedConfig, isUnifiedMode, getCursorConfig } from '../config/unified-config-loader';
import { getCcsDir } from '../utils/config-manager';
import { getProfileLookupCandidates, isLegacyProfileAlias } from '../utils/profile-compat';
import type { CLIProxyProvider } from '../cliproxy/types';
import { CLIPROXY_PROVIDER_IDS, isCLIProxyProvider } from '../cliproxy/provider-capabilities';
@@ -30,6 +29,12 @@ import { LEGACY_CURSOR_PROFILE_NAME } from '../cursor/constants';
import { normalizeCopilotModelId } from '../copilot/copilot-model-normalizer';
import type { TargetType } from '../targets/target-adapter';
import type { ProfileType } from '../types/profile';
import {
getCcsDir,
getCursorConfig,
isUnifiedMode,
loadUnifiedConfig,
} from '../config/config-loader-facade';
export type { ProfileType } from '../types/profile';
/** CLIProxy profile names (OAuth-based, zero config) */
+14 -12
View File
@@ -1,15 +1,17 @@
import * as fs from 'fs';
import * as path from 'path';
import { ProfileMetadata } from '../types';
import {
loadOrCreateUnifiedConfig,
mutateUnifiedConfig,
isUnifiedMode,
} from '../config/unified-config-loader';
import type { AccountConfig } from '../config/unified-config-types';
import { getCcsDir } from '../utils/config-manager';
import { isValidContextGroupName, normalizeContextGroupName } from './account-context';
import { createLogger } from '../services/logging';
import {
getCcsDir,
isUnifiedMode,
loadOrCreateUnifiedConfig,
mutateConfig,
} from '../config/config-loader-facade';
const logger = createLogger('auth:profile-registry');
@@ -323,7 +325,7 @@ export class ProfileRegistry {
* Create account in unified config (config.yaml)
*/
createAccountUnified(name: string, metadata: CreateMetadata = {}): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (config.accounts[name]) {
throw new Error(`Account already exists: ${name}`);
}
@@ -342,7 +344,7 @@ export class ProfileRegistry {
* Update account metadata in unified config
*/
updateAccountUnified(name: string, updates: Partial<AccountConfig>): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (!config.accounts[name]) {
throw new Error(`Account not found: ${name}`);
}
@@ -357,7 +359,7 @@ export class ProfileRegistry {
* Remove account from unified config
*/
removeAccountUnified(name: string): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (!config.accounts[name]) {
throw new Error(`Account not found: ${name}`);
}
@@ -372,7 +374,7 @@ export class ProfileRegistry {
* Set default profile in unified config
*/
setDefaultUnified(name: string): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
const exists =
config.accounts[name] || config.profiles[name] || config.cliproxy?.variants?.[name];
if (!exists) {
@@ -386,7 +388,7 @@ export class ProfileRegistry {
* Clear default profile in unified config (restore original CCS behavior)
*/
clearDefaultUnified(): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
config.default = undefined;
});
}
@@ -426,7 +428,7 @@ export class ProfileRegistry {
* Update account last_used in unified config
*/
touchAccountUnified(name: string): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (!config.accounts[name]) {
throw new Error(`Account not found: ${name}`);
}
+2 -1
View File
@@ -1,11 +1,12 @@
import * as fs from 'fs';
import * as path from 'path';
import { getDefaultClaudeConfigDir } from '../utils/claude-config-path';
import { getCcsDir } from '../utils/config-manager';
import InstanceManager from '../management/instance-manager';
import ProfileDetector from './profile-detector';
import { resolveConfiguredContinuitySourceAccount } from './profile-continuity-inheritance';
import type { ProfileType } from '../types/profile';
import { getCcsDir } from '../config/config-loader-facade';
export type ResumeLaneKind =
| 'native'
+8 -11
View File
@@ -2,12 +2,7 @@ import './utils/fetch-proxy-setup';
import * as fs from 'fs';
import { detectClaudeCli } from './utils/claude-detector';
import {
getSettingsPath,
loadSettings,
setGlobalConfigDir,
detectCloudSyncPath,
} from './utils/config-manager';
import { getSettingsPath, setGlobalConfigDir, detectCloudSyncPath } from './utils/config-manager';
import { expandPath } from './utils/helpers';
import {
validateGlmKey,
@@ -46,11 +41,7 @@ import {
resolveOptionalBrowserAttachRuntime,
syncBrowserMcpToConfigDir,
} from './utils/browser';
import {
getBrowserConfig,
getGlobalEnvConfig,
getOfficialChannelsConfig,
} from './config/unified-config-loader';
import {
ensureProfileHooks as ensureImageAnalyzerHooks,
removeImageAnalysisProfileHook,
@@ -122,6 +113,12 @@ import {
checkCachedUpdate,
isCacheStale,
} from './utils/update-checker';
import {
getBrowserConfig,
getGlobalEnvConfig,
getOfficialChannelsConfig,
loadSettings,
} from './config/config-loader-facade';
// Note: npm is now the only supported installation method
// ========== Profile Detection ==========
+2 -1
View File
@@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir } from '../utils/config-manager';
import { getDefaultClaudeConfigDir } from '../utils/claude-config-path';
import type { OfficialChannelId } from '../config/unified-config-types';
import {
@@ -10,6 +10,7 @@ import {
getOfficialChannelTokenIds,
isOfficialChannelTokenRequired,
} from './official-channels-runtime';
import { getCcsDir } from '../config/config-loader-facade';
export type OfficialChannelTokenSource = 'saved_env' | 'process_env' | 'missing';
+2 -2
View File
@@ -14,7 +14,7 @@ import * as path from 'path';
import { warn, info } from '../../utils/ui';
import { CLIProxyProvider } from '../types';
import { loadAccountsRegistry, pauseAccount, resumeAccount } from './registry';
import { getCcsDir } from '../../utils/config-manager';
import { getCcsDir } from '../../config/config-loader-facade';
const ISSUE_509_URL = 'https://github.com/kaitranntt/ccs/issues/509';
@@ -690,7 +690,7 @@ export async function handleQuotaExhaustion(
// Dynamic imports to avoid circular dependencies
const { applyCooldown, findHealthyAccount } = await import('../quota/quota-manager');
const { setDefaultAccount, touchAccount } = await import('./account-manager');
const { loadOrCreateUnifiedConfig } = await import('../../config/unified-config-loader');
const { loadOrCreateUnifiedConfig } = await import('../../config/config-loader-facade');
const config = loadOrCreateUnifiedConfig();
const threshold = config.quota_management?.auto?.exhaustion_threshold ?? 5;
@@ -10,7 +10,7 @@
import { createInterface, Interface } from 'readline';
import { fail, info, ok, warn } from '../../utils/ui';
import { getCliproxySafetyConfig } from '../../config/unified-config-loader';
import { getCliproxySafetyConfig } from '../../config/config-loader-facade';
export const ANTIGRAVITY_RISK_ISSUE_URL = 'https://github.com/kaitranntt/ccs/issues/509';
export const ANTIGRAVITY_ACK_VERSION = '2026-02-24-antigravity-oauth-v2';
+6 -5
View File
@@ -8,8 +8,9 @@
*/
import { randomBytes } from 'crypto';
import { loadOrCreateUnifiedConfig, mutateUnifiedConfig } from '../../config/unified-config-loader';
import { CCS_INTERNAL_API_KEY, CCS_CONTROL_PANEL_SECRET } from '../config/generator';
import { loadOrCreateUnifiedConfig, mutateConfig } from '../../config/config-loader-facade';
/**
* Generate a cryptographically secure token.
@@ -87,7 +88,7 @@ export function getEffectiveManagementSecret(): string {
* @param apiKey - New API key (or undefined to reset to default)
*/
export function setGlobalApiKey(apiKey: string | undefined): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (!config.cliproxy.auth) {
config.cliproxy.auth = {};
}
@@ -107,7 +108,7 @@ export function setGlobalApiKey(apiKey: string | undefined): void {
* @param secret - New management secret (or undefined to reset to default)
*/
export function setGlobalManagementSecret(secret: string | undefined): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (!config.cliproxy.auth) {
config.cliproxy.auth = {};
}
@@ -128,7 +129,7 @@ export function setGlobalManagementSecret(secret: string | undefined): void {
* @param apiKey - New API key (or undefined to remove override)
*/
export function setVariantApiKey(variantName: string, apiKey: string | undefined): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
const variant = config.cliproxy.variants[variantName];
if (!variant) {
@@ -155,7 +156,7 @@ export function setVariantApiKey(variantName: string, apiKey: string | undefined
* Removes cliproxy.auth and all variant auth overrides.
*/
export function resetAuthToDefaults(): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
delete config.cliproxy.auth;
for (const variantName of Object.keys(config.cliproxy.variants)) {
+1 -1
View File
@@ -5,8 +5,8 @@
* Returns null if disabled or not configured.
*/
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import type { TokenRefreshSettings } from '../../config/unified-config-types';
import { loadOrCreateUnifiedConfig } from '../../config/config-loader-facade';
/**
* Get token refresh configuration from unified config
+2 -1
View File
@@ -18,7 +18,7 @@ import {
} from './binary/platform-detector';
import { stopProxy } from './services/proxy-lifecycle-service';
import { waitForPortFree } from '../utils/port-utils';
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
import {
UpdateCheckResult,
checkForUpdates,
@@ -39,6 +39,7 @@ import {
import type { CLIProxyBackend } from './types';
import { getVersionListCachePath } from './binary/version-cache';
import { loadOrCreateUnifiedConfig } from '../config/config-loader-facade';
export const CLIPROXY_DELETED_PLUS_REPO = 'router-for-me/CLIProxyAPIPlus';
export const CLIPROXY_PLUS_FALLBACK_TRACKING_URL = 'https://github.com/kaitranntt/ccs/issues/1062';
+2 -1
View File
@@ -7,7 +7,7 @@ import * as fs from 'fs';
import * as path from 'path';
import type { CLIProxyProvider, ProviderModelMapping } from '../types';
import { getModelMappingFromConfig, getEnvVarsFromConfig } from '../config/base-config-loader';
import { getGlobalEnvConfig } from '../../config/unified-config-loader';
import { getEffectiveApiKey } from '../auth/auth-token-manager';
import { expandPath } from '../../utils/helpers';
import { warn } from '../../utils/ui';
@@ -31,6 +31,7 @@ import {
normalizeIFlowLegacyModelAliases,
normalizeModelIdForProvider,
} from '../ai-providers/model-id-normalizer';
import { getGlobalEnvConfig } from '../../config/config-loader-facade';
/** Settings file structure for user overrides */
interface ProviderSettings {
+2 -1
View File
@@ -9,11 +9,12 @@ import type { CLIProxyProvider, ProviderConfig } from '../types';
import { getProviderDisplayName } from '../provider-capabilities';
import { getModelMappingFromConfig } from '../config/base-config-loader';
import { AI_PROVIDER_FAMILY_IDS } from '../ai-providers/types';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import { getEffectiveApiKey, getEffectiveManagementSecret } from '../auth/auth-token-manager';
import { getDeniedModelIdReasonForProvider } from '../ai-providers/model-id-normalizer';
import { getAuthDir, getProviderAuthDir, getConfigPathForPort } from './path-resolver';
import { CLIPROXY_DEFAULT_PORT } from './port-manager';
import { loadOrCreateUnifiedConfig } from '../../config/config-loader-facade';
/** Internal API key for CCS-managed requests */
export const CCS_INTERNAL_API_KEY = 'ccs-internal-managed';
+2 -1
View File
@@ -12,8 +12,9 @@ import { getProviderCatalog, supportsModelConfig, ModelEntry } from '../model-ca
import { getClaudeEnvVars, resolveProviderSettingsPath } from './config-generator';
import { CLIProxyProvider } from '../types';
import { initUI, color, bold, dim, ok, info, header } from '../../utils/ui';
import { getCcsDir } from '../../utils/config-manager';
import { normalizeModelIdForProvider } from '../ai-providers/model-id-normalizer';
import { getCcsDir } from '../../config/config-loader-facade';
function canonicalizeModelForProvider(provider: CLIProxyProvider, model: string): string {
return normalizeModelIdForProvider(model, provider);
+2 -1
View File
@@ -4,9 +4,10 @@
*/
import * as path from 'path';
import { getCcsDir } from '../../utils/config-manager';
import type { CLIProxyProvider } from '../types';
import { CLIPROXY_DEFAULT_PORT } from './port-manager';
import { getCcsDir } from '../../config/config-loader-facade';
/**
* Get CLIProxy base directory
+2 -1
View File
@@ -6,11 +6,12 @@
import type { CLIProxyProvider } from '../types';
import { DEFAULT_THINKING_TIER_DEFAULTS } from '../../config/unified-config-types';
import type { ThinkingConfig } from '../../config/unified-config-types';
import { getThinkingConfig } from '../../config/unified-config-loader';
import { getModelThinkingSupport, supportsThinking } from '../model-catalog';
import { isThinkingOffValue, validateThinking } from '../thinking-validator';
import { normalizeModelIdForProvider } from '../ai-providers/model-id-normalizer';
import { warn } from '../../utils/ui';
import { getThinkingConfig } from '../../config/config-loader-facade';
/** Model tier types for thinking budget defaults */
export type ModelTier = 'opus' | 'sonnet' | 'haiku';
@@ -20,7 +20,7 @@ import {
resolveOptionalBrowserAttachRuntime,
syncBrowserMcpToConfigDir,
} from '../../utils/browser';
import { getBrowserConfig } from '../../config/unified-config-loader';
import { getBrowserConfig } from '../../config/config-loader-facade';
export interface BrowserLaunchSetupResult {
/** CLI override flag if --browser-launch / --no-browser-launch was passed */
+1 -1
View File
@@ -28,7 +28,7 @@ import {
ensureImageAnalysisMcpOrThrow,
syncImageAnalysisMcpToConfigDir,
} from '../../utils/image-analysis';
import { loadOrCreateUnifiedConfig, getThinkingConfig } from '../../config/unified-config-loader';
import { loadOrCreateUnifiedConfig, getThinkingConfig } from '../../config/config-loader-facade';
import { HttpsTunnelProxy } from '../proxy/https-tunnel-proxy';
import { resolveProfileContinuityInheritance } from '../../auth/profile-continuity-inheritance';
+1 -1
View File
@@ -21,7 +21,7 @@
import * as path from 'path';
import { warn } from '../../utils/ui';
import { getCcsDir } from '../../utils/config-manager';
import { getCcsDir } from '../../config/config-loader-facade';
import {
ToolSanitizationProxy,
type ToolSanitizationProxyConfig,
+1 -1
View File
@@ -5,7 +5,6 @@
* based on unified config. Used by stats-fetcher, auth-routes, and UI.
*/
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import type { CliproxyServerConfig } from '../../config/unified-config-types';
import {
CLIPROXY_DEFAULT_PORT,
@@ -15,6 +14,7 @@ import {
} from '../config/port-manager';
import { getProxyEnvVars } from './proxy-config-resolver';
import { getEffectiveManagementSecret } from '../auth/auth-token-manager';
import { loadOrCreateUnifiedConfig } from '../../config/config-loader-facade';
/** Resolved proxy target for making requests */
export interface ProxyTarget {
@@ -25,8 +25,9 @@ import {
stripCodexEffortSuffix,
} from '../ai-providers/model-id-normalizer';
import { getModelMaxLevel } from '../model-catalog';
import { getCcsDir } from '../../utils/config-manager';
import { createLogger } from '../../services/logging';
import { getCcsDir } from '../../config/config-loader-facade';
export interface ToolSanitizationProxyConfig {
/** Upstream CLIProxy URL */
+2 -1
View File
@@ -32,8 +32,9 @@ import {
touchAccount,
type AccountInfo,
} from '../accounts/account-manager';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import type { RuntimeMonitorConfig } from '../../config/unified-config-types';
import { loadOrCreateUnifiedConfig } from '../../config/config-loader-facade';
export type ManagedQuotaProvider = 'agy' | 'claude' | 'codex' | 'gemini' | 'ghcp';
type ManagedQuotaResult =
+3 -3
View File
@@ -1,4 +1,3 @@
import { mutateUnifiedConfig, loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import { regenerateConfig } from '../config/generator';
import { getAuthDir, getConfigPathForPort } from '../config/path-resolver';
import {
@@ -7,6 +6,7 @@ import {
getRoutingErrorMessage,
} from './routing-strategy-http';
import type { CliproxyRoutingStrategy } from '../types';
import { loadOrCreateUnifiedConfig, mutateConfig } from '../../config/config-loader-facade';
export const DEFAULT_CLIPROXY_ROUTING_STRATEGY: CliproxyRoutingStrategy = 'round-robin';
export const DEFAULT_CLIPROXY_SESSION_AFFINITY_ENABLED = false;
@@ -223,7 +223,7 @@ export async function applyCliproxyRoutingStrategy(
};
}
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (config.cliproxy) {
config.cliproxy.routing = { ...config.cliproxy.routing, strategy };
}
@@ -278,7 +278,7 @@ export async function applyCliproxySessionAffinitySettings(
current.ttl ??
DEFAULT_CLIPROXY_SESSION_AFFINITY_TTL;
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (config.cliproxy) {
config.cliproxy.routing = {
...config.cliproxy.routing,
+1 -1
View File
@@ -22,7 +22,7 @@ import {
} from '../binary-manager';
import { BACKEND_CONFIG, DEFAULT_BACKEND } from '../binary/platform-detector';
import { CLIProxyBackend } from '../types';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import { loadOrCreateUnifiedConfig } from '../../config/config-loader-facade';
/** Binary status result */
export interface BinaryStatusResult {
+2 -1
View File
@@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir } from '../../utils/config-manager';
import type { CLIProxyProvider } from '../types';
import type { ModelEntry, ProviderCatalog, ThinkingSupport } from '../model-catalog';
import { MODEL_CATALOG } from '../model-catalog';
@@ -15,6 +15,7 @@ import {
buildProxyUrl,
getProxyTarget,
} from '../proxy/proxy-target-resolver';
import { getCcsDir } from '../../config/config-loader-facade';
const CACHE_FILE_NAME = 'model-catalog-cache.json';
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
@@ -1,5 +1,5 @@
import * as fs from 'fs';
import { getConfigPath, loadConfigSafe } from '../../utils/config-manager';
import { getConfigPath } from '../../utils/config-manager';
import { CLIProxyProvider } from '../types';
import type { TargetType } from '../../targets/target-adapter';
import {
@@ -8,12 +8,14 @@ import {
CompositeTierConfig,
CLIPROXY_SUPPORTED_PROVIDERS,
} from '../../config/unified-config-types';
import {
loadOrCreateUnifiedConfig,
mutateUnifiedConfig,
isUnifiedMode,
} from '../../config/unified-config-loader';
import { CLIPROXY_DEFAULT_PORT } from '../config/config-generator';
import {
isUnifiedMode,
loadConfigSafe,
loadOrCreateUnifiedConfig,
mutateConfig,
} from '../../config/config-loader-facade';
export const VARIANT_PORT_BASE = CLIPROXY_DEFAULT_PORT + 1;
export const VARIANT_PORT_MAX_OFFSET = 100;
@@ -171,7 +173,7 @@ export function listVariantsFromConfig(): Record<string, VariantConfig> {
}
export function saveCompositeVariantUnified(name: string, config: CompositeVariantConfig): void {
mutateUnifiedConfig((unifiedConfig) => {
mutateConfig((unifiedConfig) => {
if (!unifiedConfig.cliproxy) {
unifiedConfig.cliproxy = {
oauth_accounts: {},
@@ -195,7 +197,7 @@ export function saveVariantUnified(
port?: number,
target: TargetType = 'claude'
): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (!config.cliproxy) {
config.cliproxy = {
oauth_accounts: {},
@@ -266,7 +268,7 @@ export function saveVariantLegacy(
export function removeVariantFromUnifiedConfig(name: string): VariantConfig | null {
let removedVariant: CLIProxyVariantConfig | CompositeVariantConfig | null = null;
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (!config.cliproxy?.variants || !(name in config.cliproxy.variants)) {
return;
}
+3 -2
View File
@@ -12,11 +12,11 @@ import { CLIProxyProvider, PLUS_ONLY_PROVIDERS } from '../types';
import { CompositeTierConfig, CompositeVariantConfig } from '../../config/unified-config-types';
import type { TargetType } from '../../targets/target-adapter';
import { isReservedName, isWindowsReservedName } from '../../config/reserved-names';
import { isUnifiedMode } from '../../config/unified-config-loader';
import { deleteConfigForPort } from '../config/config-generator';
import { hasActiveSessions, deleteSessionLockForPort } from '../session-tracker';
import { warn } from '../../utils/ui';
import { getCcsDir } from '../../utils/config-manager';
import { validateCompositeTiers } from '../config/composite-validator';
import {
canonicalizeModelIdForProvider,
@@ -43,6 +43,7 @@ import {
getNextAvailablePort,
} from './variant-config-adapter';
import { getConfiguredBackend, getPlusBackendUnavailableMessage } from '../binary-manager';
import { getCcsDir, isUnifiedMode } from '../../config/config-loader-facade';
// Re-export VariantConfig from adapter
export type { VariantConfig } from './variant-config-adapter';
+2 -1
View File
@@ -9,7 +9,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CLIProxyProfileName } from '../../auth/profile-detector';
import { getCcsDir } from '../../utils/config-manager';
import { expandPath } from '../../utils/helpers';
import { getClaudeEnvVars, CLIPROXY_DEFAULT_PORT } from '../config/config-generator';
import { CLIProxyProvider } from '../types';
@@ -24,6 +24,7 @@ import { prepareImageAnalysisFallbackHook } from '../../utils/hooks';
import { getEffectiveApiKey } from '../auth/auth-token-manager';
import { warn } from '../../utils/ui';
import { normalizeModelIdForProvider } from '../ai-providers/model-id-normalizer';
import { getCcsDir } from '../../config/config-loader-facade';
/** Environment settings structure */
interface SettingsEnv {
+2 -2
View File
@@ -7,9 +7,9 @@
import * as chokidar from 'chokidar';
import * as path from 'path';
import { getCcsDir } from '../../utils/config-manager';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import { syncToLocalConfig } from './local-config-sync';
import { getCcsDir, loadOrCreateUnifiedConfig } from '../../config/config-loader-facade';
/** Debounce delay in milliseconds */
const DEBOUNCE_MS = 3000;
+2 -1
View File
@@ -6,10 +6,11 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir } from '../../utils/config-manager';
import { expandPath } from '../../utils/helpers';
import { listApiProfiles, isApiProfileConfigured } from '../../api/services/profile-reader';
import type { ClaudeKey } from '../management/management-api-types';
import { getCcsDir } from '../../config/config-loader-facade';
/**
* Profile info with settings for sync.
+4 -3
View File
@@ -1,9 +1,10 @@
import * as browserUtils from '../utils/browser';
import { getBrowserConfig, mutateUnifiedConfig } from '../config/unified-config-loader';
import type { BrowserToolPolicy } from '../config/unified-config-types';
import { getCcsPathDisplay } from '../utils/config-manager';
import { getNodePlatformKey } from '../utils/browser/platform';
import { color, dim, header, initUI, subheader } from '../utils/ui';
import { getBrowserConfig, mutateConfig } from '../config/config-loader-facade';
type HelpWriter = (line: string) => void;
type BrowserLane = 'claude' | 'codex' | 'all';
@@ -216,7 +217,7 @@ function updateBrowserPolicies(updates: {
claude?: BrowserToolPolicy;
codex?: BrowserToolPolicy;
}): void {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
const current = getBrowserConfig();
config.browser = {
claude: {
@@ -235,7 +236,7 @@ function updateBrowserPolicies(updates: {
function updateBrowserEnabled(subcommand: 'enable' | 'disable', lane: BrowserLane): void {
const nextEnabled = subcommand === 'enable';
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
const current = getBrowserConfig();
config.browser = {
claude: {
@@ -1,6 +1,7 @@
import { CLIPROXY_DEFAULT_PORT, validatePort } from '../../cliproxy/config/port-manager';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import type { UnifiedConfig } from '../../config/unified-config-types';
import { loadOrCreateUnifiedConfig } from '../../config/config-loader-facade';
type LifecyclePortConfig = Pick<UnifiedConfig, 'cliproxy_server'>;
+2 -1
View File
@@ -17,7 +17,7 @@ import type { CliproxyProviderRoutingHints } from '../../shared/cliproxy-model-r
import { CLIProxyProvider, CLIProxyBackend } from '../../cliproxy/types';
import type { TargetType } from '../../targets/target-adapter';
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
import { isUnifiedMode } from '../../config/unified-config-loader';
import { initUI, header, color, ok, fail, warn, info, infoBox, dim } from '../../utils/ui';
import { InteractivePrompt } from '../../utils/prompt';
import {
@@ -32,6 +32,7 @@ import {
import { DEFAULT_BACKEND } from '../../cliproxy/binary/platform-detector';
import { CompositeTierConfig } from '../../config/unified-config-types';
import { formatAccountDisplayName } from '../../cliproxy/accounts/email-account-identity';
import { isUnifiedMode } from '../../config/config-loader-facade';
interface CliproxyProfileArgs {
name?: string;
+3 -2
View File
@@ -5,8 +5,9 @@
*/
import { InteractivePrompt } from '../../utils/prompt';
import { getDashboardAuthConfig, mutateUnifiedConfig } from '../../config/unified-config-loader';
import { initUI, header, ok, info, warn, dim } from '../../utils/ui';
import { getDashboardAuthConfig, mutateConfig } from '../../config/config-loader-facade';
/**
* Handle disable command - disable auth with confirmation
@@ -53,7 +54,7 @@ export async function handleDisable(): Promise<void> {
}
// Disable auth
mutateUnifiedConfig((fullConfig) => {
mutateConfig((fullConfig) => {
fullConfig.dashboard_auth = {
enabled: false,
username: fullConfig.dashboard_auth?.username ?? '',
+3 -2
View File
@@ -8,9 +8,10 @@
import bcrypt from 'bcrypt';
import { InteractivePrompt } from '../../utils/prompt';
import { mutateUnifiedConfig } from '../../config/unified-config-loader';
import { initUI, header, subheader, ok, fail, info, warn, dim } from '../../utils/ui';
import type { AuthSetupResult } from './types';
import { mutateConfig } from '../../config/config-loader-facade';
const BCRYPT_ROUNDS = 10;
const MIN_PASSWORD_LENGTH = 8;
@@ -99,7 +100,7 @@ export async function handleSetup(): Promise<AuthSetupResult> {
// Hash password
const passwordHash = await bcrypt.hash(password, BCRYPT_ROUNDS);
const config = mutateUnifiedConfig((currentConfig) => {
const config = mutateConfig((currentConfig) => {
currentConfig.dashboard_auth = {
enabled: true,
username,
+1 -1
View File
@@ -4,9 +4,9 @@
* Display current dashboard authentication status.
*/
import { getDashboardAuthConfig } from '../../config/unified-config-loader';
import { initUI, header, subheader, ok, info, warn, dim, color } from '../../utils/ui';
import type { AuthStatusInfo } from './types';
import { getDashboardAuthConfig } from '../../config/config-loader-facade';
/**
* Get auth status info with ENV override detection
+7 -6
View File
@@ -1,9 +1,5 @@
import { initUI, header, ok, info, warn, fail, subheader, color, dim } from '../utils/ui';
import {
getOfficialChannelsConfig,
loadOrCreateUnifiedConfig,
updateUnifiedConfig,
} from '../config/unified-config-loader';
import type { OfficialChannelId } from '../config/unified-config-types';
import { DEFAULT_OFFICIAL_CHANNELS_CONFIG } from '../config/unified-config-types';
import {
@@ -42,6 +38,11 @@ import {
isOfficialChannelSelectionValid,
} from '../channels/official-channels-runtime';
import { extractOption, hasAnyFlag } from './arg-extractor';
import {
getOfficialChannelsConfig,
loadOrCreateUnifiedConfig,
updateConfig,
} from '../config/config-loader-facade';
interface ChannelsCommandOptions {
enable: boolean;
@@ -440,7 +441,7 @@ export async function handleConfigChannelsCommand(args: string[]): Promise<void>
try {
if (hasConfigMutation) {
updateUnifiedConfig({ channels: nextConfig });
updateConfig({ channels: nextConfig });
}
if (options.setToken) {
+2 -1
View File
@@ -12,7 +12,7 @@ import { startServer } from '../web-server';
import { setupGracefulShutdown } from '../web-server/shutdown';
import { ensureCliproxyService } from '../cliproxy/service-manager';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/config-generator';
import { getDashboardAuthConfig } from '../config/unified-config-loader';
import { initUI, header, ok, info, warn, fail } from '../utils/ui';
import { resolveNamedCommand, type NamedCommandRoute } from './named-command-router';
import {
@@ -23,6 +23,7 @@ import {
} from './config-dashboard-host';
import { parseConfigCommandArgs, showConfigCommandHelp } from './config-command-options';
import { createLogger } from '../services/logging';
import { getDashboardAuthConfig } from '../config/config-loader-facade';
const logger = createLogger('command:config');
@@ -6,11 +6,7 @@
*/
import { initUI, header, ok, info, warn, fail, subheader, color, dim } from '../utils/ui';
import {
getImageAnalysisConfig,
updateUnifiedConfig,
loadOrCreateUnifiedConfig,
} from '../config/unified-config-loader';
import { DEFAULT_IMAGE_ANALYSIS_CONFIG } from '../config/unified-config-types';
import {
CLIPROXY_PROVIDER_IDS,
@@ -19,6 +15,11 @@ import {
} from '../cliproxy/provider-capabilities';
import { extractOption, hasAnyFlag } from './arg-extractor';
import { normalizeImageAnalysisBackendId } from '../utils/hooks';
import {
getImageAnalysisConfig,
loadOrCreateUnifiedConfig,
updateConfig,
} from '../config/config-loader-facade';
interface ImageAnalysisCommandOptions {
enable?: boolean;
@@ -346,7 +347,7 @@ export async function handleConfigImageAnalysisCommand(args: string[]): Promise<
}
if (hasChanges) {
updateUnifiedConfig({ image_analysis: imageConfig });
updateConfig({ image_analysis: imageConfig });
console.log(ok('Configuration updated'));
console.log('');
}
+7 -6
View File
@@ -6,11 +6,7 @@
*/
import { initUI, header, ok, info, warn, fail, subheader, color, dim } from '../utils/ui';
import {
getThinkingConfig,
updateUnifiedConfig,
loadOrCreateUnifiedConfig,
} from '../config/unified-config-loader';
import { DEFAULT_THINKING_TIER_DEFAULTS } from '../config/unified-config-types';
import { VALID_THINKING_LEVELS } from '../cliproxy/thinking-validator';
import {
@@ -18,6 +14,11 @@ import {
parseThinkingCommandArgs,
parseThinkingOverrideInput,
} from './config-thinking-parser';
import {
getThinkingConfig,
loadOrCreateUnifiedConfig,
updateConfig,
} from '../config/config-loader-facade';
const VALID_THINKING_MODES = ['auto', 'off', 'manual'] as const;
@@ -293,7 +294,7 @@ export async function handleConfigThinkingCommand(args: string[]): Promise<void>
}
if (hasChanges) {
updateUnifiedConfig({ thinking: thinkingConfig });
updateConfig({ thinking: thinkingConfig });
console.log(ok('Configuration updated'));
console.log('');
}
+4 -3
View File
@@ -15,10 +15,11 @@ import {
normalizeCopilotConfigWithWarnings,
} from '../copilot';
import type { CopilotModel } from '../copilot';
import { loadOrCreateUnifiedConfig, mutateUnifiedConfig } from '../config/unified-config-loader';
import { DEFAULT_COPILOT_CONFIG } from '../config/unified-config-types';
import { ok, fail, info, color, warn } from '../utils/ui';
import { normalizeCopilotSubcommand } from '../copilot/constants';
import { loadOrCreateUnifiedConfig, mutateConfig } from '../config/config-loader-facade';
/**
* Handle copilot subcommand.
@@ -361,7 +362,7 @@ async function handleStop(): Promise<number> {
* Handle enable subcommand.
*/
async function handleEnable(): Promise<number> {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (!config.copilot) {
config.copilot = { ...DEFAULT_COPILOT_CONFIG };
}
@@ -383,7 +384,7 @@ async function handleEnable(): Promise<number> {
* Handle disable subcommand.
*/
async function handleDisable(): Promise<number> {
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (config.copilot) {
config.copilot.enabled = false;
}
+4 -3
View File
@@ -16,7 +16,7 @@ import {
getDefaultModel,
probeCursorRuntime,
} from '../cursor';
import { getCursorConfig, mutateUnifiedConfig } from '../config/unified-config-loader';
import { DEFAULT_CURSOR_CONFIG } from '../config/unified-config-types';
import {
renderCursorHelp,
@@ -25,6 +25,7 @@ import {
renderCursorStatus,
} from './cursor-command-display';
import { ok, fail, info } from '../utils/ui';
import { getCursorConfig, mutateConfig } from '../config/config-loader-facade';
const LEGACY_CURSOR_COMMAND = 'ccs legacy cursor';
const CLIPROXY_CURSOR_COMMAND = 'ccs cursor';
@@ -286,7 +287,7 @@ async function handleStop(): Promise<number> {
*/
async function handleEnable(): Promise<number> {
printLegacyCursorDeprecationNotice();
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (!config.cursor) {
config.cursor = { ...DEFAULT_CURSOR_CONFIG };
}
@@ -310,7 +311,7 @@ async function handleEnable(): Promise<number> {
*/
async function handleDisable(): Promise<number> {
printLegacyCursorDeprecationNotice();
mutateUnifiedConfig((config) => {
mutateConfig((config) => {
if (config.cursor) {
config.cursor.enabled = false;
}
+2 -1
View File
@@ -6,12 +6,13 @@
*/
import { initUI, header, dim, color, subheader, fail, warn } from '../utils/ui';
import { getCcsDir } from '../utils/config-manager';
import { CLAUDE_EXTENSION_HOSTS, type ClaudeExtensionHost } from '../shared/claude-extension-hosts';
import {
renderClaudeExtensionSettingsJson,
resolveClaudeExtensionSetup,
} from '../shared/claude-extension-setup';
import { getCcsDir } from '../config/config-loader-facade';
type ShellType = 'bash' | 'fish' | 'powershell';
type OutputFormat = 'openai' | 'anthropic' | 'raw' | 'claude-extension';
+2 -1
View File
@@ -16,8 +16,9 @@ import {
needsMigration,
getBackupDirectories,
} from '../config/migration-manager';
import { hasUnifiedConfig } from '../config/unified-config-loader';
import { initUI, ok, fail, info, warn, infoBox, dim } from '../utils/ui';
import { hasUnifiedConfig } from '../config/config-loader-facade';
export async function handleMigrateCommand(args: string[]): Promise<void> {
await initUI();
+2 -1
View File
@@ -1,5 +1,5 @@
import { detectShell, formatExportLine } from './env-command';
import { getSettingsPath, loadSettings } from '../utils/config-manager';
import { getSettingsPath } from '../utils/config-manager';
import { expandPath } from '../utils/helpers';
import { fail, info, ok } from '../utils/ui';
import {
@@ -10,6 +10,7 @@ import {
startOpenAICompatProxy,
stopOpenAICompatProxy,
} from '../proxy';
import { loadSettings } from '../config/config-loader-facade';
function parseOptionValue(args: string[], key: string): string | undefined {
const exactIndex = args.findIndex((arg) => arg === key);
+9 -7
View File
@@ -16,15 +16,17 @@ import * as readline from 'readline';
import * as fs from 'fs';
import * as path from 'path';
import { initUI, header, ok, info, warn } from '../utils/ui';
import { DEFAULT_CLIPROXY_SERVER_CONFIG } from '../config/unified-config-types';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
import {
getCcsDir,
hasUnifiedConfig,
loadOrCreateUnifiedConfig,
loadUnifiedConfig,
mutateUnifiedConfig,
hasUnifiedConfig,
} from '../config/unified-config-loader';
import { DEFAULT_CLIPROXY_SERVER_CONFIG } from '../config/unified-config-types';
import { getCcsDir } from '../utils/config-manager';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
mutateConfig,
} from '../config/config-loader-facade';
/** Custom error for user cancellation (Ctrl+C) */
class UserCancelledError extends Error {
@@ -377,7 +379,7 @@ async function runSetupWizard(force: boolean = false): Promise<void> {
console.log(' After creating, edit the settings file to add your API key.');
}
mutateUnifiedConfig((currentConfig) => {
mutateConfig((currentConfig) => {
currentConfig.setup_completed = true;
currentConfig.cliproxy_server = config.cliproxy_server;
});
+2 -1
View File
@@ -7,9 +7,10 @@
import * as path from 'path';
import * as fs from 'fs';
import { initUI, header, subheader, color, warn } from '../utils/ui';
import { getActiveConfigPath, getCcsDir } from '../utils/config-manager';
import { getActiveConfigPath } from '../utils/config-manager';
import { getVersion } from '../utils/version';
import { getProfileLookupCandidates } from '../utils/profile-compat';
import { getCcsDir } from '../config/config-loader-facade';
/**
* Handle version command
+339
View File
@@ -0,0 +1,339 @@
/**
* config-getters.ts
*
* Typed sub-config accessor functions extracted from unified-config-loader.ts
* (Phase 5 split issue #1164).
*
* All functions read the loaded config via loadOrCreateUnifiedConfig and
* return typed sub-configs with defaults applied.
*
* No I/O beyond what loadOrCreateUnifiedConfig performs internally.
*/
import {
DEFAULT_CLIPROXY_SAFETY_CONFIG,
DEFAULT_CURSOR_CONFIG,
DEFAULT_GLOBAL_ENV,
DEFAULT_IMAGE_ANALYSIS_CONFIG,
DEFAULT_LOGGING_CONFIG,
DEFAULT_OFFICIAL_CHANNELS_CONFIG,
DEFAULT_THINKING_CONFIG,
} from '../unified-config-types';
import type {
BrowserConfig,
CLIProxySafetyConfig,
CursorConfig,
DashboardAuthConfig,
GlobalEnvConfig,
ImageAnalysisConfig,
LoggingConfig,
OfficialChannelsConfig,
ThinkingConfig,
} from '../unified-config-types';
import { canonicalizeBrowserConfig } from './normalizers';
import { canonicalizeImageAnalysisConfig } from '../../utils/hooks/image-analysis-backend-resolver';
import { normalizeOfficialChannelIds } from '../../channels/official-channels-runtime';
import { normalizeSearxngBaseUrl } from '../../utils/websearch/types';
// ---------------------------------------------------------------------------
// Circular-import safety: loadOrCreateUnifiedConfig lives in
// unified-config-loader.ts which imports this file. We break the cycle by
// using a lazy require() inside getConfig() so the module is resolved at
// call time (after both modules have finished loading) rather than at import
// time. This also preserves spy/mock compatibility: test spies replace the
// function on the module namespace object, and require() returns that live
// namespace, so the spy is always picked up.
// ---------------------------------------------------------------------------
function getConfig(): import('../unified-config-types').UnifiedConfig {
const loader = require('../unified-config-loader') as {
loadOrCreateUnifiedConfig: () => import('../unified-config-types').UnifiedConfig;
};
return loader.loadOrCreateUnifiedConfig();
}
// ---------------------------------------------------------------------------
// GeminiWebSearchInfo interface
// ---------------------------------------------------------------------------
/**
* Gemini CLI WebSearch configuration
*/
export interface GeminiWebSearchInfo {
enabled: boolean;
model: string;
timeout: number;
}
// ---------------------------------------------------------------------------
// Accessor functions
// ---------------------------------------------------------------------------
/**
* Get websearch configuration.
* Returns defaults if not configured.
* Supports deterministic providers and optional Gemini/OpenCode/Grok CLI fallbacks.
*/
export function getWebSearchConfig(): {
enabled: boolean;
providers?: {
exa?: { enabled?: boolean; max_results?: number };
tavily?: { enabled?: boolean; max_results?: number };
brave?: { enabled?: boolean; max_results?: number };
searxng?: { enabled?: boolean; url?: string; max_results?: number };
duckduckgo?: { enabled?: boolean; max_results?: number };
gemini?: GeminiWebSearchInfo;
opencode?: { enabled?: boolean; model?: string; timeout?: number };
grok?: { enabled?: boolean; timeout?: number };
};
// Legacy fields (deprecated)
gemini?: { enabled?: boolean; timeout?: number };
} {
const config = getConfig();
// Build provider configs
const exaConfig = {
enabled: config.websearch?.providers?.exa?.enabled ?? false,
max_results: config.websearch?.providers?.exa?.max_results ?? 5,
};
const tavilyConfig = {
enabled: config.websearch?.providers?.tavily?.enabled ?? false,
max_results: config.websearch?.providers?.tavily?.max_results ?? 5,
};
const duckDuckGoConfig = {
enabled: config.websearch?.providers?.duckduckgo?.enabled ?? true,
max_results: config.websearch?.providers?.duckduckgo?.max_results ?? 5,
};
const braveConfig = {
enabled: config.websearch?.providers?.brave?.enabled ?? false,
max_results: config.websearch?.providers?.brave?.max_results ?? 5,
};
const searxngConfig = {
enabled: config.websearch?.providers?.searxng?.enabled ?? false,
url: normalizeSearxngBaseUrl(config.websearch?.providers?.searxng?.url) ?? '',
max_results: config.websearch?.providers?.searxng?.max_results ?? 5,
};
const geminiConfig: GeminiWebSearchInfo = {
enabled:
config.websearch?.providers?.gemini?.enabled ?? config.websearch?.gemini?.enabled ?? false,
model: config.websearch?.providers?.gemini?.model ?? 'gemini-2.5-flash',
timeout:
config.websearch?.providers?.gemini?.timeout ?? config.websearch?.gemini?.timeout ?? 55,
};
const opencodeConfig = {
enabled: config.websearch?.providers?.opencode?.enabled ?? false,
model: config.websearch?.providers?.opencode?.model ?? 'opencode/grok-code',
timeout: config.websearch?.providers?.opencode?.timeout ?? 90,
};
const grokConfig = {
enabled: config.websearch?.providers?.grok?.enabled ?? false,
timeout: config.websearch?.providers?.grok?.timeout ?? 55,
};
// Auto-enable master switch if ANY provider is enabled
const anyProviderEnabled =
exaConfig.enabled ||
tavilyConfig.enabled ||
braveConfig.enabled ||
searxngConfig.enabled ||
duckDuckGoConfig.enabled ||
geminiConfig.enabled ||
opencodeConfig.enabled ||
grokConfig.enabled;
const enabled = anyProviderEnabled && (config.websearch?.enabled ?? true);
return {
enabled,
providers: {
exa: exaConfig,
tavily: tavilyConfig,
brave: braveConfig,
searxng: searxngConfig,
duckduckgo: duckDuckGoConfig,
gemini: geminiConfig,
opencode: opencodeConfig,
grok: grokConfig,
},
// Legacy field for backwards compatibility
gemini: config.websearch?.gemini,
};
}
/**
* Get global_env configuration.
* Returns defaults if not configured.
*/
export function getGlobalEnvConfig(): GlobalEnvConfig {
const config = getConfig();
return {
enabled: config.global_env?.enabled ?? true,
env: config.global_env?.env ?? { ...DEFAULT_GLOBAL_ENV },
};
}
/**
* Get continuity inheritance mapping.
* Returns empty mapping when not configured.
*/
export function getContinuityInheritanceMap(): Record<string, string> {
const config = getConfig();
return config.continuity?.inherit_from_account ?? {};
}
/**
* Get cliproxy safety configuration.
* Returns defaults if not configured.
*/
export function getCliproxySafetyConfig(): CLIProxySafetyConfig {
const config = getConfig();
return {
antigravity_ack_bypass:
config.cliproxy?.safety?.antigravity_ack_bypass ??
DEFAULT_CLIPROXY_SAFETY_CONFIG.antigravity_ack_bypass,
};
}
/**
* Get thinking configuration.
* Returns defaults if not configured.
*/
export function getThinkingConfig(): ThinkingConfig {
const config = getConfig();
// W2: Check for invalid thinking config (e.g., thinking: true instead of object)
if (config.thinking !== undefined && typeof config.thinking !== 'object') {
console.warn(
`[!] Invalid thinking config: expected object, got ${typeof config.thinking}. Using defaults.`
);
console.warn(` Tip: Use 'thinking: { mode: auto }' instead of 'thinking: true'`);
return DEFAULT_THINKING_CONFIG;
}
return {
mode: config.thinking?.mode ?? DEFAULT_THINKING_CONFIG.mode,
override: config.thinking?.override,
tier_defaults: {
opus: config.thinking?.tier_defaults?.opus ?? DEFAULT_THINKING_CONFIG.tier_defaults.opus,
sonnet:
config.thinking?.tier_defaults?.sonnet ?? DEFAULT_THINKING_CONFIG.tier_defaults.sonnet,
haiku: config.thinking?.tier_defaults?.haiku ?? DEFAULT_THINKING_CONFIG.tier_defaults.haiku,
},
provider_overrides: config.thinking?.provider_overrides,
show_warnings: config.thinking?.show_warnings ?? DEFAULT_THINKING_CONFIG.show_warnings,
};
}
/**
* Get Official Channels configuration.
* Returns defaults if not configured.
*/
export function getOfficialChannelsConfig(): OfficialChannelsConfig {
const config = getConfig();
return {
selected:
config.channels?.selected && config.channels.selected.length > 0
? normalizeOfficialChannelIds(config.channels.selected)
: DEFAULT_OFFICIAL_CHANNELS_CONFIG.selected,
unattended: config.channels?.unattended ?? DEFAULT_OFFICIAL_CHANNELS_CONFIG.unattended,
};
}
/**
* Check if dashboard auth is enabled.
* Priority: ENV vars > config.yaml > defaults
*/
export function isDashboardAuthEnabled(): boolean {
const envEnabled = process.env.CCS_DASHBOARD_AUTH_ENABLED;
if (envEnabled !== undefined) {
return envEnabled === 'true' || envEnabled === '1';
}
const config = getConfig();
return config.dashboard_auth?.enabled ?? false;
}
/**
* Get dashboard_auth configuration with ENV var override.
* Priority: ENV vars > config.yaml > defaults
*/
export function getDashboardAuthConfig(): DashboardAuthConfig {
const config = getConfig();
// ENV vars take precedence
const envEnabled = process.env.CCS_DASHBOARD_AUTH_ENABLED;
const envUsername = process.env.CCS_DASHBOARD_USERNAME;
const envPasswordHash = process.env.CCS_DASHBOARD_PASSWORD_HASH;
return {
enabled:
envEnabled !== undefined
? envEnabled === 'true' || envEnabled === '1'
: (config.dashboard_auth?.enabled ?? false),
username: envUsername ?? config.dashboard_auth?.username ?? '',
password_hash: envPasswordHash ?? config.dashboard_auth?.password_hash ?? '',
session_timeout_hours: config.dashboard_auth?.session_timeout_hours ?? 24,
};
}
/**
* Get browser automation configuration.
* Returns canonicalized defaults if not configured.
*/
export function getBrowserConfig(): BrowserConfig {
const config = getConfig();
return canonicalizeBrowserConfig(config.browser);
}
/**
* Get image_analysis configuration.
* Returns defaults if not configured.
*/
export function getImageAnalysisConfig(): ImageAnalysisConfig {
const config = getConfig();
return canonicalizeImageAnalysisConfig({
enabled: config.image_analysis?.enabled ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.enabled,
timeout: config.image_analysis?.timeout ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.timeout,
provider_models:
config.image_analysis?.provider_models ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.provider_models,
fallback_backend:
config.image_analysis?.fallback_backend ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.fallback_backend,
profile_backends:
config.image_analysis?.profile_backends ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.profile_backends,
});
}
/**
* Get logging configuration.
* Returns defaults if not configured.
*/
export function getLoggingConfig(): LoggingConfig {
const config = getConfig();
return {
enabled: config.logging?.enabled ?? DEFAULT_LOGGING_CONFIG.enabled,
level: config.logging?.level ?? DEFAULT_LOGGING_CONFIG.level,
rotate_mb: config.logging?.rotate_mb ?? DEFAULT_LOGGING_CONFIG.rotate_mb,
retain_days: config.logging?.retain_days ?? DEFAULT_LOGGING_CONFIG.retain_days,
redact: config.logging?.redact ?? DEFAULT_LOGGING_CONFIG.redact,
live_buffer_size: config.logging?.live_buffer_size ?? DEFAULT_LOGGING_CONFIG.live_buffer_size,
};
}
/**
* Get cursor configuration.
* Returns defaults if not configured.
*/
export function getCursorConfig(): CursorConfig {
const config = getConfig();
return config.cursor ?? { ...DEFAULT_CURSOR_CONFIG };
}
+323
View File
@@ -0,0 +1,323 @@
/**
* defaults-merger.ts
*
* mergeWithDefaults function extracted from unified-config-loader.ts
* (Phase 4 split issue #1164).
*
* Pure transform: no I/O, no side effects. Merges a partial UnifiedConfig
* with defaults, filling in missing sections.
*
* Circular-import note: this module imports from normalizers.ts (Phase 2)
* and io-locks.ts (Phase 1) is NOT imported here, so there is no cycle.
* io-locks.ts callbacks (mergeWithDefaults, validateCompositeVariants) are
* now replaced with direct imports in unified-config-loader.ts (Phase 6).
*/
import {
createEmptyUnifiedConfig,
DEFAULT_COPILOT_CONFIG,
DEFAULT_CURSOR_CONFIG,
DEFAULT_GLOBAL_ENV,
DEFAULT_CLIPROXY_SERVER_CONFIG,
DEFAULT_CLIPROXY_SAFETY_CONFIG,
DEFAULT_OPENAI_COMPAT_PROXY_CONFIG,
DEFAULT_QUOTA_MANAGEMENT_CONFIG,
DEFAULT_THINKING_CONFIG,
DEFAULT_DASHBOARD_AUTH_CONFIG,
DEFAULT_IMAGE_ANALYSIS_CONFIG,
DEFAULT_LOGGING_CONFIG,
} from '../unified-config-types';
import type { UnifiedConfig } from '../unified-config-types';
import { canonicalizeBrowserConfig, normalizeSessionAffinityTtl } from './normalizers';
import { normalizeContinuityConfig, normalizeOfficialChannelsConfig } from './normalizers';
import type { LegacyDiscordChannelsConfig } from './normalizers';
import { canonicalizeImageAnalysisConfig } from '../../utils/hooks/image-analysis-backend-resolver';
import { normalizeSearxngBaseUrl } from '../../utils/websearch/types';
// ---------------------------------------------------------------------------
// mergeWithDefaults
// ---------------------------------------------------------------------------
/**
* Merge partial config with defaults.
* Preserves existing data while filling in missing sections.
*/
export function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
const defaults = createEmptyUnifiedConfig();
const continuity = normalizeContinuityConfig(partial);
return {
version: partial.version ?? defaults.version,
setup_completed: partial.setup_completed,
default: partial.default ?? defaults.default,
accounts: partial.accounts ?? defaults.accounts,
profiles: partial.profiles ?? defaults.profiles,
cliproxy: {
...partial.cliproxy,
oauth_accounts: partial.cliproxy?.oauth_accounts ?? defaults.cliproxy.oauth_accounts,
providers: defaults.cliproxy.providers, // Always use defaults for providers
variants: partial.cliproxy?.variants ?? defaults.cliproxy.variants,
logging: {
enabled: partial.cliproxy?.logging?.enabled ?? defaults.cliproxy.logging?.enabled ?? false,
request_log:
partial.cliproxy?.logging?.request_log ?? defaults.cliproxy.logging?.request_log ?? false,
},
safety: {
antigravity_ack_bypass:
partial.cliproxy?.safety?.antigravity_ack_bypass ??
DEFAULT_CLIPROXY_SAFETY_CONFIG.antigravity_ack_bypass,
},
// Kiro browser behavior setting (optional)
kiro_no_incognito: partial.cliproxy?.kiro_no_incognito,
// Auth config - preserve user values, no defaults (uses constants as fallback)
auth: partial.cliproxy?.auth,
// Background token refresh config (optional)
token_refresh: partial.cliproxy?.token_refresh,
// Backend selection - validate and preserve user choice (original vs plus)
backend:
partial.cliproxy?.backend === 'original' || partial.cliproxy?.backend === 'plus'
? partial.cliproxy.backend
: undefined, // Invalid values become undefined (defaults to 'original' at runtime)
// Auto-sync - default to true
auto_sync: partial.cliproxy?.auto_sync ?? defaults.cliproxy.auto_sync ?? true,
routing: {
strategy:
partial.cliproxy?.routing?.strategy === 'fill-first' ||
partial.cliproxy?.routing?.strategy === 'round-robin'
? partial.cliproxy.routing.strategy
: defaults.cliproxy.routing?.strategy,
session_affinity:
typeof partial.cliproxy?.routing?.session_affinity === 'boolean'
? partial.cliproxy.routing.session_affinity
: defaults.cliproxy.routing?.session_affinity,
session_affinity_ttl: normalizeSessionAffinityTtl(
partial.cliproxy?.routing?.session_affinity_ttl,
defaults.cliproxy.routing?.session_affinity_ttl ?? '1h'
),
},
},
proxy: {
port: partial.proxy?.port ?? DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.port,
profile_ports: partial.proxy?.profile_ports ?? {
...DEFAULT_OPENAI_COMPAT_PROXY_CONFIG.profile_ports,
},
routing: {
default: partial.proxy?.routing?.default ?? defaults.proxy?.routing?.default,
background: partial.proxy?.routing?.background ?? defaults.proxy?.routing?.background,
think: partial.proxy?.routing?.think ?? defaults.proxy?.routing?.think,
longContext: partial.proxy?.routing?.longContext ?? defaults.proxy?.routing?.longContext,
webSearch: partial.proxy?.routing?.webSearch ?? defaults.proxy?.routing?.webSearch,
longContextThreshold:
partial.proxy?.routing?.longContextThreshold ??
defaults.proxy?.routing?.longContextThreshold,
},
},
logging: {
enabled: partial.logging?.enabled ?? DEFAULT_LOGGING_CONFIG.enabled,
level: partial.logging?.level ?? DEFAULT_LOGGING_CONFIG.level,
rotate_mb: partial.logging?.rotate_mb ?? DEFAULT_LOGGING_CONFIG.rotate_mb,
retain_days: partial.logging?.retain_days ?? DEFAULT_LOGGING_CONFIG.retain_days,
redact: partial.logging?.redact ?? DEFAULT_LOGGING_CONFIG.redact,
live_buffer_size:
partial.logging?.live_buffer_size ?? DEFAULT_LOGGING_CONFIG.live_buffer_size,
},
preferences: {
...defaults.preferences,
...partial.preferences,
},
websearch: {
enabled: partial.websearch?.enabled ?? defaults.websearch?.enabled ?? true,
providers: {
exa: {
enabled: partial.websearch?.providers?.exa?.enabled ?? false,
max_results: partial.websearch?.providers?.exa?.max_results ?? 5,
},
tavily: {
enabled: partial.websearch?.providers?.tavily?.enabled ?? false,
max_results: partial.websearch?.providers?.tavily?.max_results ?? 5,
},
brave: {
enabled: partial.websearch?.providers?.brave?.enabled ?? false,
max_results: partial.websearch?.providers?.brave?.max_results ?? 5,
},
searxng: {
enabled: partial.websearch?.providers?.searxng?.enabled ?? false,
url: normalizeSearxngBaseUrl(partial.websearch?.providers?.searxng?.url) ?? '',
max_results: partial.websearch?.providers?.searxng?.max_results ?? 5,
},
duckduckgo: {
enabled: partial.websearch?.providers?.duckduckgo?.enabled ?? true,
max_results: partial.websearch?.providers?.duckduckgo?.max_results ?? 5,
},
gemini: {
enabled:
partial.websearch?.providers?.gemini?.enabled ??
partial.websearch?.gemini?.enabled ?? // Legacy fallback
false,
model: partial.websearch?.providers?.gemini?.model ?? 'gemini-2.5-flash',
timeout:
partial.websearch?.providers?.gemini?.timeout ??
partial.websearch?.gemini?.timeout ?? // Legacy fallback
55,
},
opencode: {
enabled: partial.websearch?.providers?.opencode?.enabled ?? false,
model: partial.websearch?.providers?.opencode?.model ?? 'opencode/grok-code',
timeout: partial.websearch?.providers?.opencode?.timeout ?? 90,
},
grok: {
enabled: partial.websearch?.providers?.grok?.enabled ?? false,
timeout: partial.websearch?.providers?.grok?.timeout ?? 55,
},
},
// Legacy fields (keep for backwards compatibility during read)
gemini: partial.websearch?.gemini,
},
// Copilot config - strictly opt-in, merge with defaults
copilot: {
enabled: partial.copilot?.enabled ?? DEFAULT_COPILOT_CONFIG.enabled,
auto_start: partial.copilot?.auto_start ?? DEFAULT_COPILOT_CONFIG.auto_start,
port: partial.copilot?.port ?? DEFAULT_COPILOT_CONFIG.port,
account_type: partial.copilot?.account_type ?? DEFAULT_COPILOT_CONFIG.account_type,
rate_limit: partial.copilot?.rate_limit ?? DEFAULT_COPILOT_CONFIG.rate_limit,
wait_on_limit: partial.copilot?.wait_on_limit ?? DEFAULT_COPILOT_CONFIG.wait_on_limit,
model: partial.copilot?.model ?? DEFAULT_COPILOT_CONFIG.model,
},
// Cursor config - disabled by default, merge with defaults
cursor: {
enabled: partial.cursor?.enabled ?? DEFAULT_CURSOR_CONFIG.enabled,
port: partial.cursor?.port ?? DEFAULT_CURSOR_CONFIG.port,
auto_start: partial.cursor?.auto_start ?? DEFAULT_CURSOR_CONFIG.auto_start,
ghost_mode: partial.cursor?.ghost_mode ?? DEFAULT_CURSOR_CONFIG.ghost_mode,
model: partial.cursor?.model ?? DEFAULT_CURSOR_CONFIG.model,
opus_model: partial.cursor?.opus_model,
sonnet_model: partial.cursor?.sonnet_model,
haiku_model: partial.cursor?.haiku_model,
},
// Global env - injected into all non-Claude subscription profiles
global_env: {
enabled: partial.global_env?.enabled ?? true,
env: partial.global_env?.env ?? { ...DEFAULT_GLOBAL_ENV },
},
continuity,
// CLIProxy server config - remote/local CLIProxyAPI settings
cliproxy_server: {
remote: {
enabled:
partial.cliproxy_server?.remote?.enabled ?? DEFAULT_CLIPROXY_SERVER_CONFIG.remote.enabled,
host: partial.cliproxy_server?.remote?.host ?? DEFAULT_CLIPROXY_SERVER_CONFIG.remote.host,
// Port is optional - undefined means use protocol default (443 for HTTPS, 8317 for HTTP)
port: partial.cliproxy_server?.remote?.port,
protocol:
partial.cliproxy_server?.remote?.protocol ??
DEFAULT_CLIPROXY_SERVER_CONFIG.remote.protocol,
auth_token:
partial.cliproxy_server?.remote?.auth_token ??
DEFAULT_CLIPROXY_SERVER_CONFIG.remote.auth_token,
// management_key is optional - falls back to auth_token when not set
management_key: partial.cliproxy_server?.remote?.management_key,
},
fallback: {
enabled:
partial.cliproxy_server?.fallback?.enabled ??
DEFAULT_CLIPROXY_SERVER_CONFIG.fallback.enabled,
auto_start:
partial.cliproxy_server?.fallback?.auto_start ??
DEFAULT_CLIPROXY_SERVER_CONFIG.fallback.auto_start,
},
local: {
port: partial.cliproxy_server?.local?.port ?? DEFAULT_CLIPROXY_SERVER_CONFIG.local.port,
auto_start:
partial.cliproxy_server?.local?.auto_start ??
DEFAULT_CLIPROXY_SERVER_CONFIG.local.auto_start,
},
},
// Quota management config - hybrid auto+manual account selection
quota_management: {
mode: partial.quota_management?.mode ?? DEFAULT_QUOTA_MANAGEMENT_CONFIG.mode,
auto: {
preflight_check:
partial.quota_management?.auto?.preflight_check ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.auto.preflight_check,
exhaustion_threshold:
partial.quota_management?.auto?.exhaustion_threshold ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.auto.exhaustion_threshold,
tier_priority:
partial.quota_management?.auto?.tier_priority ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.auto.tier_priority,
cooldown_minutes:
partial.quota_management?.auto?.cooldown_minutes ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.auto.cooldown_minutes,
},
manual: {
paused_accounts:
partial.quota_management?.manual?.paused_accounts ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.manual.paused_accounts,
forced_default:
partial.quota_management?.manual?.forced_default ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.manual.forced_default,
tier_lock:
partial.quota_management?.manual?.tier_lock ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.manual.tier_lock,
},
runtime_monitor: {
enabled:
partial.quota_management?.runtime_monitor?.enabled ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.runtime_monitor.enabled,
normal_interval_seconds:
partial.quota_management?.runtime_monitor?.normal_interval_seconds ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.runtime_monitor.normal_interval_seconds,
critical_interval_seconds:
partial.quota_management?.runtime_monitor?.critical_interval_seconds ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.runtime_monitor.critical_interval_seconds,
warn_threshold:
partial.quota_management?.runtime_monitor?.warn_threshold ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.runtime_monitor.warn_threshold,
exhaustion_threshold:
partial.quota_management?.runtime_monitor?.exhaustion_threshold ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.runtime_monitor.exhaustion_threshold,
cooldown_minutes:
partial.quota_management?.runtime_monitor?.cooldown_minutes ??
DEFAULT_QUOTA_MANAGEMENT_CONFIG.runtime_monitor.cooldown_minutes,
},
},
// Thinking config - auto/manual/off control for reasoning budget
thinking: {
mode: partial.thinking?.mode ?? DEFAULT_THINKING_CONFIG.mode,
override: partial.thinking?.override,
tier_defaults: {
opus: partial.thinking?.tier_defaults?.opus ?? DEFAULT_THINKING_CONFIG.tier_defaults.opus,
sonnet:
partial.thinking?.tier_defaults?.sonnet ?? DEFAULT_THINKING_CONFIG.tier_defaults.sonnet,
haiku:
partial.thinking?.tier_defaults?.haiku ?? DEFAULT_THINKING_CONFIG.tier_defaults.haiku,
},
provider_overrides: partial.thinking?.provider_overrides,
show_warnings: partial.thinking?.show_warnings ?? DEFAULT_THINKING_CONFIG.show_warnings,
},
channels: normalizeOfficialChannelsConfig(
partial as Partial<UnifiedConfig> & { discord_channels?: LegacyDiscordChannelsConfig }
),
// Dashboard auth config - disabled by default
dashboard_auth: {
enabled: partial.dashboard_auth?.enabled ?? DEFAULT_DASHBOARD_AUTH_CONFIG.enabled,
username: partial.dashboard_auth?.username ?? DEFAULT_DASHBOARD_AUTH_CONFIG.username,
password_hash:
partial.dashboard_auth?.password_hash ?? DEFAULT_DASHBOARD_AUTH_CONFIG.password_hash,
session_timeout_hours:
partial.dashboard_auth?.session_timeout_hours ??
DEFAULT_DASHBOARD_AUTH_CONFIG.session_timeout_hours,
},
browser: canonicalizeBrowserConfig(partial.browser),
// Image analysis config - enabled by default for CLIProxy providers
image_analysis: canonicalizeImageAnalysisConfig({
enabled: partial.image_analysis?.enabled ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.enabled,
timeout: partial.image_analysis?.timeout ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.timeout,
provider_models:
partial.image_analysis?.provider_models ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.provider_models,
fallback_backend:
partial.image_analysis?.fallback_backend ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.fallback_backend,
profile_backends:
partial.image_analysis?.profile_backends ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.profile_backends,
}),
};
}
+301
View File
@@ -0,0 +1,301 @@
/**
* io-locks.ts
*
* Path constants, lockfile management, and config I/O helpers extracted from
* unified-config-loader.ts (Phase 1 split issue #1164).
*
* Forward-reference note: loadUnifiedConfigWithLockHeld and
* writeUnifiedConfigWithLockHeld depend on mergeWithDefaults,
* validateCompositeVariants, generateYamlHeader, and generateYamlWithComments
* which still live in unified-config-loader.ts (extracted in later phases).
* Those are passed as callbacks to avoid a circular import. Once Phases 23
* land in separate modules with no dependency on this file, they can be
* imported directly.
*/
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import * as yaml from 'js-yaml';
import { getCcsDir } from '../../utils/config-manager';
import {
isUnifiedConfig,
createEmptyUnifiedConfig,
UNIFIED_CONFIG_VERSION,
} from '../unified-config-types';
import type { UnifiedConfig } from '../unified-config-types';
// ---------------------------------------------------------------------------
// Path constants
// ---------------------------------------------------------------------------
export const CONFIG_YAML = 'config.yaml';
export const CONFIG_JSON = 'config.json';
export const CONFIG_LOCK = 'config.yaml.lock';
/** Lock is stale after this many milliseconds */
export const LOCK_STALE_MS = 5000;
export const GO_DURATION_SEGMENT = String.raw`(?:\d+(?:\.\d+)?(?:ns|us|µs|μs|ms|s|m|h))`;
export const GO_DURATION_PATTERN = new RegExp(`^${GO_DURATION_SEGMENT}+$`);
// ---------------------------------------------------------------------------
// Path helpers
// ---------------------------------------------------------------------------
/**
* Get path to unified config.yaml
*/
export function getConfigYamlPath(): string {
return path.join(getCcsDir(), CONFIG_YAML);
}
/**
* Get path to legacy config.json
*/
export function getConfigJsonPath(): string {
return path.join(getCcsDir(), CONFIG_JSON);
}
/**
* Get path to config lockfile (internal)
*/
function getLockFilePath(): string {
return path.join(getCcsDir(), CONFIG_LOCK);
}
// ---------------------------------------------------------------------------
// Process check
// ---------------------------------------------------------------------------
function processExists(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
// ---------------------------------------------------------------------------
// Lockfile primitives
// ---------------------------------------------------------------------------
/**
* Acquire lockfile for config write operations.
* Returns a lock token if acquired, null if already locked by another process.
* Cleans up stale locks (older than LOCK_STALE_MS).
*/
export function acquireLock(): string | null {
const lockPath = getLockFilePath();
const lockDir = path.dirname(lockPath);
const lockToken = crypto.randomUUID();
const lockData = `${process.pid}\n${Date.now()}\n${lockToken}`;
try {
if (!fs.existsSync(lockDir)) {
fs.mkdirSync(lockDir, { recursive: true, mode: 0o700 });
}
// Check if lock exists
if (fs.existsSync(lockPath)) {
const content = fs.readFileSync(lockPath, 'utf8');
const [pidStr, timestampStr] = content.trim().split('\n');
const pid = Number.parseInt(pidStr, 10);
const timestamp = Number.parseInt(timestampStr, 10);
const hasLiveOwner = Number.isInteger(pid) && pid > 0 && processExists(pid);
const isStale = !Number.isFinite(timestamp) || Date.now() - timestamp > LOCK_STALE_MS;
if (hasLiveOwner) {
return null;
}
if (isStale || !hasLiveOwner) {
fs.unlinkSync(lockPath);
}
}
// Acquire lock
fs.writeFileSync(lockPath, lockData, { flag: 'wx', mode: 0o600 });
return lockToken;
} catch (error) {
// EEXIST means another process acquired the lock between our check and write
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
return null;
}
return null;
}
}
/**
* Release lockfile after config write operation.
*/
export function releaseLock(lockToken: string): void {
const lockPath = getLockFilePath();
try {
if (fs.existsSync(lockPath)) {
const content = fs.readFileSync(lockPath, 'utf8');
const fileToken = content.trim().split('\n')[2];
if (fileToken === lockToken) {
fs.unlinkSync(lockPath);
}
}
} catch {
// Ignore cleanup errors
}
}
// ---------------------------------------------------------------------------
// Config format detection
// ---------------------------------------------------------------------------
/**
* Check if unified config.yaml exists
*/
export function hasUnifiedConfig(): boolean {
return fs.existsSync(getConfigYamlPath());
}
/**
* Check if legacy config.json exists
*/
export function hasLegacyConfig(): boolean {
return fs.existsSync(getConfigJsonPath());
}
// ---------------------------------------------------------------------------
// Sync sleep
// ---------------------------------------------------------------------------
/**
* Sync sleep helper for lock retry loops.
* Uses Atomics.wait when available to avoid CPU-intensive busy-wait.
*/
export function sleepSync(ms: number): void {
try {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
} catch {
const end = Date.now() + ms;
while (Date.now() < end) {
/* busy-wait */
}
}
}
// ---------------------------------------------------------------------------
// Lock-aware execution wrapper
// ---------------------------------------------------------------------------
/**
* Execute a callback while holding the config lock.
*/
export function withConfigWriteLock<T>(callback: () => T): T {
// Acquire lock (retry for up to 1 second)
const maxRetries = 10;
const retryDelayMs = 100;
let lockToken: string | null = null;
for (let i = 0; i < maxRetries; i++) {
const acquiredToken = acquireLock();
if (acquiredToken) {
lockToken = acquiredToken;
break;
}
sleepSync(retryDelayMs);
}
if (!lockToken) {
throw new Error('Config file is locked by another process. Wait a moment and try again.');
}
try {
return callback();
} finally {
// Always release lock
releaseLock(lockToken);
}
}
// ---------------------------------------------------------------------------
// Lock-held read/write helpers
// ---------------------------------------------------------------------------
/**
* Load unified config directly from disk while lock is already held.
* Falls back to empty config when file doesn't exist.
*
* Forward-reference: mergeWithDefaults and validateCompositeVariants are
* passed as callbacks to avoid a circular dependency. They will be imported
* directly once all phases are complete.
*/
export function loadUnifiedConfigWithLockHeld(
mergeWithDefaults: (partial: Partial<UnifiedConfig>) => UnifiedConfig,
validateCompositeVariants: (config: UnifiedConfig) => void
): UnifiedConfig {
const yamlPath = getConfigYamlPath();
if (!fs.existsSync(yamlPath)) {
return createEmptyUnifiedConfig();
}
const content = fs.readFileSync(yamlPath, 'utf8');
const parsed = yaml.load(content);
if (!isUnifiedConfig(parsed)) {
throw new Error(`Invalid config format in ${yamlPath}`);
}
const merged = mergeWithDefaults(parsed);
validateCompositeVariants(merged);
return merged;
}
/**
* Write unified config to disk while lock is already held.
* Uses atomic write (temp file + rename) to prevent corruption.
*
* Forward-reference: generateYamlHeader and generateYamlWithComments are
* passed as callbacks to avoid a circular dependency. They will be imported
* directly once all phases are complete.
*/
export function writeUnifiedConfigWithLockHeld(
config: UnifiedConfig,
generateYamlHeader: () => string,
generateYamlWithComments: (config: UnifiedConfig) => string
): void {
const yamlPath = getConfigYamlPath();
const dir = path.dirname(yamlPath);
// Ensure directory exists
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}
// Ensure version is set
config.version = UNIFIED_CONFIG_VERSION;
// Generate YAML with section comments
const yamlContent = generateYamlWithComments(config);
const content = generateYamlHeader() + yamlContent;
// Atomic write: write to temp file, then rename
const tempPath = `${yamlPath}.tmp.${process.pid}`;
try {
fs.writeFileSync(tempPath, content, { mode: 0o600 });
fs.renameSync(tempPath, yamlPath);
} catch (error) {
// Clean up temp file on error
if (fs.existsSync(tempPath)) {
try {
fs.unlinkSync(tempPath);
} catch {
// Ignore cleanup errors
}
}
// Classify filesystem errors
const err = error as NodeJS.ErrnoException;
if (err.code === 'ENOSPC') {
throw new Error('Disk full - cannot save config. Free up space and try again.');
} else if (err.code === 'EROFS' || err.code === 'EACCES') {
throw new Error(`Cannot write config - check file permissions: ${err.message}`);
}
throw error;
}
}
+228
View File
@@ -0,0 +1,228 @@
/**
* normalizers.ts
*
* Input normalization and validation helpers extracted from
* unified-config-loader.ts (Phase 2 split issue #1164).
*
* Contains: browser config normalizers, session affinity TTL normalizer,
* composite variant validator, continuity config normalizer, and official
* channels config normalizer.
*/
import { DEFAULT_BROWSER_CONFIG, DEFAULT_OFFICIAL_CHANNELS_CONFIG } from '../unified-config-types';
import type {
UnifiedConfig,
BrowserConfig,
BrowserEvalMode,
BrowserToolPolicy,
OfficialChannelsConfig,
OfficialChannelId,
ContinuityConfig,
} from '../unified-config-types';
import { validateCompositeTiers } from '../../cliproxy/config/composite-validator';
import {
isOfficialChannelId,
normalizeOfficialChannelIds,
resolveLegacyDiscordSelection,
} from '../../channels/official-channels-runtime';
import { getRecommendedBrowserUserDataDir } from '../../utils/browser/browser-settings';
import { GO_DURATION_PATTERN, GO_DURATION_SEGMENT } from './io-locks';
// ---------------------------------------------------------------------------
// Browser normalizers
// ---------------------------------------------------------------------------
export function normalizeBrowserDevtoolsPort(value: number | undefined): number {
if (!Number.isFinite(value)) {
return DEFAULT_BROWSER_CONFIG.claude.devtools_port;
}
const port = Math.floor(value as number);
if (port < 1 || port > 65535) {
return DEFAULT_BROWSER_CONFIG.claude.devtools_port;
}
return port;
}
export function normalizeBrowserPolicy(value: string | undefined): BrowserToolPolicy {
return value === 'auto' || value === 'manual' ? value : DEFAULT_BROWSER_CONFIG.claude.policy;
}
export function normalizeBrowserEvalMode(value: string | undefined): BrowserEvalMode {
if (value === 'disabled' || value === 'readonly' || value === 'readwrite') {
return value;
}
return DEFAULT_BROWSER_CONFIG.claude.eval_mode ?? 'readonly';
}
export function canonicalizeBrowserConfig(
config?: BrowserConfig,
fallback: BrowserConfig = DEFAULT_BROWSER_CONFIG
): BrowserConfig {
const claudeUserDataDir =
config?.claude?.user_data_dir === undefined
? fallback.claude.user_data_dir || getRecommendedBrowserUserDataDir()
: config.claude.user_data_dir.trim() || getRecommendedBrowserUserDataDir();
return {
claude: {
enabled: config?.claude?.enabled ?? fallback.claude.enabled,
policy: normalizeBrowserPolicy(config?.claude?.policy ?? fallback.claude.policy),
user_data_dir: claudeUserDataDir,
devtools_port: normalizeBrowserDevtoolsPort(
config?.claude?.devtools_port ?? fallback.claude.devtools_port
),
eval_mode: normalizeBrowserEvalMode(config?.claude?.eval_mode ?? fallback.claude.eval_mode),
},
codex: {
enabled: config?.codex?.enabled ?? fallback.codex.enabled,
policy: normalizeBrowserPolicy(config?.codex?.policy ?? fallback.codex.policy),
eval_mode: normalizeBrowserEvalMode(config?.codex?.eval_mode ?? fallback.codex.eval_mode),
},
};
}
// ---------------------------------------------------------------------------
// Session affinity TTL normalizer
// ---------------------------------------------------------------------------
export function normalizeSessionAffinityTtl(value: unknown, fallback: string): string {
if (typeof value !== 'string') {
return fallback;
}
const trimmed = value.trim();
if (!trimmed || !GO_DURATION_PATTERN.test(trimmed) || !hasPositiveDuration(trimmed)) {
return fallback;
}
return trimmed;
}
export function hasPositiveDuration(value: string): boolean {
const segments = value.match(new RegExp(GO_DURATION_SEGMENT, 'g'));
if (!segments) {
return false;
}
return segments.some((segment) => {
const numeric = parseFloat(segment);
return Number.isFinite(numeric) && numeric > 0;
});
}
// ---------------------------------------------------------------------------
// Composite variant validator
// ---------------------------------------------------------------------------
/**
* Validate composite variant provider strings.
* Warns about invalid providers in composite variant configurations.
*/
export function validateCompositeVariants(config: UnifiedConfig): void {
const variants = config.cliproxy?.variants;
if (!variants) return;
for (const [name, variant] of Object.entries(variants)) {
if ('type' in variant && variant.type === 'composite') {
const error = validateCompositeTiers(variant.tiers, {
defaultTier: variant.default_tier,
requireAllTiers: true,
});
if (error) {
console.warn(`[!] Variant '${name}': invalid composite config (${error})`);
}
}
}
}
// ---------------------------------------------------------------------------
// Continuity config normalizer
// ---------------------------------------------------------------------------
/**
* Normalize continuity inheritance mapping payload.
* Keeps only non-empty string keys and values.
*/
export function normalizeContinuityInheritanceMap(
value: unknown
): Record<string, string> | undefined {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return undefined;
}
const normalized: Record<string, string> = {};
for (const [profileName, accountName] of Object.entries(value as Record<string, unknown>)) {
const normalizedProfile = profileName.trim();
const normalizedAccount = typeof accountName === 'string' ? accountName.trim() : '';
if (!normalizedProfile || !normalizedAccount) {
continue;
}
normalized[normalizedProfile] = normalizedAccount;
}
return Object.keys(normalized).length > 0 ? normalized : undefined;
}
/**
* Normalize continuity section.
* Supports legacy root key: continuity_inherit_from_account.
*/
export function normalizeContinuityConfig(
partial: Partial<UnifiedConfig>
): ContinuityConfig | undefined {
const legacyMap = normalizeContinuityInheritanceMap(
(partial as Partial<UnifiedConfig> & { continuity_inherit_from_account?: unknown })
.continuity_inherit_from_account
);
const continuityMap = normalizeContinuityInheritanceMap(partial.continuity?.inherit_from_account);
if (!legacyMap && !continuityMap) {
return undefined;
}
return {
inherit_from_account: {
...(legacyMap ?? {}),
...(continuityMap ?? {}),
},
};
}
// ---------------------------------------------------------------------------
// Official channels config normalizer
// ---------------------------------------------------------------------------
export interface LegacyDiscordChannelsConfig {
enabled?: boolean;
unattended?: boolean;
}
export function normalizeOfficialChannelsConfig(
partial: Partial<UnifiedConfig> & { discord_channels?: LegacyDiscordChannelsConfig }
): OfficialChannelsConfig {
const hasCanonicalChannelsSection = partial.channels !== undefined;
const hasExplicitSelectedField =
hasCanonicalChannelsSection &&
Object.prototype.hasOwnProperty.call(partial.channels, 'selected');
const rawSelected =
hasExplicitSelectedField && Array.isArray(partial.channels?.selected)
? partial.channels.selected.filter((value): value is OfficialChannelId =>
isOfficialChannelId(value)
)
: [];
return {
selected: hasCanonicalChannelsSection
? normalizeOfficialChannelIds(rawSelected)
: resolveLegacyDiscordSelection(partial.discord_channels?.enabled),
unattended:
partial.channels?.unattended ??
partial.discord_channels?.unattended ??
DEFAULT_OFFICIAL_CHANNELS_CONFIG.unattended,
};
}
+349
View File
@@ -0,0 +1,349 @@
/**
* yaml-serializer.ts
*
* YAML generation helpers extracted from unified-config-loader.ts
* (Phase 3 split issue #1164).
*
* Contains: generateYamlHeader, generateYamlWithComments.
* These produce the commented YAML written to config.yaml on every save.
*/
import * as yaml from 'js-yaml';
import type { UnifiedConfig } from '../unified-config-types';
/**
* Generate YAML header with helpful comments.
*/
export function generateYamlHeader(): string {
return `# CCS Unified Configuration
# Docs: https://github.com/kaitranntt/ccs
`;
}
/**
* Generate YAML content with section comments for better readability.
*/
export function generateYamlWithComments(config: UnifiedConfig): string {
const lines: string[] = [];
// Version
lines.push(`version: ${config.version}`);
if (config.setup_completed !== undefined) {
lines.push(`setup_completed: ${config.setup_completed}`);
}
lines.push('');
// Default
if (config.default) {
lines.push(`# Default profile used when running 'ccs' without arguments`);
lines.push(`default: "${config.default}"`);
lines.push('');
}
// Accounts section
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Accounts: Isolated Claude instances (each with separate auth/sessions)');
lines.push('# Manage with: ccs auth add <name>, ccs auth list, ccs auth remove <name>');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml.dump({ accounts: config.accounts }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
);
lines.push('');
// Profiles section
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Profiles: API-based providers (GLM, Kimi, custom endpoints)');
lines.push('# Each profile points to a *.settings.json file containing env vars.');
lines.push('# Edit the settings file directly to customize (ANTHROPIC_MAX_TOKENS, etc.)');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml.dump({ profiles: config.profiles }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
);
lines.push('');
// CLIProxy section
lines.push('# ----------------------------------------------------------------------------');
lines.push('# CLIProxy: OAuth-based providers (gemini, codex, agy, qwen, iflow)');
lines.push('# Each variant can reference a *.settings.json file for custom env vars.');
lines.push('# Edit the settings file directly to customize model or other settings.');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml.dump({ cliproxy: config.cliproxy }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
);
lines.push('');
if (config.proxy?.routing) {
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Proxy Routing: OpenAI-compatible local proxy model selection rules');
lines.push('# Use profile:model selectors to force a target profile and upstream model.');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml.dump({ proxy: config.proxy }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
);
lines.push('');
}
if (config.logging) {
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Logging: CCS-owned structured runtime logs');
lines.push('# Current file: ~/.ccs/logs/current.jsonl');
lines.push('# Archives rotate automatically and are pruned by retain_days.');
lines.push('# This is separate from cliproxy.logging, which controls CLIProxy runtime files.');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml.dump({ logging: config.logging }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
);
lines.push('');
}
// CLIProxy Server section (remote proxy configuration) - placed right after cliproxy
if (config.cliproxy_server) {
lines.push('# ----------------------------------------------------------------------------');
lines.push('# CLIProxy Server: Remote proxy connection settings');
lines.push('# Configure via Dashboard (`ccs config`) > Proxy tab.');
lines.push('#');
lines.push('# remote: Connect to a remote CLIProxyAPI instance');
lines.push('# fallback: Use local proxy if remote is unreachable');
lines.push('# local: Local proxy settings (port, auto-start)');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml
.dump(
{ cliproxy_server: config.cliproxy_server },
{ indent: 2, lineWidth: -1, quotingType: '"' }
)
.trim()
);
lines.push('');
}
// Preferences section
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Preferences: User settings');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml
.dump({ preferences: config.preferences }, { indent: 2, lineWidth: -1, quotingType: '"' })
.trim()
);
lines.push('');
// WebSearch section
if (config.websearch) {
lines.push('# ----------------------------------------------------------------------------');
lines.push('# WebSearch: real search backends for third-party profiles');
lines.push('# Dashboard (`ccs config`) is the source of truth for provider selection.');
lines.push('#');
lines.push('# Third-party providers (gemini, codex, agy, etc.) do not have access to');
lines.push("# Anthropic's WebSearch tool. CCS intercepts that tool and runs local search.");
lines.push('#');
lines.push(
'# Priority: Exa -> Tavily -> Brave -> DuckDuckGo -> optional legacy AI CLI fallbacks'
);
lines.push('#');
lines.push('# Exa requires EXA_API_KEY in your environment.');
lines.push('# Tavily requires TAVILY_API_KEY in your environment.');
lines.push('# Brave requires BRAVE_API_KEY in your environment.');
lines.push('# DuckDuckGo works with zero extra setup and is enabled by default.');
lines.push('#');
lines.push('# Legacy LLM fallbacks remain optional if you still want them:');
lines.push('# gemini: npm i -g @google/gemini-cli');
lines.push('# opencode: curl -fsSL https://opencode.ai/install | bash');
lines.push('# grok: npm i -g @vibe-kit/grok-cli');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml
.dump({ websearch: config.websearch }, { indent: 2, lineWidth: -1, quotingType: '"' })
.trim()
);
lines.push('');
}
// Copilot section (GitHub Copilot proxy)
if (config.copilot) {
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Copilot: GitHub Copilot API proxy (via copilot-api)');
lines.push('# Uses your existing GitHub Copilot subscription with Claude Code.');
lines.push('#');
lines.push('# !! DISCLAIMER - USE AT YOUR OWN RISK !!');
lines.push('# This uses an UNOFFICIAL reverse-engineered API.');
lines.push('# Excessive usage may trigger GitHub account restrictions.');
lines.push('# CCS provides NO WARRANTY and accepts NO RESPONSIBILITY for consequences.');
lines.push('#');
lines.push('# Setup: npx copilot-api auth (authenticate with GitHub)');
lines.push('# Usage: ccs copilot (switch to copilot profile)');
lines.push('#');
lines.push('# Models: claude-sonnet-4.5, claude-opus-4.5, gpt-5.1, gemini-2.5-pro');
lines.push('# Account types: individual, business, enterprise');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml.dump({ copilot: config.copilot }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
);
lines.push('');
}
// Cursor section (Cursor IDE proxy daemon)
if (config.cursor) {
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Cursor: Cursor IDE proxy daemon');
lines.push('# Enables Cursor IDE integration via local proxy daemon.');
lines.push('#');
lines.push('# enabled: Enable/disable Cursor integration (default: false)');
lines.push('# port: Port for cursor proxy daemon (default: 20129)');
lines.push('# auto_start: Auto-start daemon when CCS starts (default: false)');
lines.push('# ghost_mode: Disable telemetry for privacy (default: true)');
lines.push('# model: Default model ID (used for ANTHROPIC_MODEL)');
lines.push('# opus_model/sonnet_model/haiku_model: Optional tier model mapping');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml.dump({ cursor: config.cursor }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
);
lines.push('');
}
// Global env section
if (config.global_env) {
lines.push('# ----------------------------------------------------------------------------');
lines.push(
'# Global Environment Variables: Injected into all non-Claude subscription profiles'
);
lines.push('# These env vars disable telemetry/reporting for third-party providers.');
lines.push('# Configure via Dashboard (`ccs config`) > Global Env tab.');
lines.push('#');
lines.push('# Default variables:');
lines.push('# DISABLE_BUG_COMMAND: Disables /bug command (not supported by proxy)');
lines.push('# DISABLE_ERROR_REPORTING: Disables error reporting to Anthropic');
lines.push('# DISABLE_TELEMETRY: Disables usage telemetry');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml
.dump({ global_env: config.global_env }, { indent: 2, lineWidth: -1, quotingType: '"' })
.trim()
);
lines.push('');
}
// Continuity inheritance section
if (config.continuity?.inherit_from_account) {
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Continuity Inheritance: Reuse account continuity artifacts across profiles');
lines.push('# Map execution profile names to source account profiles (CLAUDE_CONFIG_DIR).');
lines.push('# Applies to Claude target only; credentials remain profile-specific.');
lines.push('# Example: continuity.inherit_from_account.glm: pro');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml
.dump({ continuity: config.continuity }, { indent: 2, lineWidth: -1, quotingType: '"' })
.trim()
);
lines.push('');
}
// Thinking section (extended thinking/reasoning configuration)
if (config.thinking) {
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Thinking: Extended thinking/reasoning budget configuration');
lines.push('# Controls reasoning depth for supported providers (agy, gemini, codex).');
lines.push('#');
lines.push(
'# Modes: auto (use tier_defaults), off (disable), manual (--thinking/--effort flags)'
);
lines.push(
'# Levels: minimal (512), low (1K), medium (8K), high (24K), xhigh (32K), max (adaptive ceiling), auto'
);
lines.push('# Override: Set global override value (number or level name)');
lines.push('# Provider overrides: Per-provider tier defaults');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml
.dump({ thinking: config.thinking }, { indent: 2, lineWidth: -1, quotingType: '"' })
.trim()
);
lines.push('');
}
// Official Channels section
if (config.channels) {
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Official Channels: Runtime auto-enable for Anthropic official channel plugins');
lines.push('# Supported channels: telegram, discord, imessage');
lines.push('# Runtime-only: CCS injects --channels at launch for compatible Claude sessions.');
lines.push('# Bot tokens live in Claude channel env files, not in config.yaml.');
lines.push('# Use selected: [telegram, discord, imessage] to choose channels.');
lines.push(
'# unattended adds --dangerously-skip-permissions only when channel auto-enable is active.'
);
lines.push('# Compatible sessions: native Claude default/account profiles only.');
lines.push('# Configure via: ccs config channels or the Settings > Channels dashboard tab.');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml
.dump({ channels: config.channels }, { indent: 2, lineWidth: -1, quotingType: '"' })
.trim()
);
lines.push('');
}
// Dashboard auth section (only if configured)
if (config.dashboard_auth?.enabled) {
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Dashboard Auth: Optional login protection for CCS dashboard');
lines.push('# Generate password hash: npx bcrypt-cli hash "your-password"');
lines.push(
'# ENV override: CCS_DASHBOARD_AUTH_ENABLED, CCS_DASHBOARD_USERNAME, CCS_DASHBOARD_PASSWORD_HASH'
);
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml
.dump(
{ dashboard_auth: config.dashboard_auth },
{ indent: 2, lineWidth: -1, quotingType: '"' }
)
.trim()
);
lines.push('');
}
// Browser automation section
if (config.browser) {
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Browser Automation: Claude browser attach and Codex browser tooling');
lines.push('# Claude attach reuses a running Chrome/Chromium session with remote debugging.');
lines.push('# Codex tooling controls whether CCS injects Playwright MCP overrides.');
lines.push('#');
lines.push('# claude.user_data_dir should point at the Chrome user-data directory for the');
lines.push('# dedicated attach session. claude.devtools_port is the expected debugging port.');
lines.push('# Configure via: Settings > Browser or `ccs browser ...`.');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml.dump({ browser: config.browser }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
);
lines.push('');
}
// Image analysis section
if (config.image_analysis) {
lines.push('# ----------------------------------------------------------------------------');
lines.push('# Image Analysis: Vision-based analysis for images and PDFs');
lines.push('# Routes Read tool requests for images/PDFs through CLIProxy vision API.');
lines.push('#');
lines.push('# When enabled: Image files trigger vision analysis instead of raw file read');
lines.push('# Provider models: Vision model used for each CLIProxy provider');
lines.push('# Timeout: Maximum seconds to wait for analysis (10-600)');
lines.push('#');
lines.push('# Supported formats: .jpg, .jpeg, .png, .gif, .webp, .heic, .bmp, .tiff, .pdf');
lines.push('# Configure via: ccs config image-analysis');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
yaml
.dump(
{ image_analysis: config.image_analysis },
{ indent: 2, lineWidth: -1, quotingType: '"' }
)
.trim()
);
lines.push('');
}
return lines.join('\n');
}
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -11,10 +11,11 @@ import * as path from 'path';
import * as http from 'http';
import { CopilotDaemonStatus } from './types';
import { CopilotConfig, DEFAULT_COPILOT_CONFIG } from '../config/unified-config-types';
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
import { getCopilotDir, getCopilotApiBinPath } from './copilot-package-manager';
import { verifyProcessOwnership } from '../cursor/daemon-process-ownership';
import { createLogger } from '../services/logging';
import { loadOrCreateUnifiedConfig } from '../config/config-loader-facade';
const logger = createLogger('copilot:daemon');
+2 -1
View File
@@ -7,7 +7,7 @@
import { spawn } from 'child_process';
import { CopilotConfig } from '../config/unified-config-types';
import { getGlobalEnvConfig } from '../config/unified-config-loader';
import { ensureCliproxyService } from '../cliproxy';
import { getEffectiveApiKey } from '../cliproxy/auth/auth-token-manager';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
@@ -36,6 +36,7 @@ import {
} from '../utils/hooks';
import { stripClaudeCodeEnv } from '../utils/shell-executor';
import { createLogger } from '../services/logging';
import { getGlobalEnvConfig } from '../config/config-loader-facade';
const logger = createLogger('copilot:executor');
+1 -1
View File
@@ -15,7 +15,7 @@ import * as path from 'path';
import { spawn, spawnSync } from 'child_process';
import { ProgressIndicator } from '../utils/progress-indicator';
import { ok, info } from '../utils/ui';
import { getCcsDir } from '../utils/config-manager';
import { getCcsDir } from '../config/config-loader-facade';
/** Cache duration for version check (1 hour in milliseconds) */
const VERSION_CACHE_DURATION_MS = 60 * 60 * 1000;
+1 -1
View File
@@ -19,7 +19,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import type { CursorCredentials, CursorAuthStatus, AutoDetectResult } from './types';
import { getCcsDir } from '../utils/config-manager';
import { getCcsDir } from '../config/config-loader-facade';
const ACCESS_TOKEN_KEYS = ['cursorAuth/accessToken', 'cursorAuth/token'] as const;
const MACHINE_ID_KEYS = [
+1 -1
View File
@@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir } from '../utils/config-manager';
import { getCcsDir } from '../config/config-loader-facade';
function getCursorDir(): string {
return path.join(getCcsDir(), 'cursor');
+2 -1
View File
@@ -1,7 +1,7 @@
import { spawn } from 'child_process';
import type { CursorConfig } from '../config/unified-config-types';
import { getGlobalEnvConfig } from '../config/unified-config-loader';
import { ensureCliproxyService } from '../cliproxy';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
import { fail, info, ok } from '../utils/ui';
@@ -15,6 +15,7 @@ import { getImageAnalysisHookEnv, resolveImageAnalysisRuntimeStatus } from '../u
import { stripClaudeCodeEnv } from '../utils/shell-executor';
import { checkAuthStatus } from './cursor-auth';
import { isDaemonRunning, startDaemon } from './cursor-daemon';
import { getGlobalEnvConfig } from '../config/config-loader-facade';
interface CursorImageAnalysisResolution {
env: Record<string, string>;
+1 -1
View File
@@ -6,7 +6,7 @@ import { ResultFormatter } from './result-formatter';
import { DelegationValidator } from '../utils/delegation-validator';
import { SettingsParser } from './settings-parser';
import { fail, warn } from '../utils/ui';
import { getCcsDir } from '../utils/config-manager';
import { getCcsDir } from '../config/config-loader-facade';
const PROFILE_FLAGS_WITH_VALUE = new Set(['-p', '--prompt', '--effort']);
+3 -2
View File
@@ -15,8 +15,8 @@ import { ui, warn, info } from '../utils/ui';
import { type ExecutionOptions, type ExecutionResult, type StreamMessage } from './executor/types';
import { StreamBuffer, formatToolVerbose } from './executor/stream-parser';
import { buildExecutionResult } from './executor/result-aggregator';
import { getCcsDir, getModelDisplayName, loadSettings } from '../utils/config-manager';
import { getGlobalEnvConfig } from '../config/unified-config-loader';
import { getModelDisplayName } from '../utils/config-manager';
import { getProfileLookupCandidates } from '../utils/profile-compat';
import {
getClaudeLaunchEnvOverrides,
@@ -58,6 +58,7 @@ import {
readWebSearchTraceRecords,
syncWebSearchMcpToConfigDir,
} from '../utils/websearch-manager';
import { getCcsDir, getGlobalEnvConfig, loadSettings } from '../config/config-loader-facade';
// Re-export types for consumers
export type { ExecutionOptions, ExecutionResult, StreamMessage } from './executor/types';
+1 -1
View File
@@ -6,7 +6,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir } from '../utils/config-manager';
import { getCcsDir } from '../config/config-loader-facade';
interface SessionData {
sessionId: string;
+2 -1
View File
@@ -1,6 +1,7 @@
import * as path from 'path';
import { getCcsDir } from '../utils/config-manager';
import type { DockerConfigSummary } from './docker-types';
import { getCcsDir } from '../config/config-loader-facade';
export const DOCKER_REMOTE_DIR = '~/.ccs/docker';
export const DOCKER_COMPOSE_SERVICE = 'ccs-cliproxy';
+2 -1
View File
@@ -11,7 +11,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { DeltaAccumulator } from './delta-accumulator';
import { getCcsDir } from '../utils/config-manager';
import { createLogger } from '../services/logging';
import {
RequestTransformer,
@@ -32,6 +32,7 @@ import {
type ThinkingSignature,
type ValidationResult,
} from './pipeline';
import { getCcsDir } from '../config/config-loader-facade';
export class GlmtTransformer {
private verbose: boolean;
+2 -1
View File
@@ -6,8 +6,9 @@ import * as fs from 'fs';
import * as path from 'path';
import { ok, fail, warn, info } from '../../utils/ui';
import { HealthCheck, IHealthChecker, createSpinner } from './types';
import { getCcsDir } from '../../utils/config-manager';
import { getClaudeConfigDir } from '../../utils/claude-config-path';
import { getCcsDir } from '../../config/config-loader-facade';
const ora = createSpinner();
+2 -1
View File
@@ -4,8 +4,9 @@
import { getEnvironmentDiagnostics } from '../environment-diagnostics';
import { ok, warn } from '../../utils/ui';
import { getCcsDir, getCcsDirSource } from '../../utils/config-manager';
import { getCcsDirSource } from '../../utils/config-manager';
import { HealthCheck, IHealthChecker, createSpinner } from './types';
import { getCcsDir } from '../../config/config-loader-facade';
const ora = createSpinner();
@@ -5,7 +5,6 @@
* Checks: enabled status, provider_models, timeout, CLIProxy availability.
*/
import { getImageAnalysisConfig } from '../../config/unified-config-loader';
import { DEFAULT_IMAGE_ANALYSIS_CONFIG } from '../../config/unified-config-types';
import {
countManagedImageAnalysisHookFiles,
@@ -17,6 +16,7 @@ import { isCliproxyRunning } from '../../cliproxy/services/stats-fetcher';
import { CLIPROXY_DEFAULT_PORT } from '../../cliproxy/config/config-generator';
import type { HealthCheck } from './types';
import { hasImageAnalyzerHook } from '../../utils/hooks/image-analyzer-hook-installer';
import { getImageAnalysisConfig } from '../../config/config-loader-facade';
/**
* Run image analysis configuration check
@@ -112,8 +112,8 @@ export async function runImageAnalysisCheck(results: HealthCheck): Promise<void>
* Fix image analysis configuration issues
*/
export async function fixImageAnalysisConfig(): Promise<boolean> {
const { updateUnifiedConfig, loadOrCreateUnifiedConfig } = await import(
'../../config/unified-config-loader'
const { updateConfig, loadOrCreateUnifiedConfig } = await import(
'../../config/config-loader-facade'
);
const config = loadOrCreateUnifiedConfig();
@@ -145,7 +145,7 @@ export async function fixImageAnalysisConfig(): Promise<boolean> {
}
if (fixed) {
updateUnifiedConfig({ image_analysis: config.image_analysis });
updateConfig({ image_analysis: config.image_analysis });
}
const repairStats = repairImageAnalysisRuntimeState();
+1 -1
View File
@@ -6,7 +6,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { ok, fail, warn, info } from '../../utils/ui';
import { HealthCheck, IHealthChecker, createSpinner } from './types';
import { getCcsDir } from '../../utils/config-manager';
import { getCcsDir } from '../../config/config-loader-facade';
const ora = createSpinner();
+1 -1
View File
@@ -7,7 +7,7 @@ import * as path from 'path';
import * as os from 'os';
import { ok, fail, warn } from '../../utils/ui';
import { HealthCheck, IHealthChecker, createSpinner } from './types';
import { getCcsDir } from '../../utils/config-manager';
import { getCcsDir } from '../../config/config-loader-facade';
const ora = createSpinner();
+1 -1
View File
@@ -8,7 +8,7 @@ import { getClaudeCliInfo } from '../../utils/claude-detector';
import { escapeShellArg, stripClaudeCodeEnv } from '../../utils/shell-executor';
import { ok, fail } from '../../utils/ui';
import { HealthCheck, IHealthChecker, createSpinner } from './types';
import { getCcsDir } from '../../utils/config-manager';
import { getCcsDir } from '../../config/config-loader-facade';
const ora = createSpinner();
+2 -1
View File
@@ -12,8 +12,9 @@ import SharedManager from './shared-manager';
import ProfileContextSyncLock from './profile-context-sync-lock';
import { DEFAULT_ACCOUNT_CONTEXT_MODE } from '../auth/account-context';
import type { AccountContextPolicy } from '../auth/account-context';
import { getCcsDir, getCcsHome } from '../utils/config-manager';
import { getCcsHome } from '../utils/config-manager';
import { createLogger } from '../services/logging';
import { getCcsDir } from '../config/config-loader-facade';
const logger = createLogger('management:instance-manager');
+5 -4
View File
@@ -7,13 +7,14 @@
import * as fs from 'fs';
import * as path from 'path';
import { info } from '../utils/ui';
import { getCcsHome, getCcsDir } from '../utils/config-manager';
import { getCcsHome } from '../utils/config-manager';
import { createEmptyUnifiedConfig, UNIFIED_CONFIG_VERSION } from '../config/unified-config-types';
import {
saveUnifiedConfig,
getCcsDir,
hasUnifiedConfig,
loadUnifiedConfig,
} from '../config/unified-config-loader';
saveConfig,
} from '../config/config-loader-facade';
/**
* Recovery Manager Class
@@ -137,7 +138,7 @@ class RecoveryManager {
config.version = UNIFIED_CONFIG_VERSION;
try {
saveUnifiedConfig(config);
saveConfig(config);
this.recovered.push(`Created ${this.ccsDir}/config.yaml`);
return true;
} catch (_saveErr) {
+2 -1
View File
@@ -5,7 +5,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { ok, warn, fail, info, header, color } from '../../utils/ui';
import { getCcsDir } from '../../utils/config-manager';
import {
CLIPROXY_DEFAULT_PORT,
configNeedsRegeneration,
@@ -16,6 +16,7 @@ import { getPortProcess, isCLIProxyProcess } from '../../utils/port-utils';
import { killProcessOnPort, getPlatformName } from '../../utils/platform-commands';
import { createSpinner } from '../checks/types';
import { fixImageAnalysisConfig } from '../checks/image-analysis-check';
import { getCcsDir } from '../../config/config-loader-facade';
const ora = createSpinner();
+2 -1
View File
@@ -13,11 +13,12 @@ import ProfileContextSyncLock from './profile-context-sync-lock';
import { ok, info, warn } from '../utils/ui';
import { DEFAULT_ACCOUNT_CONTEXT_GROUP } from '../auth/account-context';
import type { AccountContextPolicy } from '../auth/account-context';
import { getCcsDir } from '../utils/config-manager';
import {
normalizePluginMetadataContent,
normalizePluginMetadataValue,
} from './plugin-path-normalizer';
import { getCcsDir } from '../config/config-loader-facade';
export {
normalizePluginMetadataContent,
normalizePluginMetadataPathString,
+1 -1
View File
@@ -1,7 +1,7 @@
import { loadSettings } from '../utils/config-manager';
import { resolveOpenAICompatProfileConfig } from './profile-router';
import { OPENAI_COMPAT_PROXY_DEFAULT_PORT } from './proxy-daemon-paths';
import { startOpenAICompatProxyServer } from './server/proxy-server';
import { loadSettings } from '../config/config-loader-facade';
interface RuntimeOptions {
port: number;
+1 -1
View File
@@ -1,5 +1,5 @@
import * as path from 'path';
import { getCcsDir } from '../utils/config-manager';
import { getCcsDir } from '../config/config-loader-facade';
export const OPENAI_COMPAT_PROXY_LEGACY_DEFAULT_PORT = 3456;
export const OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START = 43_456;
+1 -1
View File
@@ -1,9 +1,9 @@
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
import {
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END,
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START,
OPENAI_COMPAT_PROXY_LEGACY_DEFAULT_PORT,
} from './proxy-daemon-paths';
import { loadOrCreateUnifiedConfig } from '../config/config-loader-facade';
export interface OpenAICompatProxyPortPreference {
port: number;
+1 -1
View File
@@ -1,4 +1,3 @@
import { loadConfigSafe, loadSettings } from '../utils/config-manager';
import { expandPath } from '../utils/helpers';
import type { ProxyOpenAIRequest } from './transformers/request-transformer';
import {
@@ -6,6 +5,7 @@ import {
type OpenAICompatProxyRoutingConfig,
} from './routing-config';
import { resolveOpenAICompatProfileConfig, type OpenAICompatProfileConfig } from './profile-router';
import { loadConfigSafe, loadSettings } from '../config/config-loader-facade';
export type ProxyRoutingScenario = 'default' | 'background' | 'think' | 'longContext' | 'webSearch';
+3 -2
View File
@@ -1,10 +1,11 @@
import * as fs from 'fs';
import { DEFAULT_LOGGING_CONFIG } from '../../config/unified-config-types';
import type { LoggingConfig } from './log-types';
import {
getConfigYamlPath,
getLoggingConfig as getUnifiedLoggingConfig,
} from '../../config/unified-config-loader';
import type { LoggingConfig } from './log-types';
} from '../../config/config-loader-facade';
const CACHE_RECHECK_MS = 1000;
let cachedConfig: LoggingConfig = { ...DEFAULT_LOGGING_CONFIG };
+1 -1
View File
@@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir } from '../../utils/config-manager';
import { getCcsDir } from '../../config/config-loader-facade';
const LOGS_DIR = 'logs';
const ARCHIVE_DIR = 'archive';
+4 -4
View File
@@ -1,4 +1,3 @@
import { getBrowserConfig, mutateUnifiedConfig } from '../../config/unified-config-loader';
import type { BrowserConfig } from '../../config/unified-config-types';
import { getNodePlatformKey } from './platform';
import { type BrowserStatusPayload, getBrowserStatus } from './browser-status';
@@ -9,6 +8,7 @@ import {
getRecommendedBrowserUserDataDir,
isManagedClaudeBrowserAttachConfig,
} from './browser-settings';
import { getBrowserConfig, mutateConfig } from '../../config/config-loader-facade';
export interface BrowserSetupResult {
configUpdated: boolean;
@@ -23,14 +23,14 @@ export interface BrowserSetupResult {
export interface BrowserSetupDeps {
getBrowserConfig: typeof getBrowserConfig;
mutateUnifiedConfig: typeof mutateUnifiedConfig;
mutateConfig: typeof mutateConfig;
ensureBrowserMcp: typeof ensureBrowserMcp;
getBrowserStatus: typeof getBrowserStatus;
}
const defaultBrowserSetupDeps: BrowserSetupDeps = {
getBrowserConfig,
mutateUnifiedConfig,
mutateConfig,
ensureBrowserMcp,
getBrowserStatus,
};
@@ -81,7 +81,7 @@ export async function runBrowserSetup(
function persistBrowserSetupConfig(deps: BrowserSetupDeps, currentConfig: BrowserConfig): boolean {
const before = JSON.stringify(currentConfig);
deps.mutateUnifiedConfig((config) => {
deps.mutateConfig((config) => {
const existingBrowser = config.browser ?? currentConfig;
const currentUserDataDir = existingBrowser.claude.user_data_dir?.trim();
+2 -1
View File
@@ -4,7 +4,7 @@ import type {
BrowserEvalMode,
BrowserToolPolicy,
} from '../../config/unified-config-types';
import { getBrowserConfig, loadUnifiedConfig } from '../../config/unified-config-loader';
import { getCcsPathDisplay } from '../config-manager';
import { getCodexBinaryInfo } from '../../targets/codex-detector';
import { type BrowserRuntimeEnv, resolveBrowserRuntimeEnv } from './chrome-reuse';
@@ -19,6 +19,7 @@ import {
getEffectiveClaudeBrowserAttachConfig,
getRecommendedBrowserUserDataDir,
} from './browser-settings';
import { getBrowserConfig, loadUnifiedConfig } from '../../config/config-loader-facade';
export interface ClaudeBrowserStatus {
enabled: boolean;
+1 -1
View File
@@ -6,7 +6,7 @@ import { isConfig, isSettings } from '../types';
import type { Config, Settings, CLIProxyVariantsConfig, CLIProxyVariantConfig } from '../types';
import { expandPath, error } from './helpers';
import { info } from './ui';
import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/config-loader-facade';
// TODO: Replace with proper imports after converting these files
// const { ErrorManager } = require('./error-manager');
@@ -7,7 +7,6 @@
* @module utils/hooks/image-analysis-hook-env
*/
import { getImageAnalysisConfig } from '../../config/unified-config-loader';
import { resolveCliproxyBridgeProfile } from '../../api/services/cliproxy-profile-bridge';
import { getEffectiveApiKey } from '../../cliproxy/auth/auth-token-manager';
import { mapExternalProviderName } from '../../cliproxy/provider-capabilities';
@@ -21,6 +20,7 @@ import {
resolveImageAnalysisStatus,
type ImageAnalysisResolutionContext,
} from './image-analysis-backend-resolver';
import { getImageAnalysisConfig } from '../../config/config-loader-facade';
/**
* Serialize provider_models map to env var format: provider:model,provider:model
@@ -7,8 +7,9 @@
*/
import * as path from 'path';
import { getImageAnalysisConfig } from '../../config/unified-config-loader';
import { getCcsHooksDir } from '../config-manager';
import { getImageAnalysisConfig } from '../../config/config-loader-facade';
// Hook file name
const IMAGE_ANALYZER_HOOK = 'image-analyzer-transformer.cjs';
@@ -12,9 +12,10 @@ import * as path from 'path';
import { info, warn } from '../ui';
import { getImageAnalyzerHookPath } from './image-analyzer-hook-configuration';
import { getCcsHooksDir } from '../config-manager';
import { getImageAnalysisConfig } from '../../config/unified-config-loader';
import { removeMigrationMarker } from './image-analyzer-profile-hook-injector';
import { installImageAnalysisPrompts } from '../image-analysis/hook-installer';
import { getImageAnalysisConfig } from '../../config/config-loader-facade';
// Re-export from hook-configuration for backward compatibility
export {
@@ -21,12 +21,13 @@ import {
isCcsImageAnalyzerHook,
removeCcsImageAnalyzerHooks,
} from './image-analyzer-hook-utils';
import { getImageAnalysisConfig } from '../../config/unified-config-loader';
import { getCcsDir } from '../config-manager';
import {
resolveImageAnalysisStatus,
type ImageAnalysisResolutionContext,
} from './image-analysis-backend-resolver';
import { getImageAnalysisConfig } from '../../config/config-loader-facade';
// Valid profile name pattern (alphanumeric, dot, dash, underscore only)
const VALID_PROFILE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
+2 -1
View File
@@ -5,12 +5,13 @@
import * as fs from 'fs';
import * as path from 'path';
import * as lockfile from 'proper-lockfile';
import { getImageAnalysisConfig } from '../../config/unified-config-loader';
import { getCcsDir } from '../config-manager';
import { getClaudeUserConfigPath } from '../claude-config-path';
import { info, warn } from '../ui';
import { InstanceManager } from '../../management/instance-manager';
import { installImageAnalysisPrompts } from './hook-installer';
import { getImageAnalysisConfig } from '../../config/config-loader-facade';
const IMAGE_ANALYSIS_MCP_SERVER = 'ccs-image-analysis-server.cjs';
const IMAGE_ANALYSIS_MCP_RUNTIME = 'image-analysis-runtime.cjs';
+2 -1
View File
@@ -8,8 +8,9 @@ import { spawn, spawnSync, ChildProcess, type SpawnOptions } from 'child_process
import { ErrorManager } from './error-manager';
import { getWebSearchHookEnv } from './websearch-manager';
import { wireChildProcessSignals } from './signal-forwarder';
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
import SharedManager from '../management/shared-manager';
import { loadOrCreateUnifiedConfig } from '../config/config-loader-facade';
/**
* Strip ANTHROPIC_* env vars from an environment object.
+2 -1
View File
@@ -9,10 +9,11 @@
import * as fs from 'fs';
import * as path from 'path';
import { info, warn } from '../ui';
import { getWebSearchConfig } from '../../config/unified-config-loader';
import { getCcsHooksDir } from '../config-manager';
import { getClaudeSettingsPath } from '../claude-config-path';
import { isCcsWebSearchHook, deduplicateCcsHooks } from './hook-utils';
import { getWebSearchConfig } from '../../config/config-loader-facade';
// Hook file name
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
+1 -1
View File
@@ -6,9 +6,9 @@
* @module utils/websearch/hook-env
*/
import { getWebSearchConfig } from '../../config/unified-config-loader';
import { normalizeSearxngBaseUrl } from './types';
import { resolveAllowedWebSearchTraceFile } from './trace';
import { getWebSearchConfig } from '../../config/config-loader-facade';
/**
* Get environment variables for WebSearch hook configuration.
+2 -1
View File
@@ -9,9 +9,10 @@
import * as fs from 'fs';
import * as path from 'path';
import { info, warn } from '../ui';
import { getWebSearchConfig } from '../../config/unified-config-loader';
import { getCcsDir, getCcsHooksDir } from '../config-manager';
import { getHookPath } from './hook-config';
import { getWebSearchConfig } from '../../config/config-loader-facade';
// Re-export from hook-config for backward compatibility
export { getHookPath, getWebSearchHookConfig } from './hook-config';

Some files were not shown because too many files have changed in this diff Show More