feat(cliproxy): refactor config loading and enhance base settings support

- Introduce `BaseConfigLoader` for centralized configuration management
- Add `base-agy.settings.json` configuration
- Refactor `ConfigGenerator` to utilize new loading patterns
- Update help command with improved documentation
- Clean up lint violations file
- Enhance postinstall script
This commit is contained in:
kaitranntt
2025-11-28 20:43:11 -05:00
parent 50d32ca7da
commit e54d401656
16 changed files with 262 additions and 111 deletions
+10
View File
@@ -0,0 +1,10 @@
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/agy",
"ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed",
"ANTHROPIC_MODEL": "gemini-3-pro-preview",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "gemini-3-pro-preview",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "gemini-3-pro-preview",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gemini-2.5-flash"
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/gemini",
"ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed",
"ANTHROPIC_MODEL": "gemini-2.5-pro",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "gemini-3-pro-preview",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "gemini-2.5-pro",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "gemini-2.5-pro",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gemini-2.5-flash"
}
-1
View File
@@ -1 +0,0 @@
$ eslint src/
+12 -1
View File
@@ -121,6 +121,8 @@ function createConfigFiles() {
// This gives users control over when to modify their Claude configuration
// Create config.json if missing
// NOTE: gemini/codex profiles NOT included - they are added on-demand when user
// runs `ccs gemini` or `ccs codex` for first time (requires OAuth auth first)
const configPath = path.join(ccsDir, 'config.json');
if (!fs.existsSync(configPath)) {
const config = {
@@ -145,12 +147,17 @@ function createConfigFiles() {
if (!config.profiles) {
config.profiles = {};
}
let configUpdated = false;
if (!config.profiles.glmt) {
config.profiles.glmt = '~/.ccs/glmt.settings.json';
configUpdated = true;
}
// NOTE: gemini/codex profiles added on-demand, not during migration
if (configUpdated) {
const tmpPath = `${configPath}.tmp`;
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
fs.renameSync(tmpPath, configPath);
console.log('[OK] Updated config with GLMT profile');
console.log('[OK] Updated config with glmt profile');
} else {
console.log('[OK] Config exists: ~/.ccs/config.json (preserved)');
}
@@ -299,6 +306,10 @@ function createConfigFiles() {
console.log('[OK] Kimi profile exists: ~/.ccs/kimi.settings.json (preserved)');
}
// NOTE: gemini.settings.json and codex.settings.json are NOT created during install
// They are created on-demand when user runs `ccs gemini` or `ccs codex` for the first time
// This prevents confusion - users need to run `--auth` first anyway
// Migrate existing Kimi configs to remove deprecated model fields (v4.1.2)
// Kimi API changed - model fields now cause 401 errors
if (fs.existsSync(kimiSettingsPath)) {
+2 -2
View File
@@ -14,7 +14,7 @@ import { Config, Settings, ProfileMetadata } from '../types';
export type ProfileType = 'settings' | 'account' | 'cliproxy' | 'default';
/** CLIProxy profile names (OAuth-based, zero config) */
export const CLIPROXY_PROFILES = ['gemini', 'codex', 'qwen'] as const;
export const CLIPROXY_PROFILES = ['gemini', 'codex', 'agy'] as const;
export type CLIProxyProfileName = (typeof CLIPROXY_PROFILES)[number];
export interface ProfileDetectionResult {
@@ -92,7 +92,7 @@ class ProfileDetector {
return this.resolveDefaultProfile();
}
// Priority 0: Check CLIProxy profiles (gemini, chatgpt, qwen) - OAuth-based, zero config
// Priority 0: Check CLIProxy profiles (gemini, codex, agy) - OAuth-based, zero config
if (CLIPROXY_PROFILES.includes(profileName as CLIProxyProfileName)) {
return {
type: 'cliproxy',
+1 -1
View File
@@ -295,7 +295,7 @@ async function main(): Promise<void> {
const profileInfo = detector.detectProfileType(profile);
if (profileInfo.type === 'cliproxy') {
// CLIPROXY FLOW: OAuth-based profiles (gemini, chatgpt, qwen)
// CLIPROXY FLOW: OAuth-based profiles (gemini, codex, agy)
await execClaudeWithCLIProxy(claudeCli, profileInfo.name as CLIProxyProvider, remainingArgs);
} else if (profileInfo.type === 'settings') {
// Check if this is GLMT profile (requires proxy)
+8 -8
View File
@@ -1,7 +1,7 @@
/**
* Auth Handler for CLIProxyAPI
*
* Manages OAuth authentication for CLIProxy providers (Gemini, Codex, Qwen).
* Manages OAuth authentication for CLIProxy providers (Gemini, Codex, Antigravity).
* CLIProxyAPI handles OAuth internally - we just need to:
* 1. Check if auth exists (token files in CCS auth directory)
* 2. Trigger OAuth flow by spawning binary with auth flag
@@ -93,12 +93,12 @@ const OAUTH_CONFIGS: Record<CLIProxyProvider, ProviderOAuthConfig> = {
scopes: ['openid', 'profile'],
authFlag: '-codex-login',
},
qwen: {
provider: 'qwen',
displayName: 'Alibaba Qwen',
authUrl: 'https://auth.aliyun.com/oauth2/authorize',
scopes: ['dashscope'],
authFlag: '-qwen-login',
agy: {
provider: 'agy',
displayName: 'Antigravity',
authUrl: 'https://antigravity.ai/oauth/authorize',
scopes: ['api'],
authFlag: '-antigravity-login',
},
};
@@ -181,7 +181,7 @@ export function getAuthStatus(provider: CLIProxyProvider): AuthStatus {
* Get auth status for all providers
*/
export function getAllAuthStatus(): AuthStatus[] {
const providers: CLIProxyProvider[] = ['gemini', 'codex', 'qwen'];
const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy'];
return providers.map(getAuthStatus);
}
+121
View File
@@ -0,0 +1,121 @@
/**
* Base Config Loader for CLIProxy Providers
*
* Loads provider configurations from config/base-{provider}.settings.json files.
* This allows model mappings to be easily updated without digging into code.
*
* Config files are bundled with the npm package and read at runtime.
*/
import * as fs from 'fs';
import * as path from 'path';
import { CLIProxyProvider, ProviderModelMapping } from './types';
/** Base settings file structure */
interface BaseSettings {
env: {
ANTHROPIC_BASE_URL: string;
ANTHROPIC_AUTH_TOKEN: string;
ANTHROPIC_MODEL: string;
ANTHROPIC_DEFAULT_OPUS_MODEL: string;
ANTHROPIC_DEFAULT_SONNET_MODEL: string;
ANTHROPIC_DEFAULT_HAIKU_MODEL: string;
};
}
/** Cached configs to avoid repeated file reads */
const configCache: Map<CLIProxyProvider, BaseSettings> = new Map();
/**
* Get path to base config file for provider
* Config files are in the config/ directory relative to package root
*/
function getBaseConfigPath(provider: CLIProxyProvider): string {
// __dirname points to dist/cliproxy at runtime
// Config files are at package root: ../config/
const configDir = path.join(__dirname, '..', '..', 'config');
return path.join(configDir, `base-${provider}.settings.json`);
}
/**
* Load base config for a provider
* Returns parsed settings from config/base-{provider}.settings.json
*/
export function loadBaseConfig(provider: CLIProxyProvider): BaseSettings {
// Check cache first
const cached = configCache.get(provider);
if (cached) {
return cached;
}
const configPath = getBaseConfigPath(provider);
if (!fs.existsSync(configPath)) {
throw new Error(
`Base config not found for provider '${provider}': ${configPath}\n` +
`Expected file: config/base-${provider}.settings.json`
);
}
try {
const content = fs.readFileSync(configPath, 'utf-8');
const settings: BaseSettings = JSON.parse(content);
// Validate required fields
if (!settings.env || typeof settings.env !== 'object') {
throw new Error('Missing or invalid "env" object');
}
const required = [
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
];
for (const field of required) {
if (!settings.env[field as keyof BaseSettings['env']]) {
throw new Error(`Missing required field: env.${field}`);
}
}
// Cache and return
configCache.set(provider, settings);
return settings;
} catch (error) {
const err = error as Error;
throw new Error(`Failed to load base config for '${provider}': ${err.message}`);
}
}
/**
* Get model mapping from base config
* Extracts model names from env vars
*/
export function getModelMappingFromConfig(provider: CLIProxyProvider): ProviderModelMapping {
const config = loadBaseConfig(provider);
return {
defaultModel: config.env.ANTHROPIC_MODEL,
claudeModel: config.env.ANTHROPIC_MODEL,
opusModel: config.env.ANTHROPIC_DEFAULT_OPUS_MODEL,
sonnetModel: config.env.ANTHROPIC_DEFAULT_SONNET_MODEL,
haikuModel: config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL,
};
}
/**
* Get full env vars from base config
* Returns the complete env object for Claude CLI
*/
export function getEnvVarsFromConfig(provider: CLIProxyProvider): NodeJS.ProcessEnv {
const config = loadBaseConfig(provider);
return config.env as unknown as NodeJS.ProcessEnv;
}
/**
* Clear config cache (useful for testing)
*/
export function clearConfigCache(): void {
configCache.clear();
}
+1 -1
View File
@@ -80,7 +80,7 @@ async function waitForProxyReady(
* Execute Claude CLI with CLIProxy (main entry point)
*
* @param claudeCli Path to Claude CLI executable
* @param provider CLIProxy provider (gemini, chatgpt, qwen)
* @param provider CLIProxy provider (gemini, codex, agy)
* @param args Arguments to pass to Claude CLI
* @param config Optional executor configuration
*/
+26 -46
View File
@@ -3,12 +3,16 @@
*
* Generates config.yaml for CLIProxyAPI based on provider.
* Handles OAuth token paths and provider-specific settings.
*
* Model mappings are loaded from config/base-{provider}.settings.json files
* to allow easy updates without code changes.
*/
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir } from '../utils/config-manager';
import { CLIProxyProvider, ProviderConfig, ProviderModelMapping } from './types';
import { getModelMappingFromConfig } from './base-config-loader';
/** Settings file structure for user overrides */
interface ProviderSettings {
@@ -21,61 +25,37 @@ export const CLIPROXY_DEFAULT_PORT = 8317;
/** Internal API key for CCS-managed requests */
const CCS_INTERNAL_API_KEY = 'ccs-internal-managed';
/**
* Provider configurations with model mappings
*/
export const PROVIDER_CONFIGS: Record<CLIProxyProvider, ProviderConfig> = {
gemini: {
name: 'gemini',
displayName: 'Gemini',
models: {
defaultModel: 'gemini-2.5-pro',
claudeModel: 'gemini-2.5-pro',
opusModel: 'gemini-3-pro-preview',
sonnetModel: 'gemini-2.5-pro',
haikuModel: 'gemini-2.5-flash',
},
requiresOAuth: true,
},
codex: {
name: 'codex',
displayName: 'Codex',
models: {
defaultModel: 'gpt-5.1-codex-max',
claudeModel: 'gpt-5.1-codex-max',
opusModel: 'gpt-5.1-codex-max-high',
sonnetModel: 'gpt-5.1-codex-max',
haikuModel: 'gpt-5.1-codex-mini-high',
},
requiresOAuth: true,
},
qwen: {
name: 'qwen',
displayName: 'Qwen',
models: {
defaultModel: 'qwen-max',
claudeModel: 'qwen-max',
opusModel: 'qwen-max',
sonnetModel: 'qwen-plus',
haikuModel: 'qwen-turbo',
},
requiresOAuth: true,
},
/** Provider display names (static metadata) */
const PROVIDER_DISPLAY_NAMES: Record<CLIProxyProvider, string> = {
gemini: 'Gemini',
codex: 'Codex',
agy: 'Antigravity',
};
/**
* Get provider configuration
* Model mappings are loaded from config/base-{provider}.settings.json
*/
export function getProviderConfig(provider: CLIProxyProvider): ProviderConfig {
const config = PROVIDER_CONFIGS[provider];
if (!config) {
const displayName = PROVIDER_DISPLAY_NAMES[provider];
if (!displayName) {
throw new Error(`Unknown provider: ${provider}`);
}
return config;
// Load models from base config file
const models = getModelMappingFromConfig(provider);
return {
name: provider,
displayName,
models,
requiresOAuth: true, // All CLIProxy providers require OAuth
};
}
/**
* Get model mapping for provider
* Loads from config/base-{provider}.settings.json
*/
export function getModelMapping(provider: CLIProxyProvider): ProviderModelMapping {
return getProviderConfig(provider).models;
@@ -124,7 +104,7 @@ export function getBinDir(): string {
/**
* Generate UNIFIED config.yaml content for ALL providers
* This enables concurrent usage of gemini/codex/qwen without config conflicts.
* This enables concurrent usage of gemini/codex/agy without config conflicts.
* CLIProxyAPI routes requests by model name to the appropriate provider.
*/
function generateUnifiedConfigContent(port: number = CLIPROXY_DEFAULT_PORT): string {
@@ -132,7 +112,7 @@ function generateUnifiedConfigContent(port: number = CLIPROXY_DEFAULT_PORT): str
// Unified config with all providers
const config = `# CLIProxyAPI unified config generated by CCS
# Supports: gemini, codex, qwen (concurrent usage)
# Supports: gemini, codex, agy (concurrent usage)
# Generated: ${new Date().toISOString()}
port: ${port}
@@ -144,7 +124,7 @@ usage-statistics-enabled: false
api-keys:
- "${CCS_INTERNAL_API_KEY}"
# OAuth tokens stored in subdirectories (gemini/, codex/, qwen/)
# OAuth tokens stored in auth/ directory
# CLIProxyAPI auto-discovers auth files in subdirectories
auth-dir: "${authDir}"
+8 -1
View File
@@ -58,10 +58,17 @@ export {
getBinDir,
configExists,
deleteConfig,
PROVIDER_CONFIGS,
CLIPROXY_DEFAULT_PORT,
} from './config-generator';
// Base config loader (for reading config/base-*.settings.json)
export {
loadBaseConfig,
getModelMappingFromConfig,
getEnvVarsFromConfig,
clearConfigCache,
} from './base-config-loader';
// Executor
export { execClaudeWithCLIProxy, isPortAvailable, findAvailablePort } from './cliproxy-executor';
+4 -1
View File
@@ -107,8 +107,11 @@ export interface DownloadResult {
/**
* Supported CLIProxy providers
* - gemini: Google Gemini via OAuth
* - codex: OpenAI Codex via OAuth
* - agy: Antigravity via OAuth (short name for easy usage)
*/
export type CLIProxyProvider = 'gemini' | 'codex' | 'qwen';
export type CLIProxyProvider = 'gemini' | 'codex' | 'agy';
/**
* CLIProxy config.yaml structure (minimal)
+56 -45
View File
@@ -4,9 +4,7 @@ import { colored } from '../utils/helpers';
* Display comprehensive help information for CCS (Claude Code Switch)
*/
export function handleHelpCommand(): void {
console.log(
colored('CCS (Claude Code Switch) - Instant profile switching for Claude CLI', 'bold')
);
console.log(colored('CCS (Claude Code Switch) - Profile switching for Claude CLI', 'bold'));
console.log('');
console.log(colored('Usage:', 'cyan'));
@@ -14,43 +12,61 @@ export function handleHelpCommand(): void {
console.log(` ${colored('ccs', 'yellow')} [flags]`);
console.log('');
console.log(colored('Description:', 'cyan'));
console.log(' Switch between multiple Claude accounts and alternative models');
console.log(' (GLM, Kimi) instantly. Run different Claude CLI sessions concurrently');
console.log(' with auto-recovery. Zero downtime.');
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 1: API KEY MODELS
// ═══════════════════════════════════════════════════════════════════════════
console.log(colored('═══ API Key Models ═══', 'cyanBold'));
console.log(' Configure API keys in ~/.ccs/*.settings.json');
console.log('');
console.log(colored('Requirements:', 'cyan'));
console.log(' Node.js 14+ (detected automatically by bootstrap)');
console.log(' npm 5.2+ (for npx, comes with Node.js 8.2+)');
console.log('');
console.log(colored('Model Switching:', 'cyan'));
console.log(` ${colored('ccs', 'yellow')} Use default Claude account`);
console.log(
` ${colored('ccs codex', 'yellow')} Codex via OAuth (zero config)`
);
console.log(
` ${colored('ccs gemini', 'yellow')} Gemini via OAuth (zero config)`
);
console.log(` ${colored('ccs glm', 'yellow')} GLM 4.6 (API key required)`);
console.log(` ${colored('ccs glmt', 'yellow')} GLM with thinking mode`);
console.log(` ${colored('ccs kimi', 'yellow')} Kimi for Coding (API key)`);
console.log(
` ${colored('ccs qwen', 'yellow')} Qwen via OAuth (zero config)`
);
console.log(` ${colored('ccs gemini', 'yellow')} "explain this" Use Gemini with prompt`);
console.log('');
console.log(colored('Account Management:', 'cyan'));
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 2: ACCOUNT MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
console.log(colored('═══ Account Management ═══', 'cyanBold'));
console.log(' Run multiple Claude accounts concurrently');
console.log('');
console.log(
` ${colored('ccs auth --help', 'yellow')} Run multiple Claude accounts concurrently`
` ${colored('ccs auth --help', 'yellow')} Show account management commands`
);
console.log(` ${colored('ccs auth create <name>', 'yellow')} Create new account profile`);
console.log(` ${colored('ccs auth list', 'yellow')} List all account profiles`);
console.log('');
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 3: CLI PROXY (OAUTH PROVIDERS)
// ═══════════════════════════════════════════════════════════════════════════
console.log(colored('═══ CLI Proxy (OAuth Providers) ═══', 'cyanBold'));
console.log(' Zero-config OAuth authentication via CLIProxyAPI');
console.log(' First run: Browser opens for authentication');
console.log(' Settings: ~/.ccs/{provider}.settings.json (created after auth)');
console.log('');
console.log(
` ${colored('ccs gemini', 'yellow')} Google Gemini (gemini-2.5-pro)`
);
console.log(
` ${colored('ccs codex', 'yellow')} OpenAI Codex (gpt-5.1-codex-max)`
);
console.log(
` ${colored('ccs agy', 'yellow')} Antigravity (gemini-3-pro-preview)`
);
console.log('');
console.log(` ${colored('ccs gemini --auth', 'yellow')} Authenticate only`);
console.log(` ${colored('ccs gemini --logout', 'yellow')} Clear authentication`);
console.log(` ${colored('ccs gemini --headless', 'yellow')} Headless auth (for SSH)`);
console.log(` ${colored('ccs gemini "explain code"', 'yellow')} Use with prompt`);
console.log('');
// ═══════════════════════════════════════════════════════════════════════════
// DELEGATION
// ═══════════════════════════════════════════════════════════════════════════
console.log(colored('Delegation (inside Claude Code CLI):', 'cyan'));
console.log(
` ${colored('/ccs "task"', 'yellow')} Delegate task (auto-selects best profile)`
` ${colored('/ccs "task"', 'yellow')} Delegate task (auto-selects profile)`
);
console.log(
` ${colored('/ccs --glm "task"', 'yellow')} Force GLM-4.6 for simple tasks`
@@ -59,7 +75,6 @@ export function handleHelpCommand(): void {
console.log(
` ${colored('/ccs:continue "follow-up"', 'yellow')} Continue last delegation session`
);
console.log(' Save tokens by delegating simple tasks to cost-optimized models');
console.log('');
console.log(colored('Diagnostics:', 'cyan'));
@@ -87,38 +102,34 @@ export function handleHelpCommand(): void {
console.log(' Profiles: ~/.ccs/profiles.json');
console.log(' Instances: ~/.ccs/instances/');
console.log(' Settings: ~/.ccs/*.settings.json');
console.log(' Environment: CCS_CONFIG (override config path)');
console.log('');
console.log(colored('CLI Proxy:', 'cyan'));
console.log(' Binary: ~/.ccs/cliproxy/bin/cli-proxy-api');
console.log(' Config: ~/.ccs/cliproxy/config.yaml');
console.log(' Auth: ~/.ccs/cliproxy/auth/');
console.log(' Port: 8317 (default)');
console.log('');
console.log(colored('Shared Data:', 'cyan'));
console.log(' Commands: ~/.ccs/shared/commands/');
console.log(' Skills: ~/.ccs/shared/skills/');
console.log(' Agents: ~/.ccs/shared/agents/');
console.log(' Plugins: ~/.ccs/shared/plugins/');
console.log(' Note: Commands, skills, agents, and plugins are symlinked across all profiles');
console.log(' Note: Symlinked across all profiles');
console.log('');
console.log(colored('Examples:', 'cyan'));
console.log(` ${colored('$ ccs', 'yellow')} # Use default account`);
console.log(` ${colored('$ ccs gemini "explain code"', 'yellow')} # Zero-config OAuth`);
console.log(` ${colored('$ ccs glm "implement API"', 'yellow')} # Cost-optimized model`);
console.log('');
console.log(
` For more: ${colored('https://github.com/kaitranntt/ccs/blob/main/README.md', 'cyan')}`
` ${colored('$ ccs gemini', 'yellow')} # OAuth (browser opens first time)`
);
console.log(` ${colored('$ ccs glm "implement API"', 'yellow')} # API key model`);
console.log('');
console.log(` Docs: ${colored('https://github.com/kaitranntt/ccs', 'cyan')}`);
console.log('');
console.log(colored('Uninstall:', 'yellow'));
console.log(' npm (recommended): npm uninstall -g @kaitranntt/ccs');
console.log(' Legacy (deprecated):');
console.log(' macOS/Linux: curl -fsSL ccs.kaitran.ca/uninstall | bash');
console.log(' Windows: irm ccs.kaitran.ca/uninstall | iex');
console.log('');
console.log(colored('Documentation:', 'cyan'));
console.log(` GitHub: ${colored('https://github.com/kaitranntt/ccs', 'cyan')}`);
console.log(' Docs: https://github.com/kaitranntt/ccs/blob/main/README.md');
console.log(' Issues: https://github.com/kaitranntt/ccs/issues');
console.log(' npm uninstall -g @kaitranntt/ccs');
console.log('');
console.log(`${colored('License:', 'cyan')} MIT`);
+1 -1
View File
@@ -754,7 +754,7 @@ class Doctor {
}
/**
* Check 11: CLIProxy health (OAuth profiles: gemini, chatgpt, qwen)
* Check 11: CLIProxy health (OAuth profiles: gemini, codex, agy)
*/
private async checkCLIProxy(): Promise<void> {
// 1. Binary installed?
+9 -1
View File
@@ -18,7 +18,15 @@ export enum LogLevel {
/**
* Color codes (TTY-aware)
*/
export type ColorName = 'red' | 'green' | 'yellow' | 'blue' | 'cyan' | 'bold' | 'reset';
export type ColorName =
| 'red'
| 'green'
| 'yellow'
| 'blue'
| 'cyan'
| 'bold'
| 'cyanBold'
| 'reset';
/**
* Terminal capabilities
+2 -1
View File
@@ -20,11 +20,12 @@ function getColors(): Record<ColorName, string> {
green: '\x1b[0;32m',
blue: '\x1b[0;34m',
bold: '\x1b[1m',
cyanBold: '\x1b[1;36m',
reset: '\x1b[0m',
};
}
return { red: '', yellow: '', cyan: '', green: '', blue: '', bold: '', reset: '' };
return { red: '', yellow: '', cyan: '', green: '', blue: '', bold: '', cyanBold: '', reset: '' };
}
// Colors object (dynamic)