/** * Profile Mapper for CLIProxy Sync * * Transforms CCS settings-based profiles into CLIProxy ClaudeKey format. */ import * as fs from 'fs'; import * as path from 'path'; import { getCcsDir } from '../../utils/config-manager'; import { listApiProfiles, isApiProfileConfigured } from '../../api/services/profile-reader'; import type { ClaudeKey } from '../management-api-types'; /** * Profile info with settings for sync. */ export interface SyncableProfile { /** Profile name (e.g., "glm", "kimi") */ name: string; /** Path to settings.json file */ settingsPath: string; /** Whether profile has valid API key */ isConfigured: boolean; /** Environment variables from settings.json */ env?: Record; } /** * Settings.json file structure (Claude compatible). */ interface SettingsJson { env?: Record; } /** * Load syncable API profiles from CCS config. * Filters to only configured profiles (with real API keys). */ export function loadSyncableProfiles(): SyncableProfile[] { const { profiles } = listApiProfiles(); const syncable: SyncableProfile[] = []; for (const profile of profiles) { // Skip unconfigured profiles if (!profile.isConfigured) { continue; } // Load settings.json for env vars const ccsDir = getCcsDir(); const settingsPath = path.join(ccsDir, `${profile.name}.settings.json`); let env: Record | undefined; try { if (fs.existsSync(settingsPath)) { const content = fs.readFileSync(settingsPath, 'utf8'); const settings = JSON.parse(content) as SettingsJson; env = settings.env; } } catch { // Skip profiles with unreadable settings continue; } // Must have ANTHROPIC_AUTH_TOKEN const token = env?.ANTHROPIC_AUTH_TOKEN; if (!token || token.includes('YOUR_') || token.includes('your-')) { continue; } syncable.push({ name: profile.name, settingsPath, isConfigured: true, env, }); } return syncable; } /** * Sanitize profile name for YAML safety. * Replaces non-alphanumeric chars (except - and _) with hyphens. */ function sanitizeProfileName(name: string): string { return name.replace(/[^a-zA-Z0-9-_]/g, '-'); } /** * Map a single profile to ClaudeKey format. */ export function mapProfileToClaudeKey(profile: SyncableProfile): ClaudeKey | null { const env = profile.env; if (!env) return null; const apiKey = env.ANTHROPIC_AUTH_TOKEN; if (!apiKey) return null; const baseUrl = env.ANTHROPIC_BASE_URL; const modelName = env.ANTHROPIC_MODEL; // Generate prefix from profile name (e.g., "glm" -> "glm-") const sanitizedName = sanitizeProfileName(profile.name); if (!sanitizedName || sanitizedName === '') { return null; // Skip profiles with invalid names } const prefix = `${sanitizedName}-`; const claudeKey: ClaudeKey = { 'api-key': apiKey, prefix, }; if (baseUrl) { claudeKey['base-url'] = baseUrl; } // Use model name directly from profile (no alias mapping) if (modelName) { claudeKey.models = [ { name: modelName, alias: '', }, ]; } return claudeKey; } /** * Generate sync payload from all configured profiles. * Returns array of ClaudeKey ready to push to remote CLIProxy. */ export function generateSyncPayload(): ClaudeKey[] { const profiles = loadSyncableProfiles(); const keys: ClaudeKey[] = []; for (const profile of profiles) { const key = mapProfileToClaudeKey(profile); if (key) { keys.push(key); } } return keys; } /** * Generate sync preview with profile details. * Used for dry-run mode to show what would be synced. */ export interface SyncPreviewItem { /** Profile name */ name: string; /** Base URL (masked) */ baseUrl?: string; /** Model name */ modelName?: string; } export function generateSyncPreview(): SyncPreviewItem[] { const profiles = loadSyncableProfiles(); const preview: SyncPreviewItem[] = []; for (const profile of profiles) { preview.push({ name: profile.name, baseUrl: profile.env?.ANTHROPIC_BASE_URL, modelName: profile.env?.ANTHROPIC_MODEL, }); } return preview; } /** * Get count of syncable profiles. */ export function getSyncableProfileCount(): number { return loadSyncableProfiles().length; } /** * Check if profile is syncable (configured with valid API key). */ export function isProfileSyncable(profileName: string): boolean { return isApiProfileConfigured(profileName); }