mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
Support native Anthropic API keys (sk-ant-...) via `ccs api create --preset anthropic`. Profiles use ANTHROPIC_API_KEY env var without BASE_URL, letting Claude CLI authenticate natively with x-api-key header instead of proxy Bearer token. - Add anthropic preset to provider catalog - Branch profile-writer env structure (native vs proxy) - Add validateAnthropicKey() preflight with x-api-key header - Auto-detect api.anthropic.com URL in interactive flow - Fix hasApiKey/isConfigured to recognize ANTHROPIC_API_KEY - Update delegation-validator for native mode profiles - Add Droid target API key fallback (ANTHROPIC_API_KEY) Closes #688 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
196 lines
6.4 KiB
JavaScript
196 lines
6.4 KiB
JavaScript
#!/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<string, unknown>;
|
|
|
|
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<string, unknown>;
|
|
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;
|
|
}
|
|
}
|