mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-06 20:24:01 +00:00
refactor(config/loader): extract defaults-merger, config-getters, polish orchestrator
Phases 4-6 of #1164. Final extractions and orchestrator cleanup: - src/config/loader/defaults-merger.ts (323 LOC): mergeWithDefaults — pure transform that fills defaults across browser, websearch, dashboard auth, image analysis, logging, cursor, continuity, official channels. - src/config/loader/config-getters.ts (339 LOC): getWebSearchConfig, getGlobalEnvConfig, getContinuityInheritanceMap, getCliproxySafetyConfig, getThinkingConfig, getOfficialChannelsConfig, isDashboardAuthEnabled, getDashboardAuthConfig, getBrowserConfig, getImageAnalysisConfig, getLoggingConfig, getCursorConfig, GeminiWebSearchInfo. Lazy require('../unified-config-loader') call-time resolution preserves spy-based test compatibility while breaking the config-getters -> orchestrator cycle. - unified-config-loader.ts: rewritten as 268-LOC orchestrator. Hosts core load/save/mutate logic + re-exports of all moved symbols for backwards compat with codebase imports. Forward-reference callbacks in io-locks.ts kept as-is — defaults-merger and normalizers don't import from io-locks (no cycle), but simplifying io-locks's internal callback API risks lock-semantics regression. Deferred per KISS. unified-config-loader.ts: 1508 -> 268 LOC (-1240, -82%). Behavior unchanged; full suite passes 1824/1824. Refs #1164
This commit is contained in:
@@ -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 };
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Unified Config Loader
|
* Unified Config Loader — orchestrator
|
||||||
*
|
*
|
||||||
* Loads and saves the unified YAML configuration.
|
* Loads and saves the unified YAML configuration.
|
||||||
* Provides fallback to legacy JSON format for backward compatibility.
|
* Provides fallback to legacy JSON format for backward compatibility.
|
||||||
*
|
*
|
||||||
* Phase 1-3 refactor (issue #1164): io-locks, normalizers, and yaml-serializer
|
* Phase 1-6 refactor (issue #1164):
|
||||||
* have been extracted to src/config/loader/. This file re-exports everything
|
* Phase 1 → src/config/loader/io-locks.ts
|
||||||
* needed by callers so existing import sites continue to work unchanged.
|
* Phase 2 → src/config/loader/normalizers.ts
|
||||||
|
* Phase 3 → src/config/loader/yaml-serializer.ts
|
||||||
|
* Phase 4 → src/config/loader/defaults-merger.ts
|
||||||
|
* Phase 5 → src/config/loader/config-getters.ts
|
||||||
|
*
|
||||||
|
* This file re-exports the full public API so all existing import sites
|
||||||
|
* continue to work without modification.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
@@ -15,37 +21,14 @@ import {
|
|||||||
isUnifiedConfig,
|
isUnifiedConfig,
|
||||||
createEmptyUnifiedConfig,
|
createEmptyUnifiedConfig,
|
||||||
UNIFIED_CONFIG_VERSION,
|
UNIFIED_CONFIG_VERSION,
|
||||||
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_OFFICIAL_CHANNELS_CONFIG,
|
|
||||||
DEFAULT_DASHBOARD_AUTH_CONFIG,
|
|
||||||
DEFAULT_IMAGE_ANALYSIS_CONFIG,
|
|
||||||
DEFAULT_LOGGING_CONFIG,
|
|
||||||
} from './unified-config-types';
|
|
||||||
import type {
|
|
||||||
UnifiedConfig,
|
|
||||||
CLIProxySafetyConfig,
|
|
||||||
GlobalEnvConfig,
|
|
||||||
ThinkingConfig,
|
|
||||||
OfficialChannelsConfig,
|
|
||||||
DashboardAuthConfig,
|
|
||||||
BrowserConfig,
|
|
||||||
ImageAnalysisConfig,
|
|
||||||
LoggingConfig,
|
|
||||||
CursorConfig,
|
|
||||||
} from './unified-config-types';
|
} from './unified-config-types';
|
||||||
|
import type { UnifiedConfig } from './unified-config-types';
|
||||||
import { isUnifiedConfigEnabled } from './feature-flags';
|
import { isUnifiedConfigEnabled } from './feature-flags';
|
||||||
import { normalizeOfficialChannelIds } from '../channels/official-channels-runtime';
|
|
||||||
import { canonicalizeImageAnalysisConfig } from '../utils/hooks/image-analysis-backend-resolver';
|
|
||||||
import { normalizeSearxngBaseUrl } from '../utils/websearch/types';
|
|
||||||
|
|
||||||
// Phase 1: io-locks
|
// ---------------------------------------------------------------------------
|
||||||
|
// Phase 1 re-exports: io-locks
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
CONFIG_YAML,
|
CONFIG_YAML,
|
||||||
CONFIG_JSON,
|
CONFIG_JSON,
|
||||||
@@ -71,7 +54,10 @@ import {
|
|||||||
writeUnifiedConfigWithLockHeld,
|
writeUnifiedConfigWithLockHeld,
|
||||||
} from './loader/io-locks';
|
} from './loader/io-locks';
|
||||||
|
|
||||||
// Phase 2: normalizers
|
// ---------------------------------------------------------------------------
|
||||||
|
// Phase 2 re-exports: normalizers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
normalizeBrowserDevtoolsPort,
|
normalizeBrowserDevtoolsPort,
|
||||||
normalizeBrowserPolicy,
|
normalizeBrowserPolicy,
|
||||||
@@ -84,21 +70,44 @@ export {
|
|||||||
normalizeContinuityConfig,
|
normalizeContinuityConfig,
|
||||||
normalizeOfficialChannelsConfig,
|
normalizeOfficialChannelsConfig,
|
||||||
} from './loader/normalizers';
|
} from './loader/normalizers';
|
||||||
import type { LegacyDiscordChannelsConfig } from './loader/normalizers';
|
import { canonicalizeBrowserConfig, validateCompositeVariants } from './loader/normalizers';
|
||||||
import {
|
|
||||||
canonicalizeBrowserConfig,
|
// ---------------------------------------------------------------------------
|
||||||
validateCompositeVariants,
|
// Phase 3 re-exports: yaml-serializer
|
||||||
normalizeContinuityConfig,
|
// ---------------------------------------------------------------------------
|
||||||
normalizeOfficialChannelsConfig,
|
|
||||||
normalizeSessionAffinityTtl,
|
|
||||||
} from './loader/normalizers';
|
|
||||||
|
|
||||||
// Phase 3: yaml-serializer
|
|
||||||
export { generateYamlHeader, generateYamlWithComments } from './loader/yaml-serializer';
|
export { generateYamlHeader, generateYamlWithComments } from './loader/yaml-serializer';
|
||||||
import { generateYamlHeader, generateYamlWithComments } from './loader/yaml-serializer';
|
import { generateYamlHeader, generateYamlWithComments } from './loader/yaml-serializer';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// getConfigFormat (depends on hasUnifiedConfig, hasLegacyConfig, isUnifiedConfigEnabled)
|
// Phase 4 re-exports: defaults-merger
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export { mergeWithDefaults } from './loader/defaults-merger';
|
||||||
|
import { mergeWithDefaults } from './loader/defaults-merger';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Phase 5 re-exports: config-getters
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type { GeminiWebSearchInfo } from './loader/config-getters';
|
||||||
|
export {
|
||||||
|
getWebSearchConfig,
|
||||||
|
getGlobalEnvConfig,
|
||||||
|
getContinuityInheritanceMap,
|
||||||
|
getCliproxySafetyConfig,
|
||||||
|
getThinkingConfig,
|
||||||
|
getOfficialChannelsConfig,
|
||||||
|
isDashboardAuthEnabled,
|
||||||
|
getDashboardAuthConfig,
|
||||||
|
getBrowserConfig,
|
||||||
|
getImageAnalysisConfig,
|
||||||
|
getLoggingConfig,
|
||||||
|
getCursorConfig,
|
||||||
|
} from './loader/config-getters';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// getConfigFormat
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,295 +124,7 @@ export function getConfigFormat(): 'yaml' | 'json' | 'none' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// mergeWithDefaults (Phase 4 territory — kept here until then)
|
// Core load / save / mutate
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge partial config with defaults.
|
|
||||||
* Preserves existing data while filling in missing sections.
|
|
||||||
*/
|
|
||||||
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,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Public API — load / save / mutate
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -414,7 +135,6 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
|
|||||||
export function loadUnifiedConfig(): UnifiedConfig | null {
|
export function loadUnifiedConfig(): UnifiedConfig | null {
|
||||||
const yamlPath = getConfigYamlPath();
|
const yamlPath = getConfigYamlPath();
|
||||||
|
|
||||||
// If file doesn't exist, return null
|
|
||||||
if (!fs.existsSync(yamlPath)) {
|
if (!fs.existsSync(yamlPath)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -429,7 +149,6 @@ export function loadUnifiedConfig(): UnifiedConfig | null {
|
|||||||
|
|
||||||
// Auto-upgrade if version is outdated (regenerates YAML with new comments and fields)
|
// Auto-upgrade if version is outdated (regenerates YAML with new comments and fields)
|
||||||
if ((parsed.version ?? 1) < UNIFIED_CONFIG_VERSION) {
|
if ((parsed.version ?? 1) < UNIFIED_CONFIG_VERSION) {
|
||||||
// Merge with defaults to add new fields (e.g., model for websearch providers)
|
|
||||||
const upgraded = mergeWithDefaults(parsed);
|
const upgraded = mergeWithDefaults(parsed);
|
||||||
upgraded.version = UNIFIED_CONFIG_VERSION;
|
upgraded.version = UNIFIED_CONFIG_VERSION;
|
||||||
try {
|
try {
|
||||||
@@ -474,16 +193,11 @@ export function loadUnifiedConfig(): UnifiedConfig | null {
|
|||||||
export function loadOrCreateUnifiedConfig(): UnifiedConfig {
|
export function loadOrCreateUnifiedConfig(): UnifiedConfig {
|
||||||
const existing = loadUnifiedConfig();
|
const existing = loadUnifiedConfig();
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// Merge with defaults to fill any missing sections
|
|
||||||
const merged = mergeWithDefaults(existing);
|
const merged = mergeWithDefaults(existing);
|
||||||
// Validate composite variant provider strings
|
|
||||||
validateCompositeVariants(merged);
|
validateCompositeVariants(merged);
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
return createEmptyUnifiedConfig();
|
||||||
// Create empty config
|
|
||||||
const config = createEmptyUnifiedConfig();
|
|
||||||
return config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -527,301 +241,28 @@ export function updateUnifiedConfig(updates: Partial<UnifiedConfig>): UnifiedCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Public API — getters / derived helpers
|
// Derived helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if unified config mode is active.
|
* Check if unified config mode is active.
|
||||||
* Returns true if config.yaml exists OR CCS_UNIFIED_CONFIG=1.
|
* Returns true if config.yaml exists OR CCS_UNIFIED_CONFIG=1.
|
||||||
*
|
|
||||||
* Use this centralized function instead of duplicating the logic.
|
|
||||||
*/
|
*/
|
||||||
export function isUnifiedMode(): boolean {
|
export function isUnifiedMode(): boolean {
|
||||||
return hasUnifiedConfig() || isUnifiedConfigEnabled();
|
return hasUnifiedConfig() || isUnifiedConfigEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or set default profile name.
|
* Get default profile name from config.
|
||||||
*/
|
*/
|
||||||
export function getDefaultProfile(): string | undefined {
|
export function getDefaultProfile(): string | undefined {
|
||||||
const config = loadUnifiedConfig();
|
const config = loadUnifiedConfig();
|
||||||
return config?.default;
|
return config?.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set default profile name in config.
|
||||||
|
*/
|
||||||
export function setDefaultProfile(name: string): void {
|
export function setDefaultProfile(name: string): void {
|
||||||
updateUnifiedConfig({ default: name });
|
updateUnifiedConfig({ default: name });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gemini CLI WebSearch configuration
|
|
||||||
*/
|
|
||||||
export interface GeminiWebSearchInfo {
|
|
||||||
enabled: boolean;
|
|
||||||
model: string;
|
|
||||||
timeout: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = loadOrCreateUnifiedConfig();
|
|
||||||
|
|
||||||
// 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 = loadOrCreateUnifiedConfig();
|
|
||||||
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 = loadOrCreateUnifiedConfig();
|
|
||||||
return config.continuity?.inherit_from_account ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cliproxy safety configuration.
|
|
||||||
* Returns defaults if not configured.
|
|
||||||
*/
|
|
||||||
export function getCliproxySafetyConfig(): CLIProxySafetyConfig {
|
|
||||||
const config = loadOrCreateUnifiedConfig();
|
|
||||||
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 = loadOrCreateUnifiedConfig();
|
|
||||||
|
|
||||||
// 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 = loadOrCreateUnifiedConfig();
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get dashboard_auth configuration with ENV var override.
|
|
||||||
* 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 = loadOrCreateUnifiedConfig();
|
|
||||||
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 = loadOrCreateUnifiedConfig();
|
|
||||||
|
|
||||||
// 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 = loadOrCreateUnifiedConfig();
|
|
||||||
return canonicalizeBrowserConfig(config.browser);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get image_analysis configuration.
|
|
||||||
* Returns defaults if not configured.
|
|
||||||
*/
|
|
||||||
export function getImageAnalysisConfig(): ImageAnalysisConfig {
|
|
||||||
const config = loadOrCreateUnifiedConfig();
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLoggingConfig(): LoggingConfig {
|
|
||||||
const config = loadOrCreateUnifiedConfig();
|
|
||||||
|
|
||||||
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 = loadOrCreateUnifiedConfig();
|
|
||||||
return config.cursor ?? { ...DEFAULT_CURSOR_CONFIG };
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user