UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

381 lines 13.8 kB
import { readFile, writeFile, mkdir, access } from 'fs/promises'; import { join, dirname } from 'path'; import { homedir } from 'os'; import YAML from 'yaml'; import { logger } from '../core/logger.js'; import { SecurityUtils } from '../core/security-utils.js'; export class ConfigManager { static instance; config = null; configPath; defaultConfigPath; constructor() { this.configPath = join(homedir(), '.codecrucible', 'config.yaml'); this.defaultConfigPath = join(process.cwd(), 'config', 'default.yaml'); } static async load() { if (!ConfigManager.instance) { ConfigManager.instance = new ConfigManager(); // Initialize encryption for sensitive data await SecurityUtils.initializeEncryption(); } return await ConfigManager.instance.loadConfiguration(); } static async getInstance() { if (!ConfigManager.instance) { ConfigManager.instance = new ConfigManager(); await ConfigManager.instance.loadConfiguration(); } return ConfigManager.instance; } async loadConfiguration() { if (this.config) { return this.config; } try { // Check if user config exists await access(this.configPath); const userConfigContent = await readFile(this.configPath, 'utf8'); this.config = YAML.parse(userConfigContent); // Decrypt sensitive fields this.decryptSensitiveFields(this.config); logger.info('Loaded user configuration', { path: this.configPath }); } catch (error) { // User config doesn't exist, try default config try { const defaultConfigContent = await readFile(this.defaultConfigPath, 'utf8'); this.config = YAML.parse(defaultConfigContent); logger.info('Loaded default configuration', { path: this.defaultConfigPath }); // Create user config from default await this.saveUserConfig(); } catch (defaultError) { // Neither config exists, use hardcoded defaults this.config = this.getHardcodedDefaults(); logger.warn('Using hardcoded configuration - no config files found'); await this.saveUserConfig(); } } return this.config; } async set(key, value) { // eslint-disable-line @typescript-eslint/no-explicit-any if (!this.config) { await this.loadConfiguration(); } const keys = key.split('.'); let current = this.config; // eslint-disable-line @typescript-eslint/no-explicit-any // Navigate to parent object for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (key && !current[key]) { current[key] = {}; // eslint-disable-line @typescript-eslint/no-explicit-any } if (key) { current = current[key]; // eslint-disable-line @typescript-eslint/no-explicit-any } } // Set the value const finalKey = keys[keys.length - 1]; if (finalKey) { current[finalKey] = value; } await this.saveUserConfig(); logger.info(`Configuration updated: ${key} = ${JSON.stringify(value)}`); } async get(key) { // eslint-disable-line @typescript-eslint/no-explicit-any if (!this.config) { await this.loadConfiguration(); } const keys = key.split('.'); let current = this.config; // eslint-disable-line @typescript-eslint/no-explicit-any for (const k of keys) { if (current[k] === undefined) { return undefined; } current = current[k]; // eslint-disable-line @typescript-eslint/no-explicit-any } return current; } async reset() { this.config = this.getHardcodedDefaults(); await this.saveUserConfig(); logger.info('Configuration reset to defaults'); } getAll() { if (!this.config) { throw new Error('Configuration not loaded'); } return { ...this.config }; } async saveUserConfig() { try { const configDir = dirname(this.configPath); await mkdir(configDir, { recursive: true }); // Create a copy of config with encrypted sensitive fields const configToSave = JSON.parse(JSON.stringify(this.config)); this.encryptSensitiveFields(configToSave); const yamlContent = YAML.stringify(configToSave); await writeFile(this.configPath, yamlContent, 'utf8'); logger.debug('User configuration saved', { path: this.configPath }); } catch (error) { logger.error('Failed to save user configuration:', error); throw error; } } getHardcodedDefaults() { return { model: { endpoint: 'http://localhost:11434', name: '', // Will be auto-detected from available models timeout: 180000, // 3 minutes for cold model starts maxTokens: 20000, temperature: 0.7, }, llmProviders: { default: 'ollama-local', providers: { 'ollama-local': { provider: 'ollama', endpoint: 'http://localhost:11434', model: 'auto', maxTokens: 4096, temperature: 0.7, timeout: 30000, enabled: true, }, 'openai-gpt4': { provider: 'openai', model: 'gpt-4o', maxTokens: 4096, temperature: 0.7, timeout: 30000, enabled: false, }, 'anthropic-claude': { provider: 'anthropic', model: 'claude-3-5-sonnet-20241022', maxTokens: 4096, temperature: 0.7, timeout: 30000, enabled: false, }, 'google-gemini': { provider: 'google', model: 'gemini-1.5-pro', maxTokens: 4096, temperature: 0.7, timeout: 30000, enabled: false, }, }, }, agent: { enabled: true, mode: 'balanced', maxConcurrency: 3, enableCaching: true, enableMetrics: true, enableSecurity: true, }, voices: { default: ['explorer', 'maintainer'], available: [ 'explorer', 'maintainer', 'analyzer', 'developer', 'implementor', 'security', 'architect', 'designer', 'optimizer', ], parallel: true, maxConcurrent: 3, }, database: { path: 'codecrucible.db', inMemory: false, enableWAL: true, backupEnabled: true, backupInterval: 86400000, // 24 hours in milliseconds }, safety: { commandValidation: true, fileSystemRestrictions: true, requireConsent: ['delete', 'execute'], }, terminal: { shell: 'auto', prompt: 'CC> ', historySize: 1000, colorOutput: true, }, vscode: { autoActivate: true, inlineGeneration: true, showVoicePanel: true, }, mcp: { servers: { filesystem: { enabled: true, restrictedPaths: ['/etc', '/sys', '/proc'], allowedPaths: ['~/', './'], }, git: { enabled: true, autoCommitMessages: false, safeModeEnabled: true, }, terminal: { enabled: true, allowedCommands: ['ls', 'cat', 'grep', 'find', 'git', 'npm', 'node', 'python'], blockedCommands: ['rm -rf', 'sudo', 'su', 'chmod +x'], }, packageManager: { enabled: true, autoInstall: false, securityScan: true, }, smithery: { enabled: false, apiKey: '', profile: '', baseUrl: 'https://server.smithery.ai', }, }, }, performance: { responseCache: { enabled: true, maxAge: 3600000, maxSize: 100, }, voiceParallelism: { maxConcurrent: 3, batchSize: 2, }, contextManagement: { maxContextLength: 100000, compressionThreshold: 80000, retentionStrategy: 'sliding', }, }, logging: { level: 'info', toFile: true, maxFileSize: '10MB', maxFiles: 5, }, }; } /** * Encrypt sensitive configuration fields */ encryptSensitiveFields(config) { const sensitiveFields = [ 'mcp.servers.smithery.apiKey', 'model.apiKey', 'database.password', 'security.encryptionKey', ]; for (const fieldPath of sensitiveFields) { const value = this.getNestedValue(config, fieldPath); if (value && typeof value === 'string' && value.length > 0 && !SecurityUtils.isEncrypted(value)) { try { const encrypted = SecurityUtils.encrypt(value); this.setNestedValue(config, fieldPath, encrypted); } catch (error) { logger.warn(`Failed to encrypt field ${fieldPath}:`, error); } } } } /** * Decrypt sensitive configuration fields */ decryptSensitiveFields(config) { const sensitiveFields = [ 'mcp.servers.smithery.apiKey', 'model.apiKey', 'database.password', 'security.encryptionKey', ]; for (const fieldPath of sensitiveFields) { const value = this.getNestedValue(config, fieldPath); if (value && typeof value === 'string' && SecurityUtils.isEncrypted(value)) { try { const decrypted = SecurityUtils.decrypt(value); this.setNestedValue(config, fieldPath, decrypted); } catch (error) { logger.warn(`Failed to decrypt field ${fieldPath}:`, error); // Leave the field encrypted if decryption fails } } } } /** * Get nested value from object using dot notation */ getNestedValue(obj, path) { return path.split('.').reduce((current, key) => current?.[key], obj); } /** * Set nested value in object using dot notation */ setNestedValue(obj, path, value) { const keys = path.split('.'); const lastKey = keys.pop(); const target = keys.reduce((current, key) => { if (!current[key]) current[key] = {}; return current[key]; }, obj); target[lastKey] = value; } /** * Get agent configuration (consolidated from core/config.ts) */ async getAgentConfig() { const config = await this.loadConfiguration(); return config.agent; } /** * Update agent configuration (consolidated from core/config.ts) */ async updateAgentConfig(newConfig) { const config = await this.loadConfiguration(); config.agent = newConfig; await this.saveUserConfig(); } } // Export singleton instance (consolidated from core/config.ts) // Note: Since getInstance is async, this needs to be awaited where used let configManagerInstance = null; export const configManager = { async getInstance() { if (!configManagerInstance) { configManagerInstance = await ConfigManager.getInstance(); } return configManagerInstance; }, async getAgentConfig() { const instance = await this.getInstance(); return instance.getAgentConfig(); }, async updateAgentConfig(config) { const instance = await this.getInstance(); return instance.updateAgentConfig(config); }, }; //# sourceMappingURL=config-manager.js.map