UNPKG

claude-code-collective

Version:

Sub-agent collective framework for Claude Code with TDD validation, TaskMaster Task ID integration, hub-spoke coordination, and deterministic handoffs

425 lines (350 loc) 13.3 kB
const fs = require('fs-extra'); const path = require('path'); class CommandHistoryManager { constructor(historyFile) { this.historyFile = historyFile || path.join(process.cwd(), '.claude-collective', 'command-history.json'); this.history = []; this.maxHistory = 1000; this.sessionHistory = []; this.sessionId = this.generateSessionId(); this.loadHistory(); } generateSessionId() { return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } async loadHistory() { try { if (await fs.pathExists(this.historyFile)) { const data = await fs.readJson(this.historyFile); this.history = Array.isArray(data) ? data : (data.history || []); // History loaded successfully } } catch (error) { console.error('Failed to load command history:', error.message); this.history = []; } } async saveHistory() { try { await fs.ensureDir(path.dirname(this.historyFile)); const historyData = { version: '1.0', lastUpdated: new Date().toISOString(), sessionId: this.sessionId, history: this.history }; await fs.writeJson(this.historyFile, historyData, { spaces: 2 }); } catch (error) { console.error('Failed to save command history:', error.message); } } addCommand(command, result, executionTime = 0) { const entry = { id: this.generateEntryId(), command: command.trim(), result: { success: result.success, namespace: result.namespace, command: result.command || result.action, error: result.error, output: result.output }, metadata: { timestamp: new Date().toISOString(), session: this.sessionId, executionTime, originalInput: result.originalInput, processedInput: result.processedInput, naturalLanguageAttempt: result.naturalLanguageAttempt || false } }; // Add to both histories this.history.unshift(entry); this.sessionHistory.unshift(entry); // Trim history to max size if (this.history.length > this.maxHistory) { this.history = this.history.slice(0, this.maxHistory); } // No automatic save - let callers decide when to persist return entry; } generateEntryId() { return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`; } getHistory(limit = 10, filter = {}) { let filtered = this.history; // Apply filters if (filter.namespace) { filtered = filtered.filter(e => e.result.namespace === filter.namespace); } if (filter.success !== undefined) { filtered = filtered.filter(e => e.result.success === filter.success); } if (filter.session) { filtered = filtered.filter(e => e.metadata.session === filter.session); } if (filter.since) { const since = new Date(filter.since); filtered = filtered.filter(e => new Date(e.metadata.timestamp) > since); } if (filter.command) { const commandQuery = filter.command.toLowerCase(); filtered = filtered.filter(e => e.command.toLowerCase().includes(commandQuery) || (e.result.command && e.result.command.toLowerCase().includes(commandQuery)) ); } return filtered.slice(0, limit); } getSessionHistory(limit = 10) { return this.sessionHistory.slice(0, limit); } searchHistory(query, limit = 10) { const queryLower = query.toLowerCase(); const results = this.history.filter(entry => entry.command.toLowerCase().includes(queryLower) || (entry.result.output && entry.result.output.toLowerCase().includes(queryLower)) || (entry.result.error && entry.result.error.toLowerCase().includes(queryLower)) ); return results.slice(0, limit); } getRecentCommands(limit = 5) { return this.history .slice(0, limit) .map(entry => entry.command); } getPopularCommands(limit = 10) { const commandCounts = new Map(); this.history.forEach(entry => { const cmdKey = entry.result.namespace && entry.result.command ? `/${entry.result.namespace} ${entry.result.command}` : entry.command.split(' ').slice(0, 2).join(' '); commandCounts.set(cmdKey, (commandCounts.get(cmdKey) || 0) + 1); }); return Array.from(commandCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, limit) .map(([command, count]) => ({ command, count })); } getStatistics() { const stats = { total: this.history.length, sessionTotal: this.sessionHistory.length, successful: 0, failed: 0, byNamespace: {}, byCommand: {}, byTimeOfDay: {}, averageExecutionTime: 0, mostUsed: [], recentFailures: [], naturalLanguageUsage: 0 }; let totalExecutionTime = 0; let executionTimeCount = 0; // Calculate statistics this.history.forEach(entry => { // Success/failure if (entry.result.success) { stats.successful++; } else { stats.failed++; } // Natural language usage if (entry.metadata.naturalLanguageAttempt) { stats.naturalLanguageUsage++; } // By namespace const ns = entry.result.namespace; if (ns) { stats.byNamespace[ns] = (stats.byNamespace[ns] || 0) + 1; } // By command const cmd = `${ns}:${entry.result.command}`; if (entry.result.command) { stats.byCommand[cmd] = (stats.byCommand[cmd] || 0) + 1; } // By time of day const hour = new Date(entry.metadata.timestamp).getHours(); stats.byTimeOfDay[hour] = (stats.byTimeOfDay[hour] || 0) + 1; // Execution time if (entry.metadata.executionTime) { totalExecutionTime += entry.metadata.executionTime; executionTimeCount++; } }); // Calculate averages if (executionTimeCount > 0) { stats.averageExecutionTime = Math.round(totalExecutionTime / executionTimeCount); } // Most used commands stats.mostUsed = Object.entries(stats.byCommand) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([cmd, count]) => ({ command: cmd, count })); // Recent failures stats.recentFailures = this.history .filter(e => !e.result.success) .slice(0, 5) .map(e => ({ command: e.command, error: e.result.error, timestamp: e.metadata.timestamp })); // Success rate stats.successRate = stats.total > 0 ? (stats.successful / stats.total) : 0; // Natural language usage percentage stats.naturalLanguageUsageRate = stats.total > 0 ? (stats.naturalLanguageUsage / stats.total) : 0; return stats; } getCommandsByTimeRange(startDate, endDate) { const start = new Date(startDate); const end = new Date(endDate); return this.history.filter(entry => { const entryDate = new Date(entry.metadata.timestamp); return entryDate >= start && entryDate <= end; }); } getFailureAnalysis() { const failures = this.history.filter(e => !e.result.success); const analysis = { total: failures.length, byNamespace: {}, byCommand: {}, byErrorType: {}, commonErrors: [], recentTrends: {} }; failures.forEach(entry => { // By namespace const ns = entry.result.namespace || 'unknown'; analysis.byNamespace[ns] = (analysis.byNamespace[ns] || 0) + 1; // By command const cmd = `${ns}:${entry.result.command || 'unknown'}`; analysis.byCommand[cmd] = (analysis.byCommand[cmd] || 0) + 1; // By error type const errorType = this.categorizeError(entry.result.error); analysis.byErrorType[errorType] = (analysis.byErrorType[errorType] || 0) + 1; }); // Common errors analysis.commonErrors = Object.entries(analysis.byErrorType) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([error, count]) => ({ error, count })); return analysis; } categorizeError(errorMessage) { if (!errorMessage) return 'unknown'; const error = errorMessage.toLowerCase(); if (error.includes('unknown command')) return 'unknown_command'; if (error.includes('invalid command format')) return 'syntax_error'; if (error.includes('required')) return 'missing_argument'; if (error.includes('permission') || error.includes('unauthorized')) return 'permission_error'; if (error.includes('network') || error.includes('connection')) return 'network_error'; if (error.includes('timeout')) return 'timeout_error'; return 'other'; } async clearHistory() { this.history = []; this.sessionHistory = []; return this.saveHistory(); } async clearOldHistory(daysOld = 30) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - daysOld); const originalCount = this.history.length; this.history = this.history.filter(entry => new Date(entry.metadata.timestamp) > cutoffDate ); const removedCount = originalCount - this.history.length; if (removedCount > 0) { await this.saveHistory(); console.log(`Cleared ${removedCount} old commands from history`); } return removedCount; } exportHistory(format = 'json', filter = {}) { const historyToExport = this.getHistory(this.maxHistory, filter); if (format === 'json') { return JSON.stringify({ exported: new Date().toISOString(), total: historyToExport.length, sessionId: this.sessionId, history: historyToExport }, null, 2); } else if (format === 'csv') { const headers = 'Timestamp,Command,Success,Namespace,Command Type,Error,Execution Time,Session\n'; const rows = historyToExport.map(e => `"${e.metadata.timestamp}","${e.command}",${e.result.success},"${e.result.namespace || ''}","${e.result.command || ''}","${e.result.error || ''}",${e.metadata.executionTime || 0},"${e.metadata.session}"` ).join('\n'); return headers + rows; } else if (format === 'markdown') { let md = '# Command History Export\n\n'; md += `**Exported:** ${new Date().toISOString()}\n`; md += `**Total Commands:** ${historyToExport.length}\n`; md += `**Session:** ${this.sessionId}\n\n`; md += '## Commands\n\n'; md += '| Timestamp | Command | Success | Namespace | Error | Execution Time |\n'; md += '|-----------|---------|---------|-----------|-------|----------------|\n'; historyToExport.forEach(e => { const timestamp = new Date(e.metadata.timestamp).toLocaleString(); const success = e.result.success ? '✅' : '❌'; const error = (e.result.error || '').substring(0, 50); const execTime = e.metadata.executionTime || 0; md += `| ${timestamp} | \`${e.command}\` | ${success} | ${e.result.namespace || '-'} | ${error} | ${execTime}ms |\n`; }); return md; } throw new Error(`Unsupported format: ${format}`); } // Real-time command suggestion based on history getCommandSuggestions(partial, limit = 5) { const suggestions = new Set(); // Find commands that start with or contain the partial input this.history.forEach(entry => { const command = entry.command; if (command.toLowerCase().startsWith(partial.toLowerCase()) || command.toLowerCase().includes(partial.toLowerCase())) { suggestions.add(command); } }); return Array.from(suggestions).slice(0, limit); } // Get usage patterns for optimization getUsagePatterns() { const patterns = { peakHours: {}, commandSequences: [], sessionLengths: [], errorPatterns: {} }; // Peak hours analysis this.history.forEach(entry => { const hour = new Date(entry.metadata.timestamp).getHours(); patterns.peakHours[hour] = (patterns.peakHours[hour] || 0) + 1; }); // Command sequences (commands that often follow each other) for (let i = 0; i < this.history.length - 1; i++) { const current = this.history[i]; const next = this.history[i + 1]; // Only if within same session and close in time if (current.metadata.session === next.metadata.session) { const timeDiff = new Date(current.metadata.timestamp) - new Date(next.metadata.timestamp); if (timeDiff < 300000) { // 5 minutes const sequence = `${current.command}${next.command}`; const existing = patterns.commandSequences.find(s => s.sequence === sequence); if (existing) { existing.count++; } else { patterns.commandSequences.push({ sequence, count: 1 }); } } } } // Sort sequences by frequency patterns.commandSequences.sort((a, b) => b.count - a.count); patterns.commandSequences = patterns.commandSequences.slice(0, 10); return patterns; } } module.exports = CommandHistoryManager;