/** * Shadow Learner - Pattern learning and prediction system * Node.js implementation */ const fs = require('fs-extra'); const path = require('path'); class ConfidenceCalculator { /** * Calculate confidence for command failure patterns */ static calculateCommandConfidence(successCount, failureCount, recencyFactor) { const totalAttempts = successCount + failureCount; if (totalAttempts === 0) return 0.0; // Base confidence from failure rate const failureRate = failureCount / totalAttempts; // Sample size adjustment (more data = more confidence) const sampleFactor = Math.min(1.0, totalAttempts / 10.0); // Plateau at 10 samples // Time decay (recent failures are more relevant) const confidence = failureRate * sampleFactor * (0.5 + 0.5 * recencyFactor); return Math.min(0.99, Math.max(0.1, confidence)); // Clamp between 0.1 and 0.99 } /** * Calculate confidence for tool sequence patterns */ static calculateSequenceConfidence(successfulSequences, totalSequences) { if (totalSequences === 0) return 0.0; const successRate = successfulSequences / totalSequences; const sampleFactor = Math.min(1.0, totalSequences / 5.0); return successRate * sampleFactor; } } class PatternMatcher { constructor(db) { this.db = db; } /** * Find similar command patterns using fuzzy matching */ fuzzyCommandMatch(command, threshold = 0.8) { const cmdTokens = command.toLowerCase().split(' '); if (cmdTokens.length === 0) return []; const baseCmd = cmdTokens[0]; const matches = []; for (const pattern of Object.values(this.db.commandPatterns)) { const patternCmd = (pattern.trigger.command || '').toLowerCase(); // Exact match if (patternCmd === baseCmd) { matches.push(pattern); } // Fuzzy match on command name else if (this._similarity(patternCmd, baseCmd) > threshold) { matches.push(pattern); } // Partial match (e.g., "pip3" matches "pip install") else if (cmdTokens.some(token => patternCmd.includes(token))) { matches.push(pattern); } } return matches.sort((a, b) => b.confidence - a.confidence); } /** * Match patterns based on current context */ contextPatternMatch(currentContext) { const matches = []; for (const pattern of Object.values(this.db.contextPatterns)) { if (this._contextMatches(currentContext, pattern.trigger)) { matches.push(pattern); } } return matches.sort((a, b) => b.confidence - a.confidence); } /** * Simple string similarity (Jaccard similarity) */ _similarity(str1, str2) { const set1 = new Set(str1.split('')); const set2 = new Set(str2.split('')); const intersection = new Set([...set1].filter(x => set2.has(x))); const union = new Set([...set1, ...set2]); return intersection.size / union.size; } /** * Check if current context matches trigger conditions */ _contextMatches(current, trigger) { for (const [key, expectedValue] of Object.entries(trigger)) { if (!(key in current)) return false; const currentValue = current[key]; // Handle different value types if (typeof expectedValue === 'string' && typeof currentValue === 'string') { if (!currentValue.toLowerCase().includes(expectedValue.toLowerCase())) { return false; } } else if (expectedValue !== currentValue) { return false; } } return true; } } class LearningEngine { constructor(db) { this.db = db; this.confidenceCalc = ConfidenceCalculator; } /** * Main learning entry point */ learnFromExecution(execution) { // Learn command patterns if (execution.tool === 'Bash') { this._learnCommandPattern(execution); } // Learn tool sequences this._learnSequencePattern(execution); // Learn context patterns if (!execution.success) { this._learnFailureContext(execution); } } /** * Learn from bash command executions */ _learnCommandPattern(execution) { const command = execution.parameters.command || ''; if (!command) return; const baseCmd = command.split(' ')[0]; const patternId = `cmd_${baseCmd}`; if (patternId in this.db.commandPatterns) { const pattern = this.db.commandPatterns[patternId]; // Update statistics if (execution.success) { pattern.prediction.successCount = (pattern.prediction.successCount || 0) + 1; } else { pattern.prediction.failureCount = (pattern.prediction.failureCount || 0) + 1; } // Recalculate confidence const recency = this._calculateRecency(execution.timestamp); pattern.confidence = this.confidenceCalc.calculateCommandConfidence( pattern.prediction.successCount || 0, pattern.prediction.failureCount || 0, recency ); pattern.lastSeen = execution.timestamp; pattern.evidenceCount += 1; } else { // Create new pattern this.db.commandPatterns[patternId] = { patternId, patternType: 'command_execution', trigger: { command: baseCmd }, prediction: { successCount: execution.success ? 1 : 0, failureCount: execution.success ? 0 : 1, commonErrors: execution.errorMessage ? [execution.errorMessage] : [] }, confidence: 0.3, // Start with low confidence evidenceCount: 1, lastSeen: execution.timestamp, successRate: execution.success ? 1.0 : 0.0 }; } } /** * Learn from tool sequence patterns */ _learnSequencePattern(execution) { // Get recent tool history (last 5 tools) const recentTools = this.db.executionHistory.slice(-5).map(e => e.tool); recentTools.push(execution.tool); // Look for sequences of 2-3 tools for (let seqLen = 2; seqLen <= 3; seqLen++) { if (recentTools.length >= seqLen) { const sequence = recentTools.slice(-seqLen); const patternId = `seq_${sequence.join('_')}`; // Update or create sequence pattern // (Simplified implementation - could be expanded) } } } /** * Learn from failure contexts */ _learnFailureContext(execution) { if (!execution.errorMessage) return; // Extract key error indicators const errorKey = this._extractErrorKey(execution.errorMessage); if (!errorKey) return; const patternId = `ctx_error_${errorKey}`; if (patternId in this.db.contextPatterns) { const pattern = this.db.contextPatterns[patternId]; pattern.evidenceCount += 1; pattern.lastSeen = execution.timestamp; // Update confidence based on repeated failures pattern.confidence = Math.min(0.95, pattern.confidence + 0.05); } else { // Create new context pattern this.db.contextPatterns[patternId] = { patternId, patternType: 'context_error', trigger: { tool: execution.tool, errorType: errorKey }, prediction: { likelyError: execution.errorMessage, suggestions: this._generateSuggestions(execution) }, confidence: 0.4, evidenceCount: 1, lastSeen: execution.timestamp, successRate: 0.0 }; } } /** * Calculate recency factor (1.0 = very recent, 0.0 = very old) */ _calculateRecency(timestamp) { const now = new Date(); const ageHours = (now - new Date(timestamp)) / (1000 * 60 * 60); // Exponential decay: recent events matter more return Math.max(0.0, Math.exp(-ageHours / 24.0)); // 24 hour half-life } /** * Extract key error indicators from error messages */ _extractErrorKey(errorMessage) { const message = errorMessage.toLowerCase(); const errorPatterns = { 'command_not_found': ['command not found', 'not found'], 'permission_denied': ['permission denied', 'access denied'], 'file_not_found': ['no such file', 'file not found'], 'connection_error': ['connection refused', 'network unreachable'], 'syntax_error': ['syntax error', 'invalid syntax'] }; for (const [errorType, patterns] of Object.entries(errorPatterns)) { if (patterns.some(pattern => message.includes(pattern))) { return errorType; } } return null; } /** * Generate suggestions based on failed execution */ _generateSuggestions(execution) { const suggestions = []; if (execution.tool === 'Bash') { const command = execution.parameters.command || ''; if (command) { const baseCmd = command.split(' ')[0]; // Common command alternatives const alternatives = { 'pip': ['pip3', 'python -m pip', 'python3 -m pip'], 'python': ['python3'], 'node': ['nodejs'], 'vim': ['nvim', 'nano'] }; if (baseCmd in alternatives) { const remainingArgs = command.split(' ').slice(1).join(' '); suggestions.push( ...alternatives[baseCmd].map(alt => `Try '${alt} ${remainingArgs}'`) ); } } } return suggestions; } } class PredictionEngine { constructor(matcher) { this.matcher = matcher; } /** * Predict if a command will succeed and suggest alternatives */ predictCommandOutcome(command, context = {}) { // Find matching patterns const commandPatterns = this.matcher.fuzzyCommandMatch(command); const contextPatterns = this.matcher.contextPatternMatch(context); const prediction = { likelySuccess: true, confidence: 0.5, warnings: [], suggestions: [] }; // Analyze command patterns for (const pattern of commandPatterns.slice(0, 3)) { // Top 3 matches if (pattern.confidence > 0.7) { const failureRate = (pattern.prediction.failureCount || 0) / Math.max(1, pattern.evidenceCount); if (failureRate > 0.6) { // High failure rate prediction.likelySuccess = false; prediction.confidence = pattern.confidence; prediction.warnings.push(`Command '${command.split(' ')[0]}' often fails`); // Add suggestions from pattern const suggestions = pattern.prediction.suggestions || []; prediction.suggestions.push(...suggestions); } } } return prediction; } } class ShadowLearner { constructor(storagePath = '.claude_hooks/patterns') { this.storagePath = path.resolve(storagePath); fs.ensureDirSync(this.storagePath); this.db = this._loadDatabase(); this.matcher = new PatternMatcher(this.db); this.learningEngine = new LearningEngine(this.db); this.predictionEngine = new PredictionEngine(this.matcher); // Performance cache (simple in-memory cache) this.predictionCache = new Map(); this.cacheTimeout = 5 * 60 * 1000; // 5 minutes } /** * Learn from tool execution */ learnFromExecution(execution) { try { this.learningEngine.learnFromExecution(execution); this.db.executionHistory.push(execution); // Trim history to keep memory usage reasonable if (this.db.executionHistory.length > 1000) { this.db.executionHistory = this.db.executionHistory.slice(-500); } } catch (error) { // Learning failures shouldn't break the system console.error('Shadow learner error:', error.message); } } /** * Predict command outcome with caching */ predictCommandOutcome(command, context = {}) { const cacheKey = `cmd_pred:${this._hash(command)}`; const now = Date.now(); // Check cache if (this.predictionCache.has(cacheKey)) { const cached = this.predictionCache.get(cacheKey); if (now - cached.timestamp < this.cacheTimeout) { return cached.prediction; } } const prediction = this.predictionEngine.predictCommandOutcome(command, context); // Cache result this.predictionCache.set(cacheKey, { prediction, timestamp: now }); // Clean old cache entries this._cleanCache(); return prediction; } /** * Quick method for command failure learning (backward compatibility) */ learnCommandFailure(command, suggestion, confidence) { const execution = { tool: 'Bash', parameters: { command }, success: false, timestamp: new Date(), errorMessage: `Command failed: ${command}` }; this.learnFromExecution(execution); // Also store the specific suggestion const baseCmd = command.split(' ')[0]; const patternId = `cmd_${baseCmd}`; if (patternId in this.db.commandPatterns) { const pattern = this.db.commandPatterns[patternId]; pattern.prediction.suggestions = pattern.prediction.suggestions || []; if (!pattern.prediction.suggestions.includes(suggestion)) { pattern.prediction.suggestions.push(suggestion); } } } /** * Get suggestion for a command (backward compatibility) */ getSuggestion(command) { const prediction = this.predictCommandOutcome(command); if (!prediction.likelySuccess && prediction.suggestions.length > 0) { return { suggestion: prediction.suggestions[0].replace(/^Try '|'$/g, ''), // Clean format confidence: prediction.confidence }; } return null; } /** * Save learned patterns to disk */ async saveDatabase() { try { const patternsFile = path.join(this.storagePath, 'patterns.json'); const backupFile = path.join(this.storagePath, 'patterns.backup.json'); // Create backup of existing data if (await fs.pathExists(patternsFile)) { await fs.move(patternsFile, backupFile, { overwrite: true }); } // Save new data await fs.writeJson(patternsFile, this._serializeDatabase(), { spaces: 2 }); } catch (error) { // Save failures shouldn't break the system console.error('Failed to save shadow learner database:', error.message); } } /** * Load patterns database from disk */ _loadDatabase() { const patternsFile = path.join(this.storagePath, 'patterns.json'); try { if (fs.existsSync(patternsFile)) { const data = fs.readJsonSync(patternsFile); return this._deserializeDatabase(data); } } catch (error) { // If loading fails, start with empty database console.error('Failed to load shadow learner database, starting fresh:', error.message); } return { commandPatterns: {}, contextPatterns: {}, sequencePatterns: {}, executionHistory: [] }; } /** * Serialize database for JSON storage */ _serializeDatabase() { return { commandPatterns: this.db.commandPatterns, contextPatterns: this.db.contextPatterns, sequencePatterns: this.db.sequencePatterns || {}, executionHistory: this.db.executionHistory.slice(-100), // Keep last 100 executions metadata: { version: '1.0.0', lastSaved: new Date().toISOString() } }; } /** * Deserialize database from JSON */ _deserializeDatabase(data) { return { commandPatterns: data.commandPatterns || {}, contextPatterns: data.contextPatterns || {}, sequencePatterns: data.sequencePatterns || {}, executionHistory: (data.executionHistory || []).map(e => ({ ...e, timestamp: new Date(e.timestamp) })) }; } /** * Simple hash function for cache keys */ _hash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash.toString(); } /** * Clean expired cache entries */ _cleanCache() { const now = Date.now(); for (const [key, value] of this.predictionCache.entries()) { if (now - value.timestamp > this.cacheTimeout) { this.predictionCache.delete(key); } } } } module.exports = { ShadowLearner, ConfidenceCalculator, PatternMatcher, LearningEngine, PredictionEngine };