kimicc
Version:
Run Claude Code with Kimi K2 API - 支持使用多种大模型驱动运行 Claude Code
719 lines (600 loc) • 20.5 kB
JavaScript
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execSync } = require('child_process');
const readline = require('readline');
const { getProviderBySlug } = require('./providers');
const CONFIG_FILE = path.join(os.homedir(), '.kimicc.json');
function checkClaudeInPath() {
try {
execSync('which claude', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
function installClaudeCode() {
console.log('Claude Code not found. Installing @anthropic-ai/claude-code globally...');
try {
execSync('npm install -g @anthropic-ai/claude-code', { stdio: 'inherit' });
console.log('Claude Code installed successfully!');
return true;
} catch (error) {
console.error('Failed to install Claude Code:', error.message);
return false;
}
}
function readConfig() {
try {
if (fs.existsSync(CONFIG_FILE)) {
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
return JSON.parse(content);
}
} catch (error) {
console.error('Error reading config:', error.message);
}
return {};
}
function writeConfig(config) {
try {
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
} catch (error) {
console.error('Error writing config:', error.message);
}
}
async function promptForAuthToken() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question('Please enter your API Auth Token: ', (authToken) => {
rl.close();
resolve(authToken.trim());
});
});
}
function getProfileConfig(config, profileName) {
if (!config.profiles || !config.profiles[profileName]) {
return null;
}
return config.profiles[profileName];
}
function getDefaultProfile(config) {
return config.defaultProfile || null;
}
async function getAuthToken(profileName = null) {
// Check environment variables first (highest priority)
if (process.env.KIMI_AUTH_TOKEN) {
return process.env.KIMI_AUTH_TOKEN;
}
// Check config file
const config = readConfig();
// Ensure clean state - if we have profiles, we should not use legacy authToken
const hasProfiles = config.profiles && Object.keys(config.profiles).length > 0;
// If profile is specified, use profile config
if (profileName) {
const profile = getProfileConfig(config, profileName);
if (profile && profile.key) {
return profile.key;
}
console.error(`Profile '${profileName}' not found or missing auth token.`);
return null;
}
// Check if a default profile is set
if (hasProfiles) {
const defaultProfile = getDefaultProfile(config);
if (defaultProfile) {
const profile = getProfileConfig(config, defaultProfile);
if (profile && profile.key) {
return profile.key;
}
}
console.error('No default profile found and no legacy auth token.');
return null;
}
// Only use legacy authToken if no profiles exist
if (config.authToken) {
return config.authToken;
}
// Prompt for auth token
console.log('No auth token found in environment variables or config file.');
const authToken = await promptForAuthToken();
if (authToken) {
// Use legacy format only if no profiles exist
const currentConfig = readConfig();
if (!currentConfig.profiles || Object.keys(currentConfig.profiles).length === 0) {
writeConfig({ ...currentConfig, authToken });
console.log('Auth token saved to ~/.kimicc.json (legacy format)');
} else {
// Profiles exist, create default profile instead
migrateLegacyConfig();
const p = getProviderBySlug('kimi');
const model = p && p.defaultModel ? p.defaultModel : null;
const smallFastModel = p && p.defaultSmallFastModel ? p.defaultSmallFastModel : model;
const { addProfile } = require('./utils');
addProfile('default', 'https://api.moonshot.cn/anthropic', authToken, true, model, smallFastModel);
console.log('Auth token saved as default profile');
}
}
return authToken;
}
function getBaseUrl(profileName = null) {
// Check environment variables first
if (process.env.ANTHROPIC_BASE_URL) {
return process.env.ANTHROPIC_BASE_URL;
}
// Check config file
const config = readConfig();
// If profile is specified, use profile config
if (profileName) {
const profile = getProfileConfig(config, profileName);
if (profile && profile.url) {
return profile.url;
}
return 'https://api.moonshot.cn/anthropic'; // fallback
}
// Check if a default profile is set
const defaultProfile = getDefaultProfile(config);
if (defaultProfile) {
const profile = getProfileConfig(config, defaultProfile);
if (profile && profile.url) {
return profile.url;
}
}
// Default Kimi endpoint
return 'https://api.moonshot.cn/anthropic';
}
function getModel(profileName = null) {
// Check environment variables first
if (process.env.ANTHROPIC_MODEL) {
return process.env.ANTHROPIC_MODEL;
}
// Check config file
const config = readConfig();
// If profile is specified, use profile config
if (profileName) {
const profile = getProfileConfig(config, profileName);
if (profile && profile.model) {
return profile.model;
}
return null;
}
// Check if a default profile is set
const defaultProfile = getDefaultProfile(config);
if (defaultProfile) {
const profile = getProfileConfig(config, defaultProfile);
if (profile && profile.model) {
return profile.model;
}
}
return null;
}
function getSmallFastModel(profileName = null) {
// Priority: ENV > profile.smallFastModel > profile.model > null
if (process.env.ANTHROPIC_SMALL_FAST_MODEL) {
return process.env.ANTHROPIC_SMALL_FAST_MODEL;
}
const config = readConfig();
if (profileName) {
const profile = getProfileConfig(config, profileName);
if (profile) {
if (profile.smallFastModel) return profile.smallFastModel;
if (profile.model) return profile.model; // fallback to model if smallFastModel not set
}
return null;
}
const defaultProfile = getDefaultProfile(config);
if (defaultProfile) {
const profile = getProfileConfig(config, defaultProfile);
if (profile) {
if (profile.smallFastModel) return profile.smallFastModel;
if (profile.model) return profile.model; // fallback to model
}
}
return null;
}
function updateClaudeSettings() {
const claudeConfigFile = path.join(os.homedir(), '.claude.json');
try {
let claudeConfig = {};
// Read existing config if it exists
if (fs.existsSync(claudeConfigFile)) {
const content = fs.readFileSync(claudeConfigFile, 'utf8');
claudeConfig = JSON.parse(content);
}
// Update required settings
claudeConfig.autoUpdates = false;
claudeConfig.hasCompletedOnboarding = true;
// Write back the updated config
fs.writeFileSync(claudeConfigFile, JSON.stringify(claudeConfig, null, 2));
console.log('Claude settings updated successfully.');
} catch (error) {
console.error('Warning: Could not update Claude settings:', error.message);
// Don't fail the process, just warn
}
}
function detectShellType() {
// Check multiple sources for shell detection
const shell = process.env.SHELL || process.env.CMD || '';
if (shell.includes('zsh')) return 'zsh';
if (shell.includes('bash')) return 'bash';
if (shell.includes('fish')) return 'fish';
// Try to detect from process name
try {
const shellName = path.basename(shell);
if (shellName.includes('zsh')) return 'zsh';
if (shellName.includes('bash')) return 'bash';
if (shellName.includes('fish')) return 'fish';
} catch {
// fallback to bash as most common
}
return 'bash';
}
function getShellConfigFile(shellType) {
const homeDir = os.homedir();
switch (shellType) {
case 'zsh':
return path.join(homeDir, '.zshrc');
case 'bash':
return path.join(homeDir, '.bashrc');
case 'fish':
return path.join(homeDir, '.config', 'fish', 'config.fish');
default:
return path.join(homeDir, '.bashrc');
}
}
function validateShellConfig(configFile) {
try {
// Check if file exists and is writable
if (fs.existsSync(configFile)) {
fs.accessSync(configFile, fs.constants.W_OK);
} else {
// Check if directory exists and is writable, create if needed
const dir = path.dirname(configFile);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
}
fs.accessSync(dir, fs.constants.W_OK);
}
return { valid: true };
} catch (error) {
return { valid: false, error: error.message };
}
}
function backupConfigFile(configFile) {
if (fs.existsSync(configFile)) {
const backupFile = `${configFile}.backup.${Date.now()}`;
fs.copyFileSync(configFile, backupFile);
return backupFile;
}
return null;
}
function checkExistingEnvVars(configFile) {
const envVars = {
ANTHROPIC_BASE_URL: false
};
if (!fs.existsSync(configFile)) {
return envVars;
}
try {
const content = fs.readFileSync(configFile, 'utf8');
// Check for existing exports
Object.keys(envVars).forEach(key => {
const regex = new RegExp(`^\\s*export\\s+${key}=`, 'm');
envVars[key] = regex.test(content);
});
return envVars;
} catch (error) {
console.warn(`Warning: Could not read config file: ${error.message}`);
return envVars;
}
}
async function injectEnvVariables(authToken, shellType, force = false) {
const configFile = getShellConfigFile(shellType);
// Validate shell config
const validation = validateShellConfig(configFile);
if (!validation.valid) {
throw new Error(`Invalid shell configuration: ${validation.error}`);
}
// Check for existing variables
const existing = checkExistingEnvVars(configFile);
const hasExisting = Object.values(existing).some(v => v);
if (hasExisting && !force) {
console.log('⚠️ Environment variables already exist in shell config:');
Object.entries(existing).forEach(([key, exists]) => {
if (exists) console.log(` - ${key}`);
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question('Do you want to overwrite them? (y/N): ', (answer) => {
rl.close();
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
});
});
}
// Backup original config
const backupFile = backupConfigFile(configFile);
try {
const baseUrl = 'https://api.moonshot.cn/anthropic';
const timestamp = new Date().toISOString();
const markerStart = '# KimiCC Environment Variables - Added';
const markerEnd = '# End KimiCC Environment Variables';
// Prepare new content
const newContent = `
${markerStart} ${timestamp}
export ANTHROPIC_BASE_URL="${baseUrl}"
${markerEnd}
`;
// Read existing content
let existingContent = '';
if (fs.existsSync(configFile)) {
existingContent = fs.readFileSync(configFile, 'utf8');
}
// Remove existing KimiCC variables using safe string methods
let cleanedContent = existingContent;
const startMarkerIndex = cleanedContent.indexOf(markerStart);
const endMarkerIndex = cleanedContent.indexOf(markerEnd);
if (startMarkerIndex !== -1 && endMarkerIndex !== -1 && endMarkerIndex > startMarkerIndex) {
const endMarkerLength = markerEnd.length;
const endIndex = endMarkerIndex + endMarkerLength;
cleanedContent = cleanedContent.slice(0, startMarkerIndex) + cleanedContent.slice(endIndex);
}
// Append new variables
const finalContent = cleanedContent.trimEnd() + newContent;
// Write updated config with proper file locking to prevent race conditions
const tempFile = configFile + '.tmp';
fs.writeFileSync(tempFile, finalContent);
fs.renameSync(tempFile, configFile);
console.log(`✅ Environment variables injected into ${configFile}`);
if (backupFile) {
console.log(`📋 Backup created at ${backupFile}`);
}
console.log(`\n💡 To apply changes, run: source ${configFile}`);
return true;
} catch (error) {
// Restore backup on failure
if (backupFile && fs.existsSync(backupFile)) {
fs.copyFileSync(backupFile, configFile);
console.error('❌ Failed to inject variables, restored original config');
}
throw error;
}
}
async function removeEnvVariables(shellType, force = false) {
const configFile = getShellConfigFile(shellType);
// Validate shell config
const validation = validateShellConfig(configFile);
if (!validation.valid) {
throw new Error(`Invalid shell configuration: ${validation.error}`);
}
if (!fs.existsSync(configFile)) {
console.log(`ℹ️ Shell config file not found: ${configFile}`);
return false;
}
// Check if KimiCC variables exist
const content = fs.readFileSync(configFile, 'utf8');
const markerStart = '# KimiCC Environment Variables - Added';
const markerEnd = '# End KimiCC Environment Variables';
const startIndex = content.indexOf(markerStart);
const endIndex = content.indexOf(markerEnd);
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
console.log('ℹ️ No KimiCC environment variables found to remove.');
return false;
}
if (!force) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question('Are you sure you want to remove KimiCC environment variables? (y/N): ', (answer) => {
rl.close();
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
proceedWithRemoval();
resolve(true);
} else {
console.log('Removal cancelled.');
resolve(false);
}
});
});
} else {
proceedWithRemoval();
return true;
}
function proceedWithRemoval() {
// Backup original config
const backupFile = backupConfigFile(configFile);
try {
// Remove the marked section
const endMarkerLength = markerEnd.length;
const endIndexWithMarker = endIndex + endMarkerLength;
const cleanedContent = content.slice(0, startIndex) + content.slice(endIndexWithMarker);
// Write updated config with proper file locking
const tempFile = configFile + '.tmp';
fs.writeFileSync(tempFile, cleanedContent);
fs.renameSync(tempFile, configFile);
console.log(`✅ KimiCC environment variables removed from ${configFile}`);
if (backupFile) {
console.log(`📋 Backup created at ${backupFile}`);
}
console.log(`\n💡 To apply changes, run: source ${configFile}`);
} catch (error) {
// Restore backup on failure
if (backupFile && fs.existsSync(backupFile)) {
fs.copyFileSync(backupFile, configFile);
console.error('❌ Failed to remove variables, restored original config');
}
throw error;
}
}
}
function generateSlugFromUrl(url) {
try {
const urlObj = new URL(url);
let hostname = urlObj.hostname;
// Remove common prefixes including www, api, app, etc.
hostname = hostname.replace(/^(www\.|api\.|app\.|dev\.|test\.|staging\.|beta\.|alpha\.)/, '');
// Replace dots with empty string and convert to lowercase
return hostname.replace(/\./g, '').toLowerCase();
} catch (error) {
return null;
}
}
function listProfiles() {
const config = readConfig();
if (!config.profiles || Object.keys(config.profiles).length === 0) {
return [];
}
return Object.keys(config.profiles).map(slug => ({
slug,
...config.profiles[slug],
isDefault: slug === config.defaultProfile
}));
}
function migrateLegacyConfig() {
const config = readConfig();
// Check if we have legacy config (authToken exists but no profiles)
if (config.authToken && (!config.profiles || Object.keys(config.profiles).length === 0)) {
console.log('🔧 Migrating legacy configuration to profile format...');
// Create profiles object if it doesn't exist
if (!config.profiles) {
config.profiles = {};
}
// Create default profile with legacy values
const defaultSlug = 'default';
const p = getProviderBySlug('kimi');
config.profiles[defaultSlug] = {
url: 'https://api.moonshot.cn/anthropic',
key: config.authToken,
model: p && p.defaultModel ? p.defaultModel : undefined,
smallFastModel: p && p.defaultSmallFastModel ? p.defaultSmallFastModel : (p && p.defaultModel ? p.defaultModel : undefined)
};
// Set as default profile
config.defaultProfile = defaultSlug;
// Remove legacy authToken to ensure clean state
delete config.authToken;
writeConfig(config);
console.log(`✅ Migrated legacy configuration to profile '${defaultSlug}'`);
return true;
}
return false;
}
function ensureDefaultProfileKimiDefaults() {
const config = readConfig();
if (!config.profiles) return false;
const defaultProfileObj = config.profiles['default'];
if (!defaultProfileObj) return false;
const p = getProviderBySlug('kimi');
const kimiUrl = (p && p.baseUrl) || 'https://api.moonshot.cn/anthropic';
const kimiModel = p && p.defaultModel ? p.defaultModel : null;
const kimiSmallFast = p && p.defaultSmallFastModel ? p.defaultSmallFastModel : kimiModel;
let changed = false;
if (!defaultProfileObj.url || defaultProfileObj.url !== kimiUrl) {
defaultProfileObj.url = kimiUrl;
changed = true;
}
if (!defaultProfileObj.model && kimiModel) {
defaultProfileObj.model = kimiModel;
changed = true;
}
if (!defaultProfileObj.smallFastModel && kimiSmallFast) {
defaultProfileObj.smallFastModel = kimiSmallFast;
changed = true;
}
if (changed) {
// write back
writeConfig(config);
}
return changed;
}
function addProfile(slug, url, authToken, setAsDefault = false, model = null, smallFastModel = null) {
let config = readConfig();
// Check for legacy migration on first profile add
if (!config.profiles || Object.keys(config.profiles).length === 0) {
const migrated = migrateLegacyConfig();
if (migrated) {
// Migration occurred, now add the new profile alongside the migrated one
// Reload config after migration
config = readConfig();
}
}
if (!config.profiles) {
config.profiles = {};
}
config.profiles[slug] = {
url,
key: authToken
};
if (model) {
config.profiles[slug].model = model;
}
// Only set smallFastModel if explicitly provided; otherwise leave unset
if (smallFastModel) {
config.profiles[slug].smallFastModel = smallFastModel;
}
if (setAsDefault || !config.defaultProfile) {
config.defaultProfile = slug;
}
// Ensure clean state - remove legacy authToken if it exists
if (config.authToken) {
delete config.authToken;
}
writeConfig(config);
return true;
}
function deleteProfile(slug) {
const config = readConfig();
if (!config.profiles || !config.profiles[slug]) {
return false;
}
delete config.profiles[slug];
// If this was the default profile, clear it
if (config.defaultProfile === slug) {
config.defaultProfile = null;
// Set another profile as default if available
const remainingProfiles = Object.keys(config.profiles);
if (remainingProfiles.length > 0) {
config.defaultProfile = remainingProfiles[0];
}
}
writeConfig(config);
return true;
}
function setDefaultProfile(slug) {
const config = readConfig();
if (!config.profiles || !config.profiles[slug]) {
return false;
}
config.defaultProfile = slug;
writeConfig(config);
return true;
}
module.exports = {
checkClaudeInPath,
installClaudeCode,
getAuthToken,
getBaseUrl,
getModel,
getSmallFastModel,
updateClaudeSettings,
detectShellType,
getShellConfigFile,
validateShellConfig,
injectEnvVariables,
removeEnvVariables,
getProfileConfig,
getDefaultProfile,
generateSlugFromUrl,
listProfiles,
addProfile,
deleteProfile,
setDefaultProfile,
readConfig,
writeConfig,
ensureDefaultProfileKimiDefaults
};