codecrucible-synth
Version:
Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability
1,355 lines (1,180 loc) ⢠42.5 kB
text/typescript
/**
* Refactored CLI - Main Interface
* Reduced from 2334 lines to ~400 lines by extracting modules
*/
import { CLIExitCode, CLIError, ModelRequest, REPLInterface } from './types.js';
import { UnifiedModelClient } from '../refactor/unified-model-client.js';
import { VoiceArchetypeSystem } from '../voices/voice-archetype-system.js';
import { MCPServerManager } from '../mcp-servers/mcp-server-manager.js';
import { AppConfig } from '../config/config-manager.js';
import { logger } from './logger.js';
import { getErrorMessage } from '../utils/error-utils.js';
import { ContextAwareCLIIntegration } from './intelligence/context-aware-cli-integration.js';
import { AutoConfigurator } from './model-management/auto-configurator.js';
import { InteractiveREPL } from './interactive-repl.js';
// import { SecureToolFactory } from './security/secure-tool-factory.js';
import { InputSanitizer } from './security/input-sanitizer.js';
// Import modular CLI components
import { CLIOptions, CLIContext, CLIDisplay, CLIParser, CLICommands } from './cli/index.js';
import { AuthMiddleware, AuthenticatedRequest } from './middleware/auth-middleware.js';
import { RBACSystem } from './security/production-rbac-system.js';
import { SecretsManager } from './security/secrets-manager.js';
import { ProductionDatabaseManager } from '../database/production-database-manager.js';
import {
SecurityAuditLogger,
AuditEventType,
AuditSeverity,
AuditOutcome,
} from './security/security-audit-logger.js';
import { DynamicModelRouter } from './dynamic-model-router.js';
import { AdvancedToolOrchestrator } from './tools/advanced-tool-orchestrator.js';
import {
EnterpriseSystemPromptBuilder,
RuntimeContext,
} from './enterprise-system-prompt-builder.js';
import {
SequentialDualAgentSystem,
SequentialAgentConfig,
} from './collaboration/sequential-dual-agent-system.js';
export type { CLIContext, CLIOptions };
import chalk from 'chalk';
import ora from 'ora';
import inquirer from 'inquirer';
import { writeFile } from 'fs/promises';
export class CLI implements REPLInterface {
private context: CLIContext;
private initialized = false;
private workingDirectory = process.cwd();
private contextAwareCLI: ContextAwareCLIIntegration;
private autoConfigurator: AutoConfigurator;
private repl?: InteractiveREPL;
private commands: CLICommands;
private authMiddleware: AuthMiddleware;
private rbacSystem: RBACSystem;
private secretsManager: SecretsManager;
private auditLogger: SecurityAuditLogger;
private dynamicModelRouter: DynamicModelRouter;
private toolOrchestrator: AdvancedToolOrchestrator;
private static globalListenersRegistered = false;
// PERFORMANCE FIX: AbortController pattern for cleanup
private abortController: AbortController;
private activeOperations: Set<string> = new Set();
private isShuttingDown = false;
constructor(
modelClient: UnifiedModelClient,
voiceSystem: VoiceArchetypeSystem,
mcpManager: MCPServerManager,
config: AppConfig
) {
// PERFORMANCE FIX: Initialize AbortController for cleanup
this.abortController = new AbortController();
this.context = {
modelClient,
voiceSystem,
mcpManager,
config,
};
// Initialize security systems
this.secretsManager = new SecretsManager();
const databaseConfig = {
type: 'sqlite' as const,
database: ':memory:',
pool: {
min: 2,
max: 10,
acquireTimeoutMillis: 60000,
createTimeoutMillis: 30000,
destroyTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
reapIntervalMillis: 1000,
createRetryIntervalMillis: 200,
},
};
const databaseManager = new ProductionDatabaseManager(databaseConfig);
this.rbacSystem = new RBACSystem(databaseManager, this.secretsManager);
this.auditLogger = new SecurityAuditLogger(this.secretsManager);
this.authMiddleware = new AuthMiddleware(this.rbacSystem, this.secretsManager);
// Initialize dynamic model router
this.dynamicModelRouter = new DynamicModelRouter();
// Initialize tool orchestrator for autonomous operations
this.toolOrchestrator = new AdvancedToolOrchestrator(modelClient);
// Initialize subsystems with simplified constructors
this.contextAwareCLI = new ContextAwareCLIIntegration();
this.autoConfigurator = new AutoConfigurator();
// Defer InteractiveREPL creation until actually needed to avoid race conditions with piped input
this.commands = new CLICommands(this.context, this.workingDirectory);
this.registerCleanupHandlers();
}
/**
* Build runtime context for system prompt generation
*/
private async buildRuntimeContext(): Promise<RuntimeContext> {
let isGitRepo = false;
let currentBranch = 'unknown';
try {
const { execSync } = await import('child_process');
execSync('git status', { cwd: this.workingDirectory, stdio: 'ignore' });
isGitRepo = true;
currentBranch = execSync('git branch --show-current', {
cwd: this.workingDirectory,
})
.toString()
.trim();
} catch {
// Not a git repo or git not available
}
return {
workingDirectory: this.workingDirectory,
isGitRepo,
platform: process.platform,
currentBranch,
modelId: 'CodeCrucible Synth',
knowledgeCutoff: 'January 2025',
};
}
/**
* Build system prompt with enterprise configuration
*/
private async buildSystemPrompt(): Promise<string> {
try {
const context = await this.buildRuntimeContext();
return EnterpriseSystemPromptBuilder.buildSystemPrompt(context, {
conciseness: 'ultra',
securityLevel: 'enterprise',
});
} catch (error) {
logger.warn('Failed to build system prompt, using fallback', error);
return `You are CodeCrucible Synth, an AI-powered code generation and analysis tool. Use available tools to assist with software engineering tasks.`;
}
}
/**
* Get InteractiveREPL instance, creating it only when needed
*/
private getREPL(): InteractiveREPL {
if (!this.repl) {
logger.debug('Creating InteractiveREPL on demand');
this.repl = new InteractiveREPL(this, this.context);
}
return this.repl;
}
/**
* Register cleanup handlers for graceful shutdown
*/
private registerCleanupHandlers(): void {
if (CLI.globalListenersRegistered) return;
CLI.globalListenersRegistered = true;
// Handle process exit
process.on('exit', () => {
this.syncCleanup();
});
// Handle SIGINT (Ctrl+C)
process.on('SIGINT', async () => {
console.log(chalk.yellow('\nš Shutting down gracefully...'));
try {
await this.destroy();
} catch (error) {
console.error('Cleanup error:', error);
}
process.exit(0);
});
// Handle SIGTERM
process.on('SIGTERM', async () => {
try {
await this.destroy();
} catch (error) {
console.error('Cleanup error:', error);
}
process.exit(0);
});
// Cleanup on uncaught exceptions
process.on('uncaughtException', async error => {
console.error('Uncaught Exception:', error);
try {
await this.destroy();
} catch (cleanupError) {
console.error('Cleanup error:', cleanupError);
}
process.exit(1);
});
// Cleanup on unhandled promise rejections - TEMPORARILY DISABLED FOR DEBUGGING
process.on('unhandledRejection', async (reason, promise) => {
console.error('šØ UNHANDLED REJECTION DETECTED:');
console.error('Promise:', promise);
console.error('Reason:', reason);
console.error('Stack:', reason instanceof Error ? reason.stack : 'No stack trace');
// DON'T EXIT - just log for now
// try {
// await this.destroy();
// } catch (cleanupError) {
// console.error('Cleanup error:', cleanupError);
// }
// process.exit(1);
});
}
/**
* Synchronous cleanup for exit handler
*/
private syncCleanup(): void {
try {
// Only perform synchronous cleanup operations here
if (
this.contextAwareCLI &&
typeof (this.contextAwareCLI as any).removeAllListeners === 'function'
) {
(this.contextAwareCLI as any).removeAllListeners();
}
if (
this.autoConfigurator &&
typeof (this.autoConfigurator as any).removeAllListeners === 'function'
) {
(this.autoConfigurator as any).removeAllListeners();
}
} catch (error) {
// Silent fail for exit handler
console.error('Sync cleanup error:', error);
}
}
/**
* Main CLI entry point
*/
async run(args: string[]): Promise<void> {
logger.debug('CLI.run() called', { args });
try {
// Handle help requests
if (CLIParser.isHelpRequest(args)) {
CLIDisplay.showHelp();
return;
}
// Parse command and options
const { command, remainingArgs } = CLIParser.extractCommand(args);
const options = CLIParser.parseOptions(args);
// Initialize if needed
if (!this.initialized && !options.skipInit) {
logger.debug('About to initialize CLI');
await this.initialize();
logger.debug('CLI initialized');
}
// Handle commands
logger.debug('About to call executeCommand');
await this.executeCommand(command, remainingArgs, options);
logger.debug('executeCommand completed');
} catch (error) {
await this.handleError(error);
}
}
/**
* Execute specific commands
*/
private async executeCommand(
command: string,
args: string[],
options: CLIOptions
): Promise<void> {
logger.debug('executeCommand called', {
command: `"${command}"`,
args,
commandLength: command.length,
});
// Handle sequential review
if (options.sequentialReview) {
const promptText = args.join(' ') || command || 'Generate code';
await this.handleSequentialReview(promptText);
return;
}
// Audit log command execution for enterprise security compliance
if (this.initialized && this.auditLogger) {
await this.auditLogger.logEvent(
AuditEventType.API_ACCESS,
AuditSeverity.LOW,
AuditOutcome.SUCCESS,
'cli',
'command_execution',
'v4.0.0',
`CLI command executed: ${command}`,
{
ipAddress: 'localhost',
userAgent: 'codecrucible-cli',
},
{
command,
args: args.length > 0 ? args : undefined,
options: Object.keys(options).length > 0 ? options : undefined,
workingDirectory: this.workingDirectory,
}
);
}
switch (command) {
case 'status':
await this.commands.showStatus();
break;
case 'models':
await this.commands.listModels();
break;
case 'analyze':
await this.commands.handleAnalyze(args, options);
break;
case 'analyze-dir': {
// Handle analyze-dir command properly
const targetDir = args[0] || '.';
await this.commands.handleAnalyze([targetDir], options);
break;
}
case 'generate': {
const prompt = args.join(' ');
await this.commands.handleGeneration(prompt, options);
break;
}
case 'configure':
await this.handleConfiguration(options);
break;
case 'help':
CLIDisplay.showHelp();
break;
default:
logger.debug('In default case', { command, args });
// Handle as prompt if no specific command
if (args.length > 0 || command) {
const fullPrompt = [command, ...args].filter(Boolean).join(' ');
logger.debug('About to call processPrompt', { fullPrompt });
const result = await this.processPrompt(fullPrompt, options);
if (result && typeof result === 'string') {
logger.debug('About to display result');
console.log(result);
logger.debug('Result displayed');
} else {
logger.debug('Result not displayed - failed condition check');
}
} else {
logger.debug('No args or command, starting interactive mode');
// Interactive mode
await this.startInteractiveMode(options);
}
break;
}
// Handle special flags
if (options.server) {
// Import serverMode dynamically to avoid circular dependency
const { ServerMode } = await import('../server/server-mode.js');
const serverMode = new ServerMode();
await this.commands.startServer(options, serverMode);
}
}
/**
* Authenticate CLI request
*/
private async authenticateRequest(
command: string,
options: CLIOptions
): Promise<AuthenticatedRequest> {
try {
// Extract authentication headers from options if available
const headers: Record<string, string> = {};
if (options.token) {
headers['x-auth-token'] = options.token;
}
if (options.apiKey) {
headers['x-api-key'] = options.apiKey;
}
// Get client IP (for local CLI, use localhost)
const ipAddress = '127.0.0.1';
// Check for stored session token
if (!options.token && !options.apiKey) {
const storedToken = await this.authMiddleware.loadStoredToken();
if (storedToken) {
headers['x-auth-token'] = storedToken;
}
}
return await this.authMiddleware.authenticateRequest(
command,
headers,
ipAddress,
!options.batch // Interactive mode unless batch
);
} catch (error) {
if (error instanceof CLIError) {
throw error;
}
logger.error('Authentication request failed', error as Error);
throw new CLIError('Authentication failed', CLIExitCode.AUTHENTICATION_FAILED);
}
}
/**
* Process a text prompt using the voice system
*/
async processPrompt(prompt: string, options: CLIOptions = {}): Promise<string> {
if (!prompt || prompt.trim().length === 0) {
throw new CLIError('Empty prompt provided', CLIExitCode.INVALID_INPUT);
}
// Handle slash commands for role switching
const { CLIParser } = await import('./cli/cli-parser.js');
const slashCommand = CLIParser.parseSlashCommand(prompt);
if (slashCommand.command === 'role-switch' && slashCommand.role) {
return await this.handleRoleSwitch(
slashCommand.role as 'auditor' | 'writer' | 'auto',
slashCommand.content,
options
);
} else if (slashCommand.command === 'slash-help') {
return this.showSlashHelp();
} else if (slashCommand.command === 'unknown-slash') {
console.log(`Unknown slash command: ${prompt}`);
return this.showSlashHelp();
}
// Authenticate request
const auth = await this.authenticateRequest('process', options);
// Check permission for prompt processing
const hasPermission = await this.authMiddleware.validatePermission(
auth,
'process_prompt',
'cli'
);
if (!hasPermission) {
throw new CLIError(
'Insufficient permissions to process prompts',
CLIExitCode.PERMISSION_DENIED
);
}
// Modern security system with user consent (Claude Code pattern)
const isTestEnvironment =
process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
let sanitizationResult: any;
try {
const { ModernInputSanitizer } = await import('./security/modern-input-sanitizer.js');
sanitizationResult = await ModernInputSanitizer.sanitizePrompt(prompt, {
operation: 'cli_prompt_processing',
workingDirectory: process.cwd(),
});
// Handle consent requirement with actual user interaction
if (sanitizationResult.requiresConsent && !isTestEnvironment) {
logger.info('Security review required for input', {
reason: sanitizationResult.securityDecision?.reason,
riskLevel: sanitizationResult.securityDecision?.riskLevel,
});
// Show security notice and get user consent
console.log(`\nā ļø Security Review Required`);
console.log(` Operation: ${sanitizationResult.securityDecision?.reason}`);
console.log(
` Risk Level: ${sanitizationResult.securityDecision?.riskLevel?.toUpperCase()}`
);
console.log(` Input: ${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}`);
// In CLI mode, we'll proceed but make it clear this needs user confirmation
// A full GUI implementation would show an interactive dialog here
console.log(`\n ā Proceeding with operation (security policy allows with notice)`);
console.log(` š This operation is logged for audit purposes\n`);
}
// Only block if explicitly denied by security system
if (!sanitizationResult.isValid && !isTestEnvironment) {
const securityError = ModernInputSanitizer.createSecurityError(
sanitizationResult,
'CLI prompt processing'
);
logger.error('Security violation detected in prompt', securityError);
throw new CLIError(
`Security policy violation: ${sanitizationResult.violations.join(', ')}`,
CLIExitCode.INVALID_INPUT
);
}
// Update prompt with sanitized version
prompt = sanitizationResult.sanitized;
} catch (importError) {
// Fallback to old system if new one fails to load
logger.warn('Failed to load modern security system, using fallback', importError);
sanitizationResult = InputSanitizer.sanitizePrompt(prompt);
if (!sanitizationResult.isValid && !isTestEnvironment) {
const securityError = InputSanitizer.createSecurityError(
sanitizationResult,
'CLI prompt processing'
);
logger.error('Security violation detected in prompt', securityError);
throw new CLIError(
`Security violation: ${sanitizationResult.violations.join(', ')}`,
CLIExitCode.INVALID_INPUT
);
}
}
// In test environment, log but don't block for development
if (sanitizationResult && !sanitizationResult.isValid && isTestEnvironment) {
logger.warn('Test environment: Security validation bypassed for development', {
violations: sanitizationResult.violations,
prompt: `${prompt.substring(0, 100)}...`,
});
}
const sanitizedPrompt = sanitizationResult?.sanitized || prompt;
try {
// ALWAYS use the AI agent for all requests - no hardcoded shortcuts
console.log(chalk.cyan('š¤ Processing with AI agent...'));
if (options.noStream || options.batch) {
const response = await this.executePromptProcessing(sanitizedPrompt, options);
console.log(`\n${chalk.cyan('š¤ Response:')}`);
console.log(response);
return response;
} else {
await this.displayStreamingResponse(sanitizedPrompt, options);
return 'Streaming response completed';
}
} catch (error) {
logger.error('Prompt processing failed:', error);
throw new CLIError(`Processing failed: ${error}`, CLIExitCode.EXECUTION_FAILED);
}
}
/**
* Execute prompt processing with voice system
*/
// Removed isAnalysisRequest - AI agent now handles all requests
// Removed hardcoded analysis methods - AI agent now handles all requests dynamically
/**
* Legacy analysis method (kept for backward compatibility)
*/
/**
* Analyze project structure and metadata
*/
/**
* Analyze code metrics and lines of code
*/
/**
* Analyze dependencies from package.json
*/
/**
* Analyze configuration files
*/
/**
* Analyze test coverage and test files
*/
/**
* Discover project components by analyzing file structure
*/
/**
* Discover architecture components by analyzing imports and exports
*/
/**
* Detect real issues in the codebase
*/
/**
* Assess security configuration
*/
/**
* Analyze performance characteristics
*/
/**
* Generate recommendations based on analysis
*/
private async generateRecommendations(
codeMetrics: any,
testAnalysis: any,
dependencyAnalysis: any
): Promise<string> {
const recommendations: string[] = [];
// Critical recommendations based on real analysis
recommendations.push(
'1. **URGENT**: Fix UnifiedModelClient.generateText() hanging issue to restore full functionality'
);
if (testAnalysis.estimatedCoverage < 50) {
recommendations.push(
`2. **High Priority**: Expand test coverage to 70%+ (currently ~${
testAnalysis.estimatedCoverage
}%)`
);
}
if (codeMetrics.typescriptFiles > 0) {
recommendations.push(
'3. **Medium Priority**: Enable TypeScript strict mode for better type safety'
);
}
if (dependencyAnalysis.devDeps > dependencyAnalysis.prodDeps * 2) {
recommendations.push(
'4. **Low Priority**: Review development dependencies - high dev/prod ratio'
);
}
recommendations.push(
'5. **Enhancement**: Implement automated code quality gates in CI/CD pipeline'
);
return recommendations.join('\n');
}
/**
* Count files by extension for analysis
*/
private async countFilesByType(): Promise<Record<string, number>> {
const path = await import('path');
const fs = await import('fs');
const counts: Record<string, number> = {};
const countInDirectory = (dir: string, maxDepth: number = 2): void => {
if (maxDepth <= 0) return;
try {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
if (item.name.startsWith('.') || item.name === 'node_modules') continue;
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
countInDirectory(fullPath, maxDepth - 1);
} else if (item.isFile()) {
const ext = path.extname(item.name) || 'no-extension';
counts[ext] = (counts[ext] || 0) + 1;
}
}
} catch (error) {
// Ignore permission errors
}
};
countInDirectory(this.workingDirectory);
return counts;
}
/**
* Analyze the task type from the prompt
*/
/**
* Execute in Auditor mode (LM Studio optimized for fast analysis)
*/
/**
* Execute in Writer mode (Ollama optimized for quality generation)
*/
/**
* Execute in Auto mode (intelligent routing)
*/
/**
* Configure the model client to use the selected model
*/
/**
* Format the response based on execution mode
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
/**
* Display streaming response using enhanced streaming client
*/
/**
* Start interactive mode
*/
private async startInteractiveMode(options: CLIOptions = {}): Promise<void> {
console.log(chalk.cyan('\nšÆ Starting Interactive Mode'));
console.log(chalk.gray('Type "exit" to quit, "/help" for commands\n'));
// eslint-disable-next-line no-constant-condition
while (true) {
try {
const prompt = await inquirer.prompt({
type: 'input',
name: 'prompt',
message: chalk.cyan('š'),
});
if (prompt.prompt.toLowerCase().trim() === 'exit') {
console.log(chalk.yellow('š Goodbye!'));
break;
}
if (prompt.prompt.trim().startsWith('/')) {
await this.handleSlashCommand(prompt.prompt.trim(), options);
} else if (prompt.prompt.trim()) {
await this.processPrompt(prompt.prompt, options);
}
} catch (error: unknown) {
const errorMessage = getErrorMessage(error);
if (errorMessage.includes('User force closed')) {
console.log(chalk.yellow('\nš Goodbye!'));
break;
}
console.error(chalk.red('Error:'), error);
}
}
}
/**
* Handle slash commands
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async handleSlashCommand(command: string, _options: CLIOptions): Promise<void> {
const cmd = command.slice(1).toLowerCase();
switch (cmd) {
case 'help':
CLIDisplay.showHelp();
break;
case 'status':
await this.commands.showStatus();
break;
case 'models':
await this.commands.listModels();
break;
case 'clear':
console.clear();
break;
case 'exit':
console.log(chalk.yellow('š Goodbye!'));
process.exit(0);
// break; // Removed - unreachable after process.exit()
default:
// Check if it's a model switch command
if (cmd.startsWith('model:')) {
const modelName = cmd.split(':')[1];
await this.handleModelSwitch(modelName);
break;
}
if (cmd === 'model-list' || cmd === 'ml') {
await this.handleModelList();
break;
}
if (cmd === 'model-reload' || cmd === 'mr') {
await this.handleModelReload();
break;
}
console.log(chalk.yellow(`Unknown command: ${command}`));
console.log(chalk.gray('Type "/help" for available commands'));
break;
}
}
/**
* Handle file output
*/
private async handleFileOutput(filepath: string, code: string): Promise<void> {
try {
await writeFile(filepath, code, 'utf-8');
console.log(chalk.green(`ā
Code saved to: ${filepath}`));
} catch (error) {
console.error(chalk.red(`ā Failed to save file: ${error}`));
}
}
/**
* Initialize the CLI system
*/
async initialize(config?: any, workingDirectory?: string): Promise<void> {
if (this.initialized) return;
if (workingDirectory) {
this.workingDirectory = workingDirectory;
}
try {
// Initialize security systems first
await this.secretsManager.initialize();
await this.auditLogger.initialize();
await this.authMiddleware.initialize();
// Log CLI initialization attempt
await this.auditLogger.logEvent(
AuditEventType.SYSTEM_EVENT,
AuditSeverity.MEDIUM,
AuditOutcome.SUCCESS,
'cli',
'cli_initialization',
'v4.0.0',
'CLI system initialization started',
{},
{
workingDirectory: this.workingDirectory,
authEnabled: this.authMiddleware.isAuthEnabled(),
authRequired: this.authMiddleware.isAuthRequired(),
}
);
// Context awareness is initialized in constructor
this.initialized = true;
logger.info('CLI initialized successfully', {
authEnabled: this.authMiddleware.isAuthEnabled(),
authRequired: this.authMiddleware.isAuthRequired(),
});
} catch (error) {
// Log initialization failure
await this.auditLogger.logEvent(
AuditEventType.ERROR_EVENT,
AuditSeverity.HIGH,
AuditOutcome.FAILURE,
'cli',
'cli_initialization_failed',
'v4.0.0',
`CLI initialization failed: ${error}`,
{},
{ error: error instanceof Error ? error.message : 'Unknown error' }
);
logger.error('CLI initialization failed:', error);
throw new CLIError(`Initialization failed: ${error}`, CLIExitCode.INITIALIZATION_FAILED);
}
}
/**
* Handle configuration management
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async handleConfiguration(_options: CLIOptions): Promise<void> {
console.log(chalk.cyan('\nāļø Configuration Management'));
const choices = [
'View current configuration',
'Update model settings',
'Configure voice preferences',
'Set performance options',
'Reset to defaults',
];
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to configure?',
choices,
},
]);
switch (action) {
case 'View current configuration':
console.log(chalk.gray('\nCurrent configuration:'));
console.log(JSON.stringify(this.context.config, null, 2));
break;
default:
console.log(chalk.yellow('Configuration feature coming soon!'));
break;
}
}
/**
* Clean up resources
*/
async destroy(): Promise<void> {
try {
if (this.contextAwareCLI) {
await (this.contextAwareCLI as any).destroy?.();
}
if (this.autoConfigurator) {
await (this.autoConfigurator as any).destroy?.();
}
if (this.repl) {
await (this.repl as any).destroy?.();
}
this.initialized = false;
logger.info('CLI destroyed successfully');
} catch (error) {
logger.error('Error during CLI cleanup:', error);
}
}
/**
* Handle errors with proper formatting and exit codes
*/
private async handleError(error: any): Promise<void> {
console.error(chalk.red('\nā Error:'), error.message || error);
// Audit log error for enterprise security monitoring
if (this.auditLogger) {
try {
await this.auditLogger.logEvent(
AuditEventType.ERROR_EVENT,
AuditSeverity.HIGH,
AuditOutcome.ERROR,
'cli',
'cli_error',
'v4.0.0',
`CLI error occurred: ${error.message || error}`,
{
ipAddress: 'localhost',
userAgent: 'codecrucible-cli',
},
{
errorType: error.constructor.name,
errorMessage: error.message || error.toString(),
exitCode: error instanceof CLIError ? error.exitCode : CLIExitCode.UNEXPECTED_ERROR,
workingDirectory: this.workingDirectory,
stack: error.stack || 'No stack trace available',
}
);
} catch (auditError) {
// Don't fail on audit logging errors
logger.warn('Failed to log error to audit system', auditError);
}
}
if (error instanceof CLIError) {
process.exit(error.exitCode);
} else {
process.exit(CLIExitCode.UNEXPECTED_ERROR);
}
}
/**
* Track an active operation for graceful shutdown
*/
private trackOperation(operationId: string): void {
if (!this.isShuttingDown) {
this.activeOperations.add(operationId);
}
}
/**
* Untrack a completed operation
*/
private untrackOperation(operationId: string): void {
this.activeOperations.delete(operationId);
}
// Legacy compatibility methods
async checkOllamaStatus(): Promise<boolean> {
try {
const result = await this.context.modelClient.checkHealth();
return Object.values(result).some(status => status === true);
} catch {
return false;
}
}
async getAllAvailableModels(): Promise<any[]> {
try {
const result = (await this.context.modelClient.getAllAvailableModels?.()) || [];
return Array.isArray(result) ? result : [];
} catch {
return [];
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async updateConfiguration(_newConfig: any): Promise<boolean> {
try {
// Update configuration logic here
return true;
} catch (error) {
logger.error('Configuration update failed:', error);
return false;
}
}
// Delegate methods to command handlers
/**
* Handle role switching via slash commands
*/
private async handleRoleSwitch(
role: 'auditor' | 'writer' | 'auto',
content: string,
options: CLIOptions
): Promise<string> {
try {
// Switch the role in the dynamic model router
this.dynamicModelRouter.setRole(role);
// Show confirmation
const roleDescription = {
auditor:
'Auditor mode (LM Studio) - Optimized for fast code analysis and security auditing',
writer:
'Writer mode (Ollama) - Optimized for high-quality code generation and documentation',
auto: 'Auto mode - Intelligent routing based on task requirements',
};
console.log(chalk.green(`ā
Switched to ${role} mode`));
console.log(chalk.blue(`š ${roleDescription[role]}`));
// Show current best model
const bestModel = await this.dynamicModelRouter.getBestModelForCurrentRole(
role === 'auditor' ? 'audit' : role === 'writer' ? 'generate' : 'general'
);
if (bestModel) {
console.log(chalk.cyan(`š¤ Using model: ${bestModel.model} (${bestModel.provider})`));
console.log(chalk.gray(` Strengths: ${bestModel.strengths.join(', ')}`));
} else {
console.log(chalk.yellow(`ā ļø No suitable models found for ${role} mode`));
}
// If there's content after the slash command, process it with the new role
if (content && content.trim()) {
console.log(chalk.blue('\nš Processing with new role...'));
// Update options to reflect the role change
const updatedOptions = { ...options, role, forceProvider: bestModel?.provider };
return await this.executePromptProcessing(content, updatedOptions);
}
return `Role switched to ${role}`;
} catch (error: unknown) {
const errorMessage = getErrorMessage(error);
logger.error('Role switch failed:', errorMessage);
return `Failed to switch to ${role} mode: ${errorMessage}`;
}
}
/**
* Handle model switching
*/
/**
* Handle model listing
*/
/**
* Handle model reload
*/
/**
* Show slash command help
*/
/**
* Handle Sequential Dual Agent Review
*/
private async handleSequentialReview(content: string): Promise<string> {
// Implementation placeholder
return `Sequential review processing: ${content}`;
}
/**
* Show system status
*/
public async showStatus(): Promise<void> {
console.log(chalk.cyan('š CodeCrucible Synth Status'));
console.log(chalk.green('ā
System initialized'));
console.log(`Current directory: ${this.workingDirectory}`);
}
/**
* List available models
*/
public listModels(): void {
console.log(chalk.cyan('š¤ Available Models'));
console.log('- Auto detection enabled');
console.log('- Providers: Ollama, LM Studio, HuggingFace');
}
/**
* Execute prompt processing (REPLInterface method)
*/
public async executePromptProcessing(prompt: string, options: any = {}): Promise<any> {
try {
if (!options.silent) {
console.log(chalk.blue('š Processing prompt...'));
}
// Use the voice system to process the prompt
let response: string;
if (options.voices && Array.isArray(options.voices) && options.voices.length > 0) {
// Multi-voice processing
const solutions = await this.context.voiceSystem.generateMultiVoiceSolutions(
options.voices,
prompt,
{
workingDirectory: process.cwd(),
parallel: true,
}
);
// Combine the solutions into a coherent response
response = solutions.map((sol: any) => `**${sol.voice}**: ${sol.content}`).join('\n\n');
} else {
// Single voice processing using default voice
const voiceResponse = await this.context.voiceSystem.generateSingleVoiceResponse(
'explorer', // Default voice
prompt,
this.context.modelClient // Pass the actual client, not options
);
response = voiceResponse.content || 'No response generated';
}
if (!options.silent) {
console.log(chalk.green('ā
Processing complete'));
}
return response;
} catch (error) {
if (!options.silent) {
console.error(chalk.red('ā Processing failed:'), error);
}
logger.error('Prompt processing failed in CLI:', error);
throw error;
}
}
/**
* Display streaming response
*/
private async displayStreamingResponse(prompt: string, options: any = {}): Promise<void> {
console.log(chalk.blue('š Streaming response...'));
// Simulate streaming for now
const response = await this.executePromptProcessing(prompt, { ...options, silent: true });
console.log(response);
}
/**
* Show slash command help
*/
private showSlashHelp(): string {
const helpText = [
'š§ Available Slash Commands:',
'/audit - Switch to auditor mode',
'/write - Switch to writer mode',
'/auto - Switch to auto mode',
'/help - Show this help',
].join('\n');
console.log(chalk.cyan(helpText));
return helpText;
}
/**
* Handle model switch
*/
private async handleModelSwitch(model: string): Promise<void> {
try {
console.log(chalk.blue(`š Attempting to switch to model: ${model}`));
// Get the Ollama provider from the provider repository
const provider = this.context.modelClient.providerRepository.getProvider('ollama');
if (!provider || !provider.setModel) {
console.log(chalk.red('ā Model switching not supported for current provider'));
return;
}
const success = await provider.setModel(model);
if (success) {
const modelInfo = provider.getCurrentModelInfo();
console.log(chalk.green(`ā
Successfully switched to: ${modelInfo.name}`));
console.log(chalk.gray(` Type: ${modelInfo.type}`));
console.log(chalk.gray(` Function Calling: ${modelInfo.supportsFunctionCalling ? 'ā
Yes' : 'ā No'}`));
} else {
console.log(chalk.red(`ā Failed to switch to model: ${model}`));
console.log(chalk.gray(' Use /models to see available models'));
}
} catch (error) {
console.log(chalk.red(`ā Error switching model: ${error instanceof Error ? error.message : 'Unknown error'}`));
}
}
/**
* Handle model list
*/
private async handleModelList(): Promise<void> {
try {
console.log(chalk.blue('š Available Models with Capabilities:'));
console.log(chalk.gray('ā'.repeat(60)));
// Get the Ollama provider
const provider = this.context.modelClient.providerRepository.getProvider('ollama');
if (!provider || !provider.listModels) {
console.log(chalk.red('ā Model listing not supported for current provider'));
return;
}
const models = await provider.listModels();
if (models.length === 0) {
console.log(chalk.yellow('š No models available. Install models with: ollama pull <model>'));
return;
}
// Group models by capability
const functionCallingModels = provider.detectFunctionCallingModels(models);
const codingModels = provider.detectCodingModels(models);
const currentModel = provider.getCurrentModelInfo();
// Show current model
console.log(chalk.green(`šÆ Current: ${currentModel.name}`));
console.log(chalk.gray(` Type: ${currentModel.type} | Function Calling: ${currentModel.supportsFunctionCalling ? 'ā
' : 'ā'}`));
console.log();
// Show function-calling models
if (functionCallingModels.length > 0) {
console.log(chalk.cyan('š ļø Function-Calling Capable Models (Recommended):'));
functionCallingModels.forEach((model: string) => {
const current = model === currentModel.name ? ' š current' : '';
const size = provider.extractModelSize(model);
console.log(chalk.green(` ā
${model} (${size}B)${current}`));
});
console.log();
}
// Show coding models
if (codingModels.length > 0) {
console.log(chalk.blue('š» Coding Models (Limited Tool Support):'));
codingModels.forEach((model: string) => {
if (!functionCallingModels.includes(model)) {
const current = model === currentModel.name ? ' š current' : '';
const size = provider.extractModelSize(model);
console.log(chalk.yellow(` ā ļø ${model} (${size}B)${current}`));
}
});
console.log();
}
// Show other models
const otherModels = models.filter((m: string) =>
!functionCallingModels.includes(m) && !codingModels.includes(m)
);
if (otherModels.length > 0) {
console.log(chalk.gray('š¤ Other Models (No Tool Support):'));
otherModels.forEach((model: string) => {
const current = model === currentModel.name ? ' š current' : '';
const size = provider.extractModelSize(model);
console.log(chalk.gray(` ā ${model} (${size}B)${current}`));
});
console.log();
}
console.log(chalk.gray('š” Switch models with: /model:modelname'));
console.log(chalk.gray(' Example: /model:llama3.1:8b'));
} catch (error) {
console.log(chalk.red(`ā Error listing models: ${error instanceof Error ? error.message : 'Unknown error'}`));
}
}
/**
* Handle model reload
*/
private async handleModelReload(): Promise<void> {
try {
console.log(chalk.blue('š Reloading models and re-detecting capabilities...'));
// Get the Ollama provider
const provider = this.context.modelClient.providerRepository.getProvider('ollama');
if (!provider || !provider.checkStatus) {
console.log(chalk.red('ā Model reloading not supported for current provider'));
return;
}
// Re-initialize the provider to detect models
await provider.checkStatus();
console.log(chalk.green('ā
Models reloaded and capabilities updated'));
// Show current model info
const modelInfo = provider.getCurrentModelInfo();
console.log(chalk.gray(`Current model: ${modelInfo.name} (${modelInfo.type})`));
// Suggest listing models
console.log(chalk.gray('š” Use /ml to see all available models with capabilities'));
} catch (error) {
console.log(chalk.red(`ā Error reloading models: ${error instanceof Error ? error.message : 'Unknown error'}`));
}
}
}
// Export alias for backward compatibility
export const CodeCrucibleCLI = CLI;
export default CLI;