@calmhive/calmhive-cli
Version:
🐝 Intelligent Claude CLI wrapper with rule injection, AFk background processing, voice interface, and adaptive retry
354 lines (306 loc) • 10.1 kB
JavaScript
const fs = require('fs').promises;
const path = require('path');
class TUILogParser {
constructor() {
this.iterationPattern = /🚀 Starting iteration (\d+) of (\d+)/;
this.exitCodePattern = /⚠️ Iteration (\d+) exited with code (\d+)/;
this.contextPattern = /🔄 Starting iteration (\d+) with fresh context/;
this.limitPattern = /⏳ Usage limit likely hit\. Waiting (\d+) minutes/;
this.stopPattern = /🛑 Session ([\w-]+) was stopped/;
this.errorPatterns = [
/Error:/,
/Failed:/,
/FATAL:/,
/TypeError:/,
/ReferenceError:/,
/SyntaxError:/
];
}
/**
* Parse log content into structured iterations with summaries
*/
parseLogContent(logContent) {
const lines = logContent.split('\n').map(line => line.trim()).filter(Boolean);
const iterations = [];
let currentIteration = null;
let sessionMetadata = this.extractSessionMetadata(lines);
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const cleanLine = this.stripLogPrefix(line);
// Detect iteration start
const iterationMatch = cleanLine.match(this.iterationPattern);
if (iterationMatch) {
// Save previous iteration
if (currentIteration) {
currentIteration.summary = this.generateIterationSummary(currentIteration);
iterations.push(currentIteration);
}
// Start new iteration
currentIteration = {
number: parseInt(iterationMatch[1]),
total: parseInt(iterationMatch[2]),
startTime: this.extractTimestamp(line),
logs: [cleanLine],
status: 'running',
events: [],
errors: [],
warnings: []
};
continue;
}
// If we have a current iteration, add content to it
if (currentIteration) {
currentIteration.logs.push(cleanLine);
// Track specific events
this.categorizeLogLine(cleanLine, currentIteration);
}
}
// Handle final iteration
if (currentIteration) {
currentIteration.summary = this.generateIterationSummary(currentIteration);
iterations.push(currentIteration);
}
return {
sessionMetadata,
iterations,
totalIterations: iterations.length,
finalStatus: this.determineFinalStatus(iterations, lines)
};
}
/**
* Extract session metadata from initial log lines
*/
extractSessionMetadata(lines) {
const metadata = {};
for (const line of lines.slice(0, 10)) {
if (line.includes('AFk worker started for session')) {
metadata.sessionId = line.match(/session ([\w-]+)/)?.[1];
}
if (line.includes('Task:')) {
metadata.taskDescription = line.split('Task:')[1]?.trim();
}
if (line.includes('Working directory:')) {
metadata.workingDirectory = line.split('Working directory:')[1]?.trim();
}
}
return metadata;
}
/**
* Categorize log lines into events, errors, warnings
*/
categorizeLogLine(line, iteration) {
// Exit codes
const exitMatch = line.match(this.exitCodePattern);
if (exitMatch) {
const exitCode = parseInt(exitMatch[2]);
iteration.events.push({
type: 'exit_code',
code: exitCode,
message: line
});
if (exitCode !== 0) {
iteration.status = 'failed';
iteration.errors.push(`Exit code ${exitCode}`);
}
return;
}
// Context refresh
if (line.match(this.contextPattern)) {
iteration.events.push({
type: 'context_refresh',
message: line
});
return;
}
// Usage limits
const limitMatch = line.match(this.limitPattern);
if (limitMatch) {
iteration.events.push({
type: 'rate_limit',
waitTime: parseInt(limitMatch[1]),
message: line
});
iteration.warnings.push(`Rate limited - waiting ${limitMatch[1]} minutes`);
return;
}
// Stop signal
if (line.match(this.stopPattern)) {
iteration.events.push({
type: 'stopped',
message: line
});
iteration.status = 'stopped';
return;
}
// Errors
for (const errorPattern of this.errorPatterns) {
if (line.match(errorPattern)) {
iteration.errors.push(line);
iteration.status = 'error';
return;
}
}
}
/**
* Generate a beautiful summary for each iteration
*/
generateIterationSummary(iteration) {
const summary = {
status: iteration.status,
duration: this.calculateDuration(iteration),
eventCount: iteration.events.length,
errorCount: iteration.errors.length,
warningCount: iteration.warnings.length,
keyEvents: this.extractKeyEvents(iteration),
statusEmoji: this.getStatusEmoji(iteration),
progressBar: this.generateProgressBar(iteration.number, iteration.total)
};
return summary;
}
/**
* Extract the most important events from an iteration
*/
extractKeyEvents(iteration) {
const keyEvents = [];
// Add significant events
for (const event of iteration.events) {
if (event.type === 'exit_code' && event.code !== 0) {
keyEvents.push(`Failed with exit code ${event.code}`);
} else if (event.type === 'context_refresh') {
keyEvents.push('Fresh context restart');
} else if (event.type === 'rate_limit') {
keyEvents.push(`Rate limited (${event.waitTime}m wait)`);
} else if (event.type === 'stopped') {
keyEvents.push('Manually stopped');
}
}
// Add error summaries
if (iteration.errors.length > 0) {
keyEvents.push(`${iteration.errors.length} error(s)`);
}
return keyEvents.slice(0, 3); // Limit to most important
}
/**
* Get emoji for iteration status
*/
getStatusEmoji(iteration) {
switch (iteration.status) {
case 'running': return '🔄';
case 'failed': return '❌';
case 'error': return '💥';
case 'stopped': return '🛑';
case 'completed': return '✅';
default: return '🚀';
}
}
/**
* Generate ASCII progress bar
*/
generateProgressBar(current, total, width = 20) {
const percentage = current / total;
const filled = Math.floor(percentage * width);
const empty = width - filled;
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${current}/${total}`;
}
/**
* Calculate iteration duration (placeholder - would need timestamps)
*/
calculateDuration(iteration) {
// In future, parse timestamps to calculate actual duration
return 'unknown';
}
/**
* Strip log prefixes to get clean content
*/
stripLogPrefix(line) {
// Remove timestamp and arrow prefix: "[2025-07-03T18:42:07.825Z]" or " 1→"
return line.replace(/^\s*\d+→/, '').replace(/^\[[\d-T:.Z]+\]\s*/, '').trim();
}
/**
* Extract timestamp from log line
*/
extractTimestamp(line) {
const match = line.match(/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\]/);
return match ? new Date(match[1]) : null;
}
/**
* Determine final session status
*/
determineFinalStatus(iterations, allLines) {
if (iterations.length === 0) {return 'no_iterations';}
const lastIteration = iterations[iterations.length - 1];
// Check for completion signals
const hasCompletionSignal = allLines.some(line =>
line.includes('AFk worker completed successfully') ||
line.includes('Session completed')
);
if (hasCompletionSignal) {return 'completed';}
if (lastIteration.status === 'stopped') {return 'stopped';}
if (lastIteration.status === 'error') {return 'error';}
if (iterations.some(iter => iter.status === 'failed')) {return 'failed';}
return 'running';
}
/**
* Format parsed log for beautiful TUI display
*/
formatForTUI(parsedLog) {
const { sessionMetadata, iterations, finalStatus } = parsedLog;
const output = [];
// Session header
output.push('');
output.push(`{bold}📋 Session: {/bold}{cyan-fg}${sessionMetadata.sessionId || 'unknown'}{/cyan-fg}`);
output.push(`{bold}📝 Task: {/bold}${this.truncateTask(sessionMetadata.taskDescription)}`);
output.push(`{bold}📊 Status: {/bold}${this.formatStatus(finalStatus)}`);
output.push('');
// Iteration summaries
for (const iteration of iterations) {
output.push(this.formatIterationSummary(iteration));
output.push('');
}
return output.join('\n');
}
/**
* Format individual iteration summary
*/
formatIterationSummary(iteration) {
const { summary } = iteration;
const lines = [];
// Header with progress
lines.push(`{bold}${summary.statusEmoji} Iteration ${iteration.number}{/bold} ${summary.progressBar}`);
// Key events (if any)
if (summary.keyEvents.length > 0) {
lines.push(` {gray-fg}${summary.keyEvents.join(' • ')}{/gray-fg}`);
}
// Error/warning summary
if (summary.errorCount > 0 || summary.warningCount > 0) {
const indicators = [];
if (summary.errorCount > 0) {indicators.push(`{red-fg}${summary.errorCount} errors{/red-fg}`);}
if (summary.warningCount > 0) {indicators.push(`{yellow-fg}${summary.warningCount} warnings{/yellow-fg}`);}
lines.push(` ${indicators.join(', ')}`);
}
return lines.join('\n');
}
/**
* Format status with color
*/
formatStatus(status) {
const statusMap = {
'completed': '{green-fg}✅ Completed{/green-fg}',
'stopped': '{yellow-fg}🛑 Stopped{/yellow-fg}',
'error': '{red-fg}💥 Error{/red-fg}',
'failed': '{red-fg}❌ Failed{/red-fg}',
'running': '{blue-fg}🔄 Running{/blue-fg}',
'no_iterations': '{gray-fg}📭 No iterations{/gray-fg}'
};
return statusMap[status] || status;
}
/**
* Truncate long task descriptions
*/
truncateTask(task) {
if (!task) {return 'No description';}
if (task.length <= 80) {return task;}
return task.substring(0, 77) + '...';
}
}
module.exports = TUILogParser;