mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 22:21:20 +00:00
- Remove 5 deprecated model fields from Kimi settings causing auth failures - Update config/base-kimi.settings.json template with clean configuration - Add automatic migration in scripts/postinstall.js to remove deprecated fields - Preserve user API keys and custom settings during migration - Update CHANGELOG.md with v4.1.1 and v4.1.2 entries - Bump version to 4.1.2 across all files and installers Fixes authentication issues with Moonshot AI (Kimi) API due to deprecated model parameters that are now rejected by the service.
462 lines
16 KiB
JavaScript
Executable File
462 lines
16 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
|
|
/**
|
|
* CCS Postinstall Script
|
|
* Automatically creates config files in ~/.ccs/ after npm install
|
|
*
|
|
* Runs when: npm install -g @kaitranntt/ccs
|
|
* Idempotent: Safe to run multiple times (won't overwrite existing configs)
|
|
* Cross-platform: Works on Unix, macOS, Windows
|
|
*/
|
|
|
|
/**
|
|
* Validate created configuration files
|
|
* @returns {object} { success: boolean, errors: string[], warnings: string[] }
|
|
*/
|
|
function validateConfiguration() {
|
|
const homedir = os.homedir();
|
|
const errors = [];
|
|
const warnings = [];
|
|
|
|
// Check ~/.ccs/ directory
|
|
const ccsDir = path.join(homedir, '.ccs');
|
|
if (!fs.existsSync(ccsDir)) {
|
|
errors.push('~/.ccs/ directory not found');
|
|
}
|
|
|
|
// Check required files
|
|
const requiredFiles = [
|
|
{ path: path.join(ccsDir, 'config.json'), name: 'config.json' },
|
|
{ path: path.join(ccsDir, 'glm.settings.json'), name: 'glm.settings.json' },
|
|
{ path: path.join(ccsDir, 'glmt.settings.json'), name: 'glmt.settings.json' },
|
|
{ path: path.join(ccsDir, 'kimi.settings.json'), name: 'kimi.settings.json' }
|
|
];
|
|
|
|
for (const file of requiredFiles) {
|
|
if (!fs.existsSync(file.path)) {
|
|
errors.push(`${file.name} not found`);
|
|
continue;
|
|
}
|
|
|
|
// Validate JSON syntax
|
|
try {
|
|
const content = fs.readFileSync(file.path, 'utf8');
|
|
JSON.parse(content);
|
|
} catch (e) {
|
|
errors.push(`${file.name} has invalid JSON: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// Check ~/.claude/settings.json (warning only, not critical)
|
|
const claudeSettings = path.join(homedir, '.claude', 'settings.json');
|
|
if (!fs.existsSync(claudeSettings)) {
|
|
warnings.push('~/.claude/settings.json not found - run "claude /login"');
|
|
}
|
|
|
|
return { success: errors.length === 0, errors, warnings };
|
|
}
|
|
|
|
function createConfigFiles() {
|
|
try {
|
|
// Get user home directory (cross-platform)
|
|
const homedir = os.homedir();
|
|
const ccsDir = path.join(homedir, '.ccs');
|
|
|
|
// Create ~/.ccs/ directory if missing
|
|
if (!fs.existsSync(ccsDir)) {
|
|
fs.mkdirSync(ccsDir, { recursive: true, mode: 0o755 });
|
|
console.log('[OK] Created directory: ~/.ccs/');
|
|
}
|
|
|
|
// Create ~/.ccs/shared/ directory structure (Phase 1)
|
|
const sharedDir = path.join(ccsDir, 'shared');
|
|
if (!fs.existsSync(sharedDir)) {
|
|
fs.mkdirSync(sharedDir, { recursive: true, mode: 0o755 });
|
|
console.log('[OK] Created directory: ~/.ccs/shared/');
|
|
}
|
|
|
|
// Create shared subdirectories
|
|
const sharedSubdirs = ['commands', 'skills', 'agents'];
|
|
for (const subdir of sharedSubdirs) {
|
|
const subdirPath = path.join(sharedDir, subdir);
|
|
if (!fs.existsSync(subdirPath)) {
|
|
fs.mkdirSync(subdirPath, { recursive: true, mode: 0o755 });
|
|
console.log(`[OK] Created directory: ~/.ccs/shared/${subdir}/`);
|
|
}
|
|
}
|
|
|
|
// Migrate from v3.1.1 to v3.2.0 (symlink architecture)
|
|
console.log('');
|
|
try {
|
|
const SharedManager = require('../bin/management/shared-manager');
|
|
const sharedManager = new SharedManager();
|
|
sharedManager.migrateFromV311();
|
|
sharedManager.ensureSharedDirectories();
|
|
} catch (err) {
|
|
console.warn('[!] Migration warning:', err.message);
|
|
console.warn(' Migration will retry on next run');
|
|
}
|
|
console.log('');
|
|
|
|
// Copy .claude/ directory from package to ~/.ccs/.claude/ (v4.1.1)
|
|
try {
|
|
const ClaudeDirInstaller = require('../bin/utils/claude-dir-installer');
|
|
const installer = new ClaudeDirInstaller();
|
|
const packageDir = path.join(__dirname, '..');
|
|
installer.install(packageDir);
|
|
} catch (err) {
|
|
console.warn('[!] Failed to install .claude/ directory:', err.message);
|
|
console.warn(' CCS items may not be available');
|
|
}
|
|
|
|
// Install CCS items to ~/.claude/ (v4.1.0)
|
|
try {
|
|
const ClaudeSymlinkManager = require('../bin/utils/claude-symlink-manager');
|
|
const claudeSymlinkManager = new ClaudeSymlinkManager();
|
|
claudeSymlinkManager.install();
|
|
} catch (err) {
|
|
console.warn('[!] CCS item installation warning:', err.message);
|
|
console.warn(' Run "ccs update" to retry');
|
|
}
|
|
console.log('');
|
|
|
|
// Create config.json if missing
|
|
const configPath = path.join(ccsDir, 'config.json');
|
|
if (!fs.existsSync(configPath)) {
|
|
const config = {
|
|
profiles: {
|
|
glm: '~/.ccs/glm.settings.json',
|
|
glmt: '~/.ccs/glmt.settings.json',
|
|
kimi: '~/.ccs/kimi.settings.json',
|
|
default: '~/.claude/settings.json'
|
|
}
|
|
};
|
|
|
|
// Atomic write: temp file → rename
|
|
const tmpPath = `${configPath}.tmp`;
|
|
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
fs.renameSync(tmpPath, configPath);
|
|
|
|
console.log('[OK] Created config: ~/.ccs/config.json');
|
|
} else {
|
|
// Update existing config with glmt if missing (migration for v3.x users)
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
// Ensure profiles object exists
|
|
if (!config.profiles) {
|
|
config.profiles = {};
|
|
}
|
|
if (!config.profiles.glmt) {
|
|
config.profiles.glmt = '~/.ccs/glmt.settings.json';
|
|
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');
|
|
} else {
|
|
console.log('[OK] Config exists: ~/.ccs/config.json (preserved)');
|
|
}
|
|
}
|
|
|
|
// Create glm.settings.json if missing
|
|
const glmSettingsPath = path.join(ccsDir, 'glm.settings.json');
|
|
if (!fs.existsSync(glmSettingsPath)) {
|
|
const glmSettings = {
|
|
env: {
|
|
ANTHROPIC_BASE_URL: 'https://api.z.ai/api/anthropic',
|
|
ANTHROPIC_AUTH_TOKEN: 'YOUR_GLM_API_KEY_HERE',
|
|
ANTHROPIC_MODEL: 'glm-4.6',
|
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'glm-4.6',
|
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'glm-4.6',
|
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.6'
|
|
}
|
|
};
|
|
|
|
// Atomic write
|
|
const tmpPath = `${glmSettingsPath}.tmp`;
|
|
fs.writeFileSync(tmpPath, JSON.stringify(glmSettings, null, 2) + '\n', 'utf8');
|
|
fs.renameSync(tmpPath, glmSettingsPath);
|
|
|
|
console.log('[OK] Created GLM profile: ~/.ccs/glm.settings.json');
|
|
console.log('');
|
|
console.log(' [!] Configure GLM API key:');
|
|
console.log(' 1. Get key from: https://api.z.ai');
|
|
console.log(' 2. Edit: ~/.ccs/glm.settings.json');
|
|
console.log(' 3. Replace: YOUR_GLM_API_KEY_HERE');
|
|
} else {
|
|
console.log('[OK] GLM profile exists: ~/.ccs/glm.settings.json (preserved)');
|
|
}
|
|
|
|
// Create glmt.settings.json if missing
|
|
const glmtSettingsPath = path.join(ccsDir, 'glmt.settings.json');
|
|
if (!fs.existsSync(glmtSettingsPath)) {
|
|
const glmtSettings = {
|
|
env: {
|
|
ANTHROPIC_BASE_URL: 'https://api.z.ai/api/coding/paas/v4/chat/completions',
|
|
ANTHROPIC_AUTH_TOKEN: 'YOUR_GLM_API_KEY_HERE',
|
|
ANTHROPIC_MODEL: 'glm-4.6',
|
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'glm-4.6',
|
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'glm-4.6',
|
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.6',
|
|
ANTHROPIC_TEMPERATURE: '0.2',
|
|
ANTHROPIC_MAX_TOKENS: '65536',
|
|
MAX_THINKING_TOKENS: '32768',
|
|
ENABLE_STREAMING: 'true',
|
|
ANTHROPIC_SAFE_MODE: 'false',
|
|
API_TIMEOUT_MS: '3000000'
|
|
},
|
|
alwaysThinkingEnabled: true
|
|
};
|
|
|
|
// Atomic write
|
|
const tmpPath = `${glmtSettingsPath}.tmp`;
|
|
fs.writeFileSync(tmpPath, JSON.stringify(glmtSettings, null, 2) + '\n', 'utf8');
|
|
fs.renameSync(tmpPath, glmtSettingsPath);
|
|
|
|
console.log('[OK] Created GLMT profile: ~/.ccs/glmt.settings.json');
|
|
console.log('');
|
|
console.log(' [!] Configure GLMT API key:');
|
|
console.log(' 1. Get key from: https://api.z.ai');
|
|
console.log(' 2. Edit: ~/.ccs/glmt.settings.json');
|
|
console.log(' 3. Replace: YOUR_GLM_API_KEY_HERE');
|
|
console.log(' Note: GLMT enables GLM thinking mode (reasoning)');
|
|
console.log(' Defaults: Temperature 0.2, thinking enabled, 50min timeout');
|
|
} else {
|
|
console.log('[OK] GLMT profile exists: ~/.ccs/glmt.settings.json (preserved)');
|
|
}
|
|
|
|
// Migrate existing GLMT configs to include new defaults (v3.3.0)
|
|
if (fs.existsSync(glmtSettingsPath)) {
|
|
try {
|
|
const existing = JSON.parse(fs.readFileSync(glmtSettingsPath, 'utf8'));
|
|
let updated = false;
|
|
|
|
// Ensure env object exists
|
|
if (!existing.env) {
|
|
existing.env = {};
|
|
updated = true;
|
|
}
|
|
|
|
// Add missing env vars (preserve existing values)
|
|
const envDefaults = {
|
|
ANTHROPIC_TEMPERATURE: '0.2',
|
|
ANTHROPIC_MAX_TOKENS: '65536',
|
|
MAX_THINKING_TOKENS: '32768',
|
|
ENABLE_STREAMING: 'true',
|
|
ANTHROPIC_SAFE_MODE: 'false',
|
|
API_TIMEOUT_MS: '3000000'
|
|
};
|
|
|
|
for (const [key, value] of Object.entries(envDefaults)) {
|
|
if (existing.env[key] === undefined) {
|
|
existing.env[key] = value;
|
|
updated = true;
|
|
}
|
|
}
|
|
|
|
// Add alwaysThinkingEnabled if missing
|
|
if (existing.alwaysThinkingEnabled === undefined) {
|
|
existing.alwaysThinkingEnabled = true;
|
|
updated = true;
|
|
}
|
|
|
|
// Write back if updated
|
|
if (updated) {
|
|
const tmpPath = `${glmtSettingsPath}.tmp`;
|
|
fs.writeFileSync(tmpPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
fs.renameSync(tmpPath, glmtSettingsPath);
|
|
console.log('[OK] Migrated GLMT config with new defaults (v3.3.0)');
|
|
console.log(' Added: temperature, max_tokens, thinking settings, alwaysThinkingEnabled');
|
|
}
|
|
} catch (err) {
|
|
console.warn('[!] GLMT config migration failed:', err.message);
|
|
console.warn(' Existing config preserved, may be missing new defaults');
|
|
console.warn(' You can manually add fields or delete file to regenerate');
|
|
}
|
|
}
|
|
|
|
// Create kimi.settings.json if missing
|
|
const kimiSettingsPath = path.join(ccsDir, 'kimi.settings.json');
|
|
if (!fs.existsSync(kimiSettingsPath)) {
|
|
const kimiSettings = {
|
|
env: {
|
|
ANTHROPIC_BASE_URL: 'https://api.kimi.com/coding/',
|
|
ANTHROPIC_AUTH_TOKEN: 'YOUR_KIMI_API_KEY_HERE'
|
|
},
|
|
alwaysThinkingEnabled: true
|
|
};
|
|
|
|
// Atomic write
|
|
const tmpPath = `${kimiSettingsPath}.tmp`;
|
|
fs.writeFileSync(tmpPath, JSON.stringify(kimiSettings, null, 2) + '\n', 'utf8');
|
|
fs.renameSync(tmpPath, kimiSettingsPath);
|
|
|
|
console.log('[OK] Created Kimi profile: ~/.ccs/kimi.settings.json');
|
|
console.log('');
|
|
console.log(' [!] Configure Kimi API key:');
|
|
console.log(' 1. Get key from: https://www.kimi.com/coding (membership page)');
|
|
console.log(' 2. Edit: ~/.ccs/kimi.settings.json');
|
|
console.log(' 3. Replace: YOUR_KIMI_API_KEY_HERE');
|
|
} else {
|
|
console.log('[OK] Kimi profile exists: ~/.ccs/kimi.settings.json (preserved)');
|
|
}
|
|
|
|
// 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)) {
|
|
try {
|
|
const existing = JSON.parse(fs.readFileSync(kimiSettingsPath, 'utf8'));
|
|
let updated = false;
|
|
|
|
// Ensure env object exists
|
|
if (!existing.env) {
|
|
existing.env = {};
|
|
updated = true;
|
|
}
|
|
|
|
// Remove deprecated model fields that cause 401 errors
|
|
const deprecatedFields = [
|
|
'ANTHROPIC_MODEL',
|
|
'ANTHROPIC_SMALL_FAST_MODEL',
|
|
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
'ANTHROPIC_DEFAULT_HAIKU_MODEL'
|
|
];
|
|
|
|
for (const field of deprecatedFields) {
|
|
if (existing.env[field] !== undefined) {
|
|
delete existing.env[field];
|
|
updated = true;
|
|
}
|
|
}
|
|
|
|
// Ensure required fields exist
|
|
if (!existing.env.ANTHROPIC_BASE_URL) {
|
|
existing.env.ANTHROPIC_BASE_URL = 'https://api.kimi.com/coding/';
|
|
updated = true;
|
|
}
|
|
|
|
// Add alwaysThinkingEnabled if missing
|
|
if (existing.alwaysThinkingEnabled === undefined) {
|
|
existing.alwaysThinkingEnabled = true;
|
|
updated = true;
|
|
}
|
|
|
|
// Write back if updated
|
|
if (updated) {
|
|
const tmpPath = `${kimiSettingsPath}.tmp`;
|
|
fs.writeFileSync(tmpPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
fs.renameSync(tmpPath, kimiSettingsPath);
|
|
console.log('[OK] Migrated Kimi config (v4.1.2): removed deprecated model fields');
|
|
console.log(' Kimi API no longer requires model fields (they cause 401 errors)');
|
|
}
|
|
} catch (err) {
|
|
console.warn('[!] Kimi config migration failed:', err.message);
|
|
console.warn(' Existing config preserved, but may cause 401 errors');
|
|
console.warn(' Manually remove ANTHROPIC_MODEL fields from ~/.ccs/kimi.settings.json');
|
|
}
|
|
}
|
|
|
|
// Copy shell completion files to ~/.ccs/completions/
|
|
const completionsDir = path.join(ccsDir, 'completions');
|
|
const scriptsCompletionDir = path.join(__dirname, '../scripts/completion');
|
|
|
|
if (!fs.existsSync(completionsDir)) {
|
|
fs.mkdirSync(completionsDir, { recursive: true, mode: 0o755 });
|
|
}
|
|
|
|
const completionFiles = ['ccs.bash', 'ccs.zsh', 'ccs.fish', 'ccs.ps1'];
|
|
completionFiles.forEach(file => {
|
|
const src = path.join(scriptsCompletionDir, file);
|
|
const dest = path.join(completionsDir, file);
|
|
|
|
if (fs.existsSync(src)) {
|
|
fs.copyFileSync(src, dest);
|
|
}
|
|
});
|
|
|
|
console.log('[OK] Installed shell completions: ~/.ccs/completions/');
|
|
console.log('');
|
|
console.log(' [i] Enable auto-completion:');
|
|
console.log(' Run: ccs --shell-completion');
|
|
console.log('');
|
|
|
|
// Create ~/.claude/settings.json if missing (NEW)
|
|
const claudeDir = path.join(homedir, '.claude');
|
|
const claudeSettingsPath = path.join(claudeDir, 'settings.json');
|
|
|
|
if (!fs.existsSync(claudeDir)) {
|
|
fs.mkdirSync(claudeDir, { recursive: true, mode: 0o755 });
|
|
console.log('[OK] Created directory: ~/.claude/');
|
|
}
|
|
|
|
if (!fs.existsSync(claudeSettingsPath)) {
|
|
// Create empty settings (matches Claude CLI behavior)
|
|
const tmpPath = `${claudeSettingsPath}.tmp`;
|
|
fs.writeFileSync(tmpPath, '{}\n', 'utf8');
|
|
fs.renameSync(tmpPath, claudeSettingsPath);
|
|
|
|
console.log('[OK] Created default settings: ~/.claude/settings.json');
|
|
console.log('');
|
|
console.log(' [i] Configure Claude CLI:');
|
|
console.log(' Run: claude /login');
|
|
console.log('');
|
|
} else {
|
|
console.log('[OK] Claude settings exist: ~/.claude/settings.json (preserved)');
|
|
}
|
|
|
|
// Validate configuration
|
|
console.log('');
|
|
console.log('[i] Validating configuration...');
|
|
const validation = validateConfiguration();
|
|
|
|
if (!validation.success) {
|
|
console.error('');
|
|
console.error('[X] Configuration validation failed:');
|
|
validation.errors.forEach(err => console.error(` - ${err}`));
|
|
console.error('');
|
|
throw new Error('Configuration incomplete');
|
|
}
|
|
|
|
// Show warnings (non-critical)
|
|
if (validation.warnings.length > 0) {
|
|
console.warn('');
|
|
console.warn('[!] Warnings:');
|
|
validation.warnings.forEach(warn => console.warn(` - ${warn}`));
|
|
}
|
|
|
|
console.log('');
|
|
console.log('[OK] CCS configuration ready!');
|
|
console.log(' Run: ccs --version');
|
|
|
|
} catch (err) {
|
|
// Show error details
|
|
console.error('');
|
|
console.error('[X] CCS configuration failed');
|
|
console.error(` Error: ${err.message}`);
|
|
console.error('');
|
|
console.error('Recovery steps:');
|
|
console.error(' 1. Create directory manually:');
|
|
console.error(' mkdir -p ~/.ccs ~/.claude');
|
|
console.error('');
|
|
console.error(' 2. Create empty settings:');
|
|
console.error(' echo "{}" > ~/.claude/settings.json');
|
|
console.error('');
|
|
console.error(' 3. Retry installation:');
|
|
console.error(' npm install -g @kaitranntt/ccs --force');
|
|
console.error('');
|
|
console.error(' 4. If issue persists, report at:');
|
|
console.error(' https://github.com/kaitranntt/ccs/issues');
|
|
console.error('');
|
|
|
|
// Exit with error code (npm will show warning)
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run postinstall
|
|
createConfigFiles();
|