Claude hooks auto-backup: manual (backup_20250720_091250)
This commit is contained in:
parent
9445e09c48
commit
392833187e
135 changed files with 16151 additions and 3439 deletions
579
lib/shadow-learner.js
Normal file
579
lib/shadow-learner.js
Normal file
|
|
@ -0,0 +1,579 @@
|
|||
/**
|
||||
* 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
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue