codecrucible-synth
Version:
Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability
502 lines (462 loc) • 14.2 kB
text/typescript
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 interface LLMProviderConfig {
provider: 'openai' | 'google' | 'anthropic' | 'ollama';
apiKey?: string;
endpoint?: string;
model: string;
maxTokens?: number;
temperature?: number;
timeout?: number;
enabled: boolean;
}
// Consolidated from core/config.ts - Agent configuration
export interface AgentConfig {
enabled: boolean;
mode: 'fast' | 'balanced' | 'thorough' | 'auto';
maxConcurrency: number;
enableCaching: boolean;
enableMetrics: boolean;
enableSecurity: boolean;
}
export interface AppConfig {
model: {
endpoint: string;
name: string;
timeout: number;
maxTokens: number;
temperature: number;
};
llmProviders: {
default: string;
providers: Record<string, LLMProviderConfig>;
};
agent: AgentConfig;
voices: {
default: string[];
available: string[];
parallel: boolean;
maxConcurrent: number;
};
database: {
path: string;
inMemory: boolean;
enableWAL: boolean;
backupEnabled: boolean;
backupInterval: number;
};
safety: {
commandValidation: boolean;
fileSystemRestrictions: boolean;
requireConsent: string[];
};
terminal: {
shell: string;
prompt: string;
historySize: number;
colorOutput: boolean;
};
vscode: {
autoActivate: boolean;
inlineGeneration: boolean;
showVoicePanel: boolean;
};
mcp: {
servers: {
filesystem: { enabled: boolean; restrictedPaths: string[]; allowedPaths: string[] };
git: { enabled: boolean; autoCommitMessages: boolean; safeModeEnabled: boolean };
terminal: { enabled: boolean; allowedCommands: string[]; blockedCommands: string[] };
packageManager: { enabled: boolean; autoInstall: boolean; securityScan: boolean };
smithery: { enabled: boolean; apiKey?: string; profile?: string; baseUrl?: string };
};
};
performance: {
responseCache: { enabled: boolean; maxAge: number; maxSize: number };
voiceParallelism: { maxConcurrent: number; batchSize: number };
contextManagement: {
maxContextLength: number;
compressionThreshold: number;
retentionStrategy: string;
};
};
logging: {
level: string;
toFile: boolean;
maxFileSize: string;
maxFiles: number;
};
}
export class ConfigManager {
private static instance: ConfigManager;
private config: AppConfig | null = null;
private configPath: string;
private defaultConfigPath: string;
constructor() {
this.configPath = join(homedir(), '.codecrucible', 'config.yaml');
this.defaultConfigPath = join(process.cwd(), 'config', 'default.yaml');
}
static async load(): Promise<AppConfig> {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
// Initialize encryption for sensitive data
await SecurityUtils.initializeEncryption();
}
return await ConfigManager.instance.loadConfiguration();
}
static async getInstance(): Promise<ConfigManager> {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
await ConfigManager.instance.loadConfiguration();
}
return ConfigManager.instance;
}
public async loadConfiguration(): Promise<AppConfig> {
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: string, value: any): Promise<void> {
// eslint-disable-line @typescript-eslint/no-explicit-any
if (!this.config) {
await this.loadConfiguration();
}
const keys = key.split('.');
let current: Record<string, any> = this.config as any; // 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: string): Promise<any> {
// eslint-disable-line @typescript-eslint/no-explicit-any
if (!this.config) {
await this.loadConfiguration();
}
const keys = key.split('.');
let current: Record<string, any> = this.config as any; // 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(): Promise<void> {
this.config = this.getHardcodedDefaults();
await this.saveUserConfig();
logger.info('Configuration reset to defaults');
}
getAll(): AppConfig {
if (!this.config) {
throw new Error('Configuration not loaded');
}
return { ...this.config };
}
private async saveUserConfig(): Promise<void> {
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;
}
}
private getHardcodedDefaults(): AppConfig {
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
*/
private encryptSensitiveFields(config: any): void {
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
*/
private decryptSensitiveFields(config: any): void {
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
*/
private getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
/**
* Set nested value in object using dot notation
*/
private setNestedValue(obj: any, path: string, value: any): void {
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(): Promise<AgentConfig> {
const config = await this.loadConfiguration();
return config.agent;
}
/**
* Update agent configuration (consolidated from core/config.ts)
*/
async updateAgentConfig(newConfig: AgentConfig): Promise<void> {
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: ConfigManager | null = null;
export const configManager = {
async getInstance(): Promise<ConfigManager> {
if (!configManagerInstance) {
configManagerInstance = await ConfigManager.getInstance();
}
return configManagerInstance;
},
async getAgentConfig(): Promise<AgentConfig> {
const instance = await this.getInstance();
return instance.getAgentConfig();
},
async updateAgentConfig(config: AgentConfig): Promise<void> {
const instance = await this.getInstance();
return instance.updateAgentConfig(config);
},
};