UNPKG

mnemos-coder

Version:

CLI-based coding agent with graph-based execution loop and terminal UI

290 lines 10.7 kB
/** * Execution logger for capturing stdio and tracking execution progress */ import { EventEmitter } from 'events'; import { promises as fs } from 'fs'; import path from 'path'; export class ExecutionLogger extends EventEmitter { logs = []; isCapturing = false; originalConsole; capturedOutput = ''; metrics; logFile; constructor(options = {}) { super(); // Store original console methods this.originalConsole = { log: console.log.bind(console), error: console.error.bind(console), warn: console.warn.bind(console), info: console.info.bind(console), debug: console.debug.bind(console) }; this.logFile = options.logFile; // Initialize metrics this.metrics = { start_time: Date.now(), memory_usage: { initial: process.memoryUsage(), peak: process.memoryUsage() }, operations_count: 0, errors_count: 0, warnings_count: 0 }; } /** * Start capturing console output */ startCapture() { if (this.isCapturing) return; this.isCapturing = true; this.capturedOutput = ''; this.metrics.start_time = Date.now(); this.metrics.memory_usage.initial = process.memoryUsage(); // Override console methods console.log = this.createCaptureMethod('info', this.originalConsole.log); console.error = this.createCaptureMethod('error', this.originalConsole.error); console.warn = this.createCaptureMethod('warn', this.originalConsole.warn); console.info = this.createCaptureMethod('info', this.originalConsole.info); console.debug = this.createCaptureMethod('debug', this.originalConsole.debug); // Capture process stdout/stderr this.captureProcessStreams(); this.emit('captureStarted'); } /** * Stop capturing and return captured output */ stopCapture() { if (!this.isCapturing) return this.capturedOutput; this.isCapturing = false; this.metrics.end_time = Date.now(); this.metrics.duration = this.metrics.end_time - this.metrics.start_time; this.metrics.memory_usage.final = process.memoryUsage(); // Restore original console methods console.log = this.originalConsole.log; console.error = this.originalConsole.error; console.warn = this.originalConsole.warn; console.info = this.originalConsole.info; console.debug = this.originalConsole.debug; this.emit('captureStopped', { output: this.capturedOutput, metrics: this.metrics, logs: this.logs }); return this.capturedOutput; } /** * Log a message with specified level */ log(level, message, data, source) { const entry = { timestamp: Date.now(), level, message, data, source }; this.logs.push(entry); // Update metrics if (level === 'error') this.metrics.errors_count++; if (level === 'warn') this.metrics.warnings_count++; this.metrics.operations_count++; // Update peak memory usage const currentMemory = process.memoryUsage(); if (currentMemory.heapUsed > this.metrics.memory_usage.peak.heapUsed) { this.metrics.memory_usage.peak = currentMemory; } // Emit event this.emit('logEntry', entry); // Write to file if specified if (this.logFile) { this.writeLogToFile(entry).catch(error => { this.originalConsole.error('Failed to write log to file:', error); }); } // If capturing, add to captured output if (this.isCapturing) { const formattedMessage = this.formatLogEntry(entry); this.capturedOutput += formattedMessage + '\n'; } } /** * Get current execution metrics */ getMetrics() { return { ...this.metrics, memory_usage: { ...this.metrics.memory_usage, final: this.metrics.memory_usage.final || process.memoryUsage() } }; } /** * Get all captured logs */ getLogs() { return [...this.logs]; } /** * Filter logs by criteria */ filterLogs(criteria) { return this.logs.filter(log => { if (criteria.level && log.level !== criteria.level) return false; if (criteria.source && log.source !== criteria.source) return false; if (criteria.since && log.timestamp < criteria.since) return false; if (criteria.message) { if (typeof criteria.message === 'string') { if (!log.message.includes(criteria.message)) return false; } else { if (!criteria.message.test(log.message)) return false; } } return true; }); } /** * Export logs to various formats */ async exportLogs(format, filePath) { let content; switch (format) { case 'json': content = JSON.stringify({ metrics: this.getMetrics(), logs: this.logs }, null, 2); break; case 'csv': const headers = 'timestamp,level,source,message,data\n'; const rows = this.logs.map(log => `${log.timestamp},"${log.level}","${log.source || ''}","${log.message.replace(/"/g, '""')}","${JSON.stringify(log.data || '').replace(/"/g, '""')}"`).join('\n'); content = headers + rows; break; case 'txt': content = this.logs.map(log => this.formatLogEntry(log)).join('\n'); break; default: throw new Error(`Unsupported format: ${format}`); } await fs.writeFile(filePath, content, 'utf-8'); } /** * Clear all logs and reset metrics */ clear() { this.logs = []; this.capturedOutput = ''; this.metrics = { start_time: Date.now(), memory_usage: { initial: process.memoryUsage(), peak: process.memoryUsage() }, operations_count: 0, errors_count: 0, warnings_count: 0 }; this.emit('logsCleared'); } /** * Create a performance summary */ createSummary() { const duration = (this.metrics.end_time || Date.now()) - this.metrics.start_time; const opsPerSecond = this.metrics.operations_count / (duration / 1000); // Memory efficiency: lower is better (less memory growth) const initialMemory = this.metrics.memory_usage.initial.heapUsed; const peakMemory = this.metrics.memory_usage.peak.heapUsed; const memoryEfficiency = initialMemory / peakMemory; const errorRate = this.metrics.errors_count / this.metrics.operations_count; const warningRate = this.metrics.warnings_count / this.metrics.operations_count; // Find potential memory consumers (entries logged during memory spikes) const topMemoryConsumers = this.logs .filter(log => log.data && typeof log.data === 'object') .sort((a, b) => b.timestamp - a.timestamp) .slice(0, 5); const recentErrors = this.filterLogs({ level: 'error' }) .sort((a, b) => b.timestamp - a.timestamp) .slice(0, 10); return { duration, operations_per_second: opsPerSecond, memory_efficiency: memoryEfficiency, error_rate: errorRate, warnings_rate: warningRate, top_memory_consumers: topMemoryConsumers, recent_errors: recentErrors }; } createCaptureMethod(level, originalMethod) { return (...args) => { const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)).join(' '); // Log through our system this.log(level, message, args.length === 1 ? args[0] : args, 'console'); // Also call original method for immediate output originalMethod(...args); }; } captureProcessStreams() { // Capture stdout const originalWrite = process.stdout.write.bind(process.stdout); process.stdout.write = (chunk, encoding, callback) => { if (this.isCapturing && typeof chunk === 'string') { this.capturedOutput += chunk; this.log('info', chunk.trim(), undefined, 'stdout'); } return originalWrite(chunk, encoding, callback); }; // Capture stderr const originalErrorWrite = process.stderr.write.bind(process.stderr); process.stderr.write = (chunk, encoding, callback) => { if (this.isCapturing && typeof chunk === 'string') { this.capturedOutput += chunk; this.log('error', chunk.trim(), undefined, 'stderr'); } return originalErrorWrite(chunk, encoding, callback); }; } formatLogEntry(entry) { const timestamp = new Date(entry.timestamp).toISOString(); const levelPadded = entry.level.toUpperCase().padEnd(5); const source = entry.source ? `[${entry.source}]` : ''; let formatted = `${timestamp} ${levelPadded} ${source} ${entry.message}`; if (entry.data && typeof entry.data === 'object') { formatted += `\nData: ${JSON.stringify(entry.data, null, 2)}`; } return formatted; } async writeLogToFile(entry) { if (!this.logFile) return; const formatted = this.formatLogEntry(entry) + '\n'; try { // Ensure directory exists const dir = path.dirname(this.logFile); await fs.mkdir(dir, { recursive: true }); // Append to file await fs.appendFile(this.logFile, formatted, 'utf-8'); } catch (error) { // Fail silently to avoid infinite loops } } } export function createExecutionLogger(options) { return new ExecutionLogger(options); } //# sourceMappingURL=ExecutionLogger.js.map