mnemos-coder
Version:
CLI-based coding agent with graph-based execution loop and terminal UI
290 lines • 10.7 kB
JavaScript
/**
* 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