#!/usr/bin/env node import * as fs from 'fs'; import * as path from 'path'; import { Settings } from '../types'; import { ValidationResult } from '../types/utils'; import { getCcsDir } from './config-manager'; import { getProfileLookupCandidates } from './profile-compat'; /** * Extended validation result for delegation profiles * Extends base ValidationResult with domain-specific fields */ interface DelegationValidationResult extends ValidationResult { error?: string; suggestion?: string; settingsPath?: string; apiKey?: string; } /** * Validates delegation profiles for CCS delegation system * Ensures profiles exist and have valid API keys configured */ export class DelegationValidator { /** * Validate a delegation profile * @param profileName - Name of profile to validate (e.g., 'glm', 'km') * @returns Validation result { valid: boolean, error?: string, settingsPath?: string } */ static validate(profileName: string): DelegationValidationResult { const ccsDir = getCcsDir(); const candidateSettingsPath = getProfileLookupCandidates(profileName) .map((candidate) => path.join(ccsDir, `${candidate}.settings.json`)) .find((candidatePath) => fs.existsSync(candidatePath)); const primarySettingsPath = path.join(ccsDir, `${profileName}.settings.json`); const settingsPath = candidateSettingsPath || primarySettingsPath; // Check if profile directory exists if (!candidateSettingsPath) { return { valid: false, error: `Profile not found: ${profileName}`, suggestion: `Profile settings missing at: ${primarySettingsPath}\n\n` + `To set up ${profileName} profile:\n` + ` 1. Copy base settings: cp config/base-${profileName}.settings.json ~/.ccs/${profileName}.settings.json\n` + ` 2. Edit settings: Edit ~/.ccs/${profileName}.settings.json\n` + ` 3. Set your API key in ANTHROPIC_AUTH_TOKEN field`, }; } // Read and parse settings.json let settings: Settings; try { const settingsContent = fs.readFileSync(settingsPath, 'utf8'); settings = JSON.parse(settingsContent); } catch (error) { return { valid: false, error: `Failed to parse settings.json for ${profileName}`, suggestion: `Settings file is corrupted or invalid JSON.\n\n` + `Location: ${settingsPath}\n` + `Parse error: ${(error as Error).message}\n\n` + `Fix: Restore from base config:\n` + ` cp config/base-${profileName}.settings.json ~/.ccs/${profileName}.settings.json`, }; } // Validate API key exists and is not default // Support both proxy mode (ANTHROPIC_AUTH_TOKEN) and native mode (ANTHROPIC_API_KEY) const apiKey = settings.env?.ANTHROPIC_AUTH_TOKEN || settings.env?.ANTHROPIC_API_KEY; if (!apiKey) { return { valid: false, error: `API key not configured for ${profileName}`, suggestion: `Missing ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY in settings.\n\n` + `Edit: ${settingsPath}\n` + `Set: env.ANTHROPIC_AUTH_TOKEN (proxy) or env.ANTHROPIC_API_KEY (direct) to your API key`, }; } // Check for default placeholder values const defaultPlaceholders = [ 'YOUR_GLM_API_KEY_HERE', 'YOUR_KIMI_API_KEY_HERE', 'YOUR_API_KEY_HERE', 'your-api-key-here', 'PLACEHOLDER', ]; if (defaultPlaceholders.some((placeholder) => apiKey.includes(placeholder))) { return { valid: false, error: `Default API key placeholder detected for ${profileName}`, suggestion: `API key is still set to default placeholder.\n\n` + `To configure your profile:\n` + ` 1. Edit: ${settingsPath}\n` + ` 2. Replace ANTHROPIC_AUTH_TOKEN with your actual API key\n\n` + `Get API key:\n` + ` GLM: https://z.ai/manage-apikey/apikey-list\n` + ` Kimi: https://platform.moonshot.cn/console/api-keys`, }; } // Validation passed return { valid: true, settingsPath, apiKey: apiKey.substring(0, 8) + '...', // Show first 8 chars for verification }; } /** * Format validation error for display * @param result - Validation result from validate() * @returns Formatted error message */ static formatError(result: DelegationValidationResult): string { if (result.valid) { return ''; } let message = `\n[X] ${result.error}\n\n`; if (result.suggestion) { message += `${result.suggestion}\n`; } return message; } /** * Check if profile is delegation-ready (shorthand) * @param profileName - Profile to check * @returns True if ready for delegation */ static isReady(profileName: string): boolean { const result = this.validate(profileName); return result.valid; } /** * Get all delegation-ready profiles from config.yaml * Only returns profiles explicitly defined in config, not orphan settings files * @returns List of profile names ready for delegation */ static getReadyProfiles(): string[] { const ccsDir = getCcsDir(); const configPath = path.join(ccsDir, 'config.yaml'); if (!fs.existsSync(ccsDir)) { return []; } const profiles: string[] = []; // Get profiles from config.yaml (excludes 'default' which uses ~/.claude/settings.json) if (fs.existsSync(configPath)) { try { const yaml = require('js-yaml'); const content = fs.readFileSync(configPath, 'utf8'); const config = yaml.load(content) as Record; if (config.profiles && typeof config.profiles === 'object') { for (const profileName of Object.keys(config.profiles as object)) { if (profileName !== 'default' && this.isReady(profileName)) { profiles.push(profileName); } } } // Also check CLIProxy providers (gemini, codex, agy, etc.) if (config.cliproxy && typeof config.cliproxy === 'object') { const cliproxy = config.cliproxy as Record; if (Array.isArray(cliproxy.providers)) { for (const provider of cliproxy.providers) { if (typeof provider === 'string' && this.isReady(provider)) { profiles.push(provider); } } } } } catch { // Config parse error, fall back to empty } } return profiles; } }