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
JavaScript
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