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
222
hooks/command-validator.js
Executable file
222
hooks/command-validator.js
Executable file
|
|
@ -0,0 +1,222 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Command Validator Hook - PreToolUse[Bash] hook
|
||||
* Validates bash commands using shadow learner insights
|
||||
*/
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
// Add lib directory to require path
|
||||
const libPath = path.join(__dirname, '..', 'lib');
|
||||
const { ShadowLearner } = require(path.join(libPath, 'shadow-learner'));
|
||||
|
||||
class CommandValidator {
|
||||
constructor() {
|
||||
this.shadowLearner = new ShadowLearner();
|
||||
|
||||
// Dangerous command patterns
|
||||
this.dangerousPatterns = [
|
||||
/rm\s+-rf\s+\//, // Delete root
|
||||
/mkfs\./, // Format filesystem
|
||||
/dd\s+if=.*of=\/dev\//, // Overwrite devices
|
||||
/:\(\){ :\|:& };:/, // Fork bomb
|
||||
/curl.*\|\s*bash/, // Pipe to shell
|
||||
/wget.*\|\s*sh/, // Pipe to shell
|
||||
/;.*rm\s+-rf/, // Command chaining with rm
|
||||
/&&.*rm\s+-rf/, // Command chaining with rm
|
||||
];
|
||||
|
||||
this.suspiciousPatterns = [
|
||||
/sudo\s+rm/, // Sudo with rm
|
||||
/chmod\s+777/, // Overly permissive
|
||||
/\/etc\/passwd/, // System files
|
||||
/\/etc\/shadow/, // System files
|
||||
/nc.*-l.*-p/, // Netcat listener
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Comprehensive command safety validation
|
||||
*/
|
||||
validateCommandSafety(command) {
|
||||
// Normalize command for analysis
|
||||
const normalized = command.toLowerCase().trim();
|
||||
|
||||
// Check for dangerous patterns
|
||||
for (const pattern of this.dangerousPatterns) {
|
||||
if (pattern.test(normalized)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Dangerous command pattern detected',
|
||||
severity: 'critical'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for suspicious patterns
|
||||
for (const pattern of this.suspiciousPatterns) {
|
||||
if (pattern.test(normalized)) {
|
||||
return {
|
||||
allowed: true, // Allow but warn
|
||||
reason: 'Suspicious command pattern detected',
|
||||
severity: 'warning'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true, reason: 'Command appears safe' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Use shadow learner to predict command success
|
||||
*/
|
||||
validateWithShadowLearner(command) {
|
||||
try {
|
||||
const prediction = this.shadowLearner.predictCommandOutcome(command);
|
||||
|
||||
if (!prediction.likelySuccess && prediction.confidence > 0.8) {
|
||||
const suggestions = prediction.suggestions || [];
|
||||
const suggestionText = suggestions.length > 0 ? ` Try: ${suggestions[0]}` : '';
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Command likely to fail (confidence: ${Math.round(prediction.confidence * 100)}%)${suggestionText}`,
|
||||
severity: 'medium',
|
||||
suggestions
|
||||
};
|
||||
} else if (prediction.warnings && prediction.warnings.length > 0) {
|
||||
return {
|
||||
allowed: true,
|
||||
reason: prediction.warnings[0],
|
||||
severity: 'warning',
|
||||
suggestions: prediction.suggestions || []
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// If shadow learner fails, don't block
|
||||
}
|
||||
|
||||
return { allowed: true, reason: 'No issues detected' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main validation entry point
|
||||
*/
|
||||
validateCommand(command) {
|
||||
// Safety validation (blocking)
|
||||
const safetyResult = this.validateCommandSafety(command);
|
||||
if (!safetyResult.allowed) {
|
||||
return safetyResult;
|
||||
}
|
||||
|
||||
// Shadow learner validation (predictive)
|
||||
const predictionResult = this.validateWithShadowLearner(command);
|
||||
|
||||
// Return most significant result
|
||||
if (['high', 'critical'].includes(predictionResult.severity)) {
|
||||
return predictionResult;
|
||||
} else if (['warning', 'medium'].includes(safetyResult.severity)) {
|
||||
return safetyResult;
|
||||
} else {
|
||||
return predictionResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
let inputData = '';
|
||||
|
||||
// Handle stdin input
|
||||
if (process.stdin.isTTY) {
|
||||
// If called directly for testing
|
||||
inputData = JSON.stringify({
|
||||
tool: 'Bash',
|
||||
parameters: { command: 'pip install requests' }
|
||||
});
|
||||
} else {
|
||||
// Read from stdin
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
for await (const chunk of process.stdin) {
|
||||
inputData += chunk;
|
||||
}
|
||||
}
|
||||
|
||||
const input = JSON.parse(inputData);
|
||||
|
||||
// Extract command from parameters
|
||||
const tool = input.tool || '';
|
||||
const parameters = input.parameters || {};
|
||||
const command = parameters.command || '';
|
||||
|
||||
if (tool !== 'Bash' || !command) {
|
||||
// Not a bash command, allow it
|
||||
const response = { allow: true, message: 'Not a bash command' };
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Validate command
|
||||
const validator = new CommandValidator();
|
||||
const result = validator.validateCommand(command);
|
||||
|
||||
if (!result.allowed) {
|
||||
// Block the command
|
||||
const response = {
|
||||
allow: false,
|
||||
message: `⛔ Command blocked: ${result.reason}`
|
||||
};
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(1); // Exit code 1 = block operation
|
||||
}
|
||||
|
||||
else if (['warning', 'medium'].includes(result.severity)) {
|
||||
// Allow with warning
|
||||
const warningEmoji = result.severity === 'warning' ? '⚠️' : '🚨';
|
||||
let message = `${warningEmoji} ${result.reason}`;
|
||||
|
||||
if (result.suggestions && result.suggestions.length > 0) {
|
||||
message += `\n💡 Suggestion: ${result.suggestions[0]}`;
|
||||
}
|
||||
|
||||
const response = {
|
||||
allow: true,
|
||||
message
|
||||
};
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
else {
|
||||
// Allow without warning
|
||||
const response = { allow: true, message: 'Command validated' };
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Never block on validation errors - always allow operation
|
||||
const response = {
|
||||
allow: true,
|
||||
message: `Validation error: ${error.message}`
|
||||
};
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', (error) => {
|
||||
const response = {
|
||||
allow: true,
|
||||
message: `Validation error: ${error.message}`
|
||||
};
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
main();
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Command Validator Hook - PreToolUse[Bash] hook
|
||||
Validates bash commands using shadow learner insights
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add lib directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
||||
|
||||
from shadow_learner import ShadowLearner
|
||||
from models import ValidationResult
|
||||
|
||||
|
||||
class CommandValidator:
|
||||
"""Validates bash commands for safety and success probability"""
|
||||
|
||||
def __init__(self):
|
||||
self.shadow_learner = ShadowLearner()
|
||||
|
||||
# Dangerous command patterns
|
||||
self.dangerous_patterns = [
|
||||
r'rm\s+-rf\s+/', # Delete root
|
||||
r'mkfs\.', # Format filesystem
|
||||
r'dd\s+if=.*of=/dev/', # Overwrite devices
|
||||
r':(){ :|:& };:', # Fork bomb
|
||||
r'curl.*\|\s*bash', # Pipe to shell
|
||||
r'wget.*\|\s*sh', # Pipe to shell
|
||||
r';.*rm\s+-rf', # Command chaining with rm
|
||||
r'&&.*rm\s+-rf', # Command chaining with rm
|
||||
]
|
||||
|
||||
self.suspicious_patterns = [
|
||||
r'sudo\s+rm', # Sudo with rm
|
||||
r'chmod\s+777', # Overly permissive
|
||||
r'/etc/passwd', # System files
|
||||
r'/etc/shadow', # System files
|
||||
r'nc.*-l.*-p', # Netcat listener
|
||||
]
|
||||
|
||||
def validate_command_safety(self, command: str) -> ValidationResult:
|
||||
"""Comprehensive command safety validation"""
|
||||
|
||||
# Normalize command for analysis
|
||||
normalized = command.lower().strip()
|
||||
|
||||
# Check for dangerous patterns
|
||||
for pattern in self.dangerous_patterns:
|
||||
if re.search(pattern, normalized):
|
||||
return ValidationResult(
|
||||
allowed=False,
|
||||
reason=f"Dangerous command pattern detected",
|
||||
severity="critical"
|
||||
)
|
||||
|
||||
# Check for suspicious patterns
|
||||
for pattern in self.suspicious_patterns:
|
||||
if re.search(pattern, normalized):
|
||||
return ValidationResult(
|
||||
allowed=True, # Allow but warn
|
||||
reason=f"Suspicious command pattern detected",
|
||||
severity="warning"
|
||||
)
|
||||
|
||||
return ValidationResult(allowed=True, reason="Command appears safe")
|
||||
|
||||
def validate_with_shadow_learner(self, command: str) -> ValidationResult:
|
||||
"""Use shadow learner to predict command success"""
|
||||
|
||||
try:
|
||||
prediction = self.shadow_learner.predict_command_outcome(command)
|
||||
|
||||
if not prediction["likely_success"] and prediction["confidence"] > 0.8:
|
||||
suggestions = prediction.get("suggestions", [])
|
||||
suggestion_text = f" Try: {suggestions[0]}" if suggestions else ""
|
||||
|
||||
return ValidationResult(
|
||||
allowed=False,
|
||||
reason=f"Command likely to fail (confidence: {prediction['confidence']:.0%}){suggestion_text}",
|
||||
severity="medium",
|
||||
suggestions=suggestions
|
||||
)
|
||||
elif prediction["warnings"]:
|
||||
return ValidationResult(
|
||||
allowed=True,
|
||||
reason=prediction["warnings"][0],
|
||||
severity="warning",
|
||||
suggestions=prediction.get("suggestions", [])
|
||||
)
|
||||
|
||||
except Exception:
|
||||
# If shadow learner fails, don't block
|
||||
pass
|
||||
|
||||
return ValidationResult(allowed=True, reason="No issues detected")
|
||||
|
||||
def validate_command(self, command: str) -> ValidationResult:
|
||||
"""Main validation entry point"""
|
||||
|
||||
# Safety validation (blocking)
|
||||
safety_result = self.validate_command_safety(command)
|
||||
if not safety_result.allowed:
|
||||
return safety_result
|
||||
|
||||
# Shadow learner validation (predictive)
|
||||
prediction_result = self.validate_with_shadow_learner(command)
|
||||
|
||||
# Return most significant result
|
||||
if prediction_result.severity in ["high", "critical"]:
|
||||
return prediction_result
|
||||
elif safety_result.severity in ["warning", "medium"]:
|
||||
return safety_result
|
||||
else:
|
||||
return prediction_result
|
||||
|
||||
|
||||
def main():
|
||||
"""Main hook entry point"""
|
||||
try:
|
||||
# Read input from Claude Code
|
||||
input_data = json.loads(sys.stdin.read())
|
||||
|
||||
# Extract command from parameters
|
||||
tool = input_data.get("tool", "")
|
||||
parameters = input_data.get("parameters", {})
|
||||
command = parameters.get("command", "")
|
||||
|
||||
if tool != "Bash" or not command:
|
||||
# Not a bash command, allow it
|
||||
response = {"allow": True, "message": "Not a bash command"}
|
||||
print(json.dumps(response))
|
||||
sys.exit(0)
|
||||
|
||||
# Validate command
|
||||
validator = CommandValidator()
|
||||
result = validator.validate_command(command)
|
||||
|
||||
if not result.allowed:
|
||||
# Block the command
|
||||
response = {
|
||||
"allow": False,
|
||||
"message": f"⛔ Command blocked: {result.reason}"
|
||||
}
|
||||
print(json.dumps(response))
|
||||
sys.exit(1) # Exit code 1 = block operation
|
||||
|
||||
elif result.severity in ["warning", "medium"]:
|
||||
# Allow with warning
|
||||
warning_emoji = "⚠️" if result.severity == "warning" else "🚨"
|
||||
message = f"{warning_emoji} {result.reason}"
|
||||
|
||||
if result.suggestions:
|
||||
message += f"\n💡 Suggestion: {result.suggestions[0]}"
|
||||
|
||||
response = {
|
||||
"allow": True,
|
||||
"message": message
|
||||
}
|
||||
print(json.dumps(response))
|
||||
sys.exit(0)
|
||||
|
||||
else:
|
||||
# Allow without warning
|
||||
response = {"allow": True, "message": "Command validated"}
|
||||
print(json.dumps(response))
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
# Never block on validation errors - always allow operation
|
||||
response = {
|
||||
"allow": True,
|
||||
"message": f"Validation error: {str(e)}"
|
||||
}
|
||||
print(json.dumps(response))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
109
hooks/context-monitor.js
Executable file
109
hooks/context-monitor.js
Executable file
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Context Monitor Hook - UserPromptSubmit hook
|
||||
* Monitors context usage and triggers backups when needed
|
||||
*/
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
// Add lib directory to require path
|
||||
const libPath = path.join(__dirname, '..', 'lib');
|
||||
|
||||
const { ContextMonitor } = require(path.join(libPath, 'context-monitor'));
|
||||
const { BackupManager } = require(path.join(libPath, 'backup-manager'));
|
||||
const { SessionStateManager } = require(path.join(libPath, 'session-state'));
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Read input from Claude Code
|
||||
let inputData = '';
|
||||
|
||||
// Handle stdin input
|
||||
if (process.stdin.isTTY) {
|
||||
// If called directly for testing
|
||||
inputData = JSON.stringify({ prompt: 'test prompt', context_size: 1000 });
|
||||
} else {
|
||||
// Read from stdin
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
for await (const chunk of process.stdin) {
|
||||
inputData += chunk;
|
||||
}
|
||||
}
|
||||
|
||||
const input = JSON.parse(inputData);
|
||||
|
||||
// Initialize components
|
||||
const contextMonitor = new ContextMonitor();
|
||||
const backupManager = new BackupManager();
|
||||
const sessionManager = new SessionStateManager();
|
||||
|
||||
// Update context estimates from prompt
|
||||
contextMonitor.updateFromPrompt(input);
|
||||
|
||||
// Check if backup should be triggered
|
||||
const backupDecision = contextMonitor.checkBackupTriggers('UserPromptSubmit', input);
|
||||
|
||||
let message;
|
||||
|
||||
if (backupDecision.shouldBackup) {
|
||||
// Execute backup
|
||||
const sessionState = await sessionManager.getSessionSummary();
|
||||
const backupResult = await backupManager.executeBackup(backupDecision, sessionState);
|
||||
|
||||
// Record backup in session
|
||||
await sessionManager.addBackup(backupResult.backupId, {
|
||||
reason: backupDecision.reason,
|
||||
success: backupResult.success
|
||||
});
|
||||
|
||||
// Add context snapshot
|
||||
await sessionManager.addContextSnapshot({
|
||||
usageRatio: contextMonitor.getContextUsageRatio(),
|
||||
promptCount: contextMonitor.promptCount,
|
||||
toolExecutions: contextMonitor.toolExecutions
|
||||
});
|
||||
|
||||
// Notify about backup
|
||||
if (backupResult.success) {
|
||||
message = `Auto-backup created: ${backupDecision.reason} (usage: ${(contextMonitor.getContextUsageRatio() * 100).toFixed(1)}%)`;
|
||||
} else {
|
||||
message = `Backup attempted but failed: ${backupResult.error}`;
|
||||
}
|
||||
} else {
|
||||
message = `Context usage: ${(contextMonitor.getContextUsageRatio() * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
// Always allow operation (this is a monitoring hook)
|
||||
const response = {
|
||||
allow: true,
|
||||
message
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
// Never block on errors - always allow operation
|
||||
const response = {
|
||||
allow: true,
|
||||
message: `Context monitor error: ${error.message}`
|
||||
};
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', (error) => {
|
||||
const response = {
|
||||
allow: true,
|
||||
message: `Context monitor error: ${error.message}`
|
||||
};
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
main();
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Context Monitor Hook - UserPromptSubmit hook
|
||||
Monitors context usage and triggers backups when needed
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add lib directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
||||
|
||||
from context_monitor import ContextMonitor
|
||||
from backup_manager import BackupManager
|
||||
from session_state import SessionStateManager
|
||||
|
||||
|
||||
def main():
|
||||
"""Main hook entry point"""
|
||||
try:
|
||||
# Read input from Claude Code
|
||||
input_data = json.loads(sys.stdin.read())
|
||||
|
||||
# Initialize components
|
||||
context_monitor = ContextMonitor()
|
||||
backup_manager = BackupManager()
|
||||
session_manager = SessionStateManager()
|
||||
|
||||
# Update context estimates from prompt
|
||||
context_monitor.update_from_prompt(input_data)
|
||||
|
||||
# Check if backup should be triggered
|
||||
backup_decision = context_monitor.check_backup_triggers("UserPromptSubmit", input_data)
|
||||
|
||||
if backup_decision.should_backup:
|
||||
# Execute backup
|
||||
session_state = session_manager.get_session_summary()
|
||||
backup_result = backup_manager.execute_backup(backup_decision, session_state)
|
||||
|
||||
# Record backup in session
|
||||
session_manager.add_backup(backup_result.backup_id, {
|
||||
"reason": backup_decision.reason,
|
||||
"success": backup_result.success
|
||||
})
|
||||
|
||||
# Add context snapshot
|
||||
session_manager.add_context_snapshot({
|
||||
"usage_ratio": context_monitor.get_context_usage_ratio(),
|
||||
"prompt_count": context_monitor.prompt_count,
|
||||
"tool_executions": context_monitor.tool_executions
|
||||
})
|
||||
|
||||
# Notify about backup
|
||||
if backup_result.success:
|
||||
message = f"Auto-backup created: {backup_decision.reason} (usage: {context_monitor.get_context_usage_ratio():.1%})"
|
||||
else:
|
||||
message = f"Backup attempted but failed: {backup_result.error}"
|
||||
else:
|
||||
message = f"Context usage: {context_monitor.get_context_usage_ratio():.1%}"
|
||||
|
||||
# Always allow operation (this is a monitoring hook)
|
||||
response = {
|
||||
"allow": True,
|
||||
"message": message
|
||||
}
|
||||
|
||||
print(json.dumps(response))
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
# Never block on errors - always allow operation
|
||||
response = {
|
||||
"allow": True,
|
||||
"message": f"Context monitor error: {str(e)}"
|
||||
}
|
||||
print(json.dumps(response))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
210
hooks/session-finalizer.js
Executable file
210
hooks/session-finalizer.js
Executable file
|
|
@ -0,0 +1,210 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Session Finalizer Hook - Stop hook
|
||||
* Finalizes session, creates documentation, and saves state
|
||||
*/
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
// Add lib directory to require path
|
||||
const libPath = path.join(__dirname, '..', 'lib');
|
||||
const { SessionStateManager } = require(path.join(libPath, 'session-state'));
|
||||
const { ShadowLearner } = require(path.join(libPath, 'shadow-learner'));
|
||||
const { ContextMonitor } = require(path.join(libPath, 'context-monitor'));
|
||||
|
||||
async function createRecoveryInfo(sessionSummary, contextMonitor) {
|
||||
/**
|
||||
* Create recovery information if needed
|
||||
*/
|
||||
try {
|
||||
const contextUsage = contextMonitor.getContextUsageRatio();
|
||||
|
||||
// If context was high when session ended, create recovery guide
|
||||
if (contextUsage > 0.8) {
|
||||
let recoveryContent = `# Session Recovery Information
|
||||
|
||||
## Context Status
|
||||
- **Context Usage**: ${(contextUsage * 100).toFixed(1)}% when session ended
|
||||
- **Reason**: Session ended with high context usage
|
||||
|
||||
## What This Means
|
||||
Your Claude session ended while using a significant amount of context. This could mean:
|
||||
1. You were working on a complex task
|
||||
2. Context limits were approaching
|
||||
3. Session was interrupted
|
||||
|
||||
## Recovery Steps
|
||||
|
||||
### 1. Check Your Progress
|
||||
Review these recently modified files:
|
||||
`;
|
||||
|
||||
for (const filePath of sessionSummary.modifiedFiles || sessionSummary.modified_files || []) {
|
||||
recoveryContent += `- ${filePath}\n`;
|
||||
}
|
||||
|
||||
recoveryContent += `
|
||||
### 2. Review Last Actions
|
||||
Recent commands executed:
|
||||
`;
|
||||
|
||||
const recentCommands = (sessionSummary.commandsExecuted || sessionSummary.commands_executed || []).slice(-5);
|
||||
for (const cmdInfo of recentCommands) {
|
||||
recoveryContent += `- \`${cmdInfo.command}\`\n`;
|
||||
}
|
||||
|
||||
recoveryContent += `
|
||||
### 3. Continue Your Work
|
||||
1. Check \`ACTIVE_TODOS.md\` for pending tasks
|
||||
2. Review \`LAST_SESSION.md\` for complete session history
|
||||
3. Use \`git status\` to see current file changes
|
||||
4. Consider committing your progress: \`git add -A && git commit -m "Work in progress"\`
|
||||
|
||||
### 4. Available Backups
|
||||
`;
|
||||
|
||||
for (const backup of sessionSummary.backupHistory || sessionSummary.backup_history || []) {
|
||||
const status = backup.success ? '✅' : '❌';
|
||||
recoveryContent += `- ${status} ${backup.backup_id || backup.backupId} - ${backup.reason}\n`;
|
||||
}
|
||||
|
||||
recoveryContent += `
|
||||
## Quick Recovery Commands
|
||||
\`\`\`bash
|
||||
# Check current status
|
||||
git status
|
||||
|
||||
# View recent changes
|
||||
git diff
|
||||
|
||||
# List available backups
|
||||
ls .claude_hooks/backups/
|
||||
|
||||
# View active todos
|
||||
cat ACTIVE_TODOS.md
|
||||
|
||||
# View last session summary
|
||||
cat LAST_SESSION.md
|
||||
\`\`\`
|
||||
|
||||
*This recovery guide was created because your session ended with ${(contextUsage * 100).toFixed(1)}% context usage.*
|
||||
`;
|
||||
|
||||
await fs.writeFile('RECOVERY_GUIDE.md', recoveryContent);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Don't let recovery guide creation break session finalization
|
||||
}
|
||||
}
|
||||
|
||||
async function logSessionCompletion(sessionSummary) {
|
||||
/**
|
||||
* Log session completion for analysis
|
||||
*/
|
||||
try {
|
||||
const logDir = path.join('.claude_hooks', 'logs');
|
||||
await fs.ensureDir(logDir);
|
||||
|
||||
const completionLog = {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'session_completion',
|
||||
session_id: sessionSummary.sessionId || sessionSummary.session_id || 'unknown',
|
||||
duration_minutes: (sessionSummary.sessionStats || sessionSummary.session_stats || {}).duration_minutes || 0,
|
||||
total_tools: (sessionSummary.sessionStats || sessionSummary.session_stats || {}).total_tool_calls || 0,
|
||||
files_modified: (sessionSummary.modifiedFiles || sessionSummary.modified_files || []).length,
|
||||
commands_executed: (sessionSummary.sessionStats || sessionSummary.session_stats || {}).total_commands || 0,
|
||||
backups_created: (sessionSummary.backupHistory || sessionSummary.backup_history || []).length
|
||||
};
|
||||
|
||||
const logFile = path.join(logDir, 'session_completions.jsonl');
|
||||
|
||||
await fs.appendFile(logFile, JSON.stringify(completionLog) + '\n');
|
||||
|
||||
} catch (error) {
|
||||
// Don't let logging errors break finalization
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
let inputData = {};
|
||||
|
||||
// Handle stdin input
|
||||
if (!process.stdin.isTTY) {
|
||||
try {
|
||||
let input = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
for await (const chunk of process.stdin) {
|
||||
input += chunk;
|
||||
}
|
||||
|
||||
if (input.trim()) {
|
||||
inputData = JSON.parse(input);
|
||||
}
|
||||
} catch (error) {
|
||||
// If input parsing fails, use empty object
|
||||
inputData = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize components
|
||||
const sessionManager = new SessionStateManager();
|
||||
const shadowLearner = new ShadowLearner();
|
||||
const contextMonitor = new ContextMonitor();
|
||||
|
||||
// Create session documentation
|
||||
await sessionManager.createContinuationDocs();
|
||||
|
||||
// Save all learned patterns
|
||||
await shadowLearner.saveDatabase();
|
||||
|
||||
// Get session summary for logging
|
||||
const sessionSummary = await sessionManager.getSessionSummary();
|
||||
|
||||
// Create recovery guide if session was interrupted
|
||||
await createRecoveryInfo(sessionSummary, contextMonitor);
|
||||
|
||||
// Clean up session
|
||||
await sessionManager.cleanupSession();
|
||||
|
||||
// Log session completion
|
||||
await logSessionCompletion(sessionSummary);
|
||||
|
||||
const modifiedFiles = sessionSummary.modifiedFiles || sessionSummary.modified_files || [];
|
||||
const totalTools = (sessionSummary.sessionStats || sessionSummary.session_stats || {}).total_tool_calls || 0;
|
||||
|
||||
// Always allow - this is a cleanup hook
|
||||
const response = {
|
||||
allow: true,
|
||||
message: `Session finalized. Modified ${modifiedFiles.length} files, used ${totalTools} tools.`
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
// Session finalization should never block
|
||||
const response = {
|
||||
allow: true,
|
||||
message: `Session finalization error: ${error.message}`
|
||||
};
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', (error) => {
|
||||
const response = {
|
||||
allow: true,
|
||||
message: `Session finalization error: ${error.message}`
|
||||
};
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
main();
|
||||
159
hooks/session-logger.js
Executable file
159
hooks/session-logger.js
Executable file
|
|
@ -0,0 +1,159 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Session Logger Hook - PostToolUse[*] hook
|
||||
* Logs all tool usage and feeds data to shadow learner
|
||||
*/
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
// Add lib directory to require path
|
||||
const libPath = path.join(__dirname, '..', 'lib');
|
||||
const { ShadowLearner } = require(path.join(libPath, 'shadow-learner'));
|
||||
const { SessionStateManager } = require(path.join(libPath, 'session-state'));
|
||||
const { ContextMonitor } = require(path.join(libPath, 'context-monitor'));
|
||||
|
||||
async function logExecution(execution) {
|
||||
/**
|
||||
* Log execution to file for debugging and analysis
|
||||
*/
|
||||
try {
|
||||
const logDir = path.join('.claude_hooks', 'logs');
|
||||
await fs.ensureDir(logDir);
|
||||
|
||||
// Create daily log file
|
||||
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
const logFile = path.join(logDir, `executions_${date}.jsonl`);
|
||||
|
||||
// Append execution record
|
||||
await fs.appendFile(logFile, JSON.stringify(execution) + '\n');
|
||||
|
||||
// Clean up old log files (keep last 7 days)
|
||||
await cleanupOldLogs(logDir);
|
||||
|
||||
} catch (error) {
|
||||
// Don't let logging errors break the hook
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupOldLogs(logDir) {
|
||||
/**
|
||||
* Clean up log files older than 7 days
|
||||
*/
|
||||
try {
|
||||
const cutoffTime = Date.now() - (7 * 24 * 60 * 60 * 1000); // 7 days ago
|
||||
|
||||
const files = await fs.readdir(logDir);
|
||||
const logFiles = files.filter(file => file.match(/^executions_\d{8}\.jsonl$/));
|
||||
|
||||
for (const logFile of logFiles) {
|
||||
const filePath = path.join(logDir, logFile);
|
||||
const stats = await fs.stat(filePath);
|
||||
|
||||
if (stats.mtime.getTime() < cutoffTime) {
|
||||
await fs.unlink(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
let inputData = '';
|
||||
|
||||
// Handle stdin input
|
||||
if (process.stdin.isTTY) {
|
||||
// If called directly for testing
|
||||
inputData = JSON.stringify({
|
||||
tool: 'Bash',
|
||||
parameters: { command: 'echo test' },
|
||||
success: true,
|
||||
execution_time: 0.1
|
||||
});
|
||||
} else {
|
||||
// Read from stdin
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
for await (const chunk of process.stdin) {
|
||||
inputData += chunk;
|
||||
}
|
||||
}
|
||||
|
||||
const input = JSON.parse(inputData);
|
||||
|
||||
// Extract tool execution data
|
||||
const tool = input.tool || '';
|
||||
const parameters = input.parameters || {};
|
||||
const success = input.success !== undefined ? input.success : true;
|
||||
const error = input.error || '';
|
||||
const executionTime = input.execution_time || 0.0;
|
||||
|
||||
// Create tool execution record
|
||||
const execution = {
|
||||
timestamp: new Date(),
|
||||
tool,
|
||||
parameters,
|
||||
success,
|
||||
errorMessage: error || null,
|
||||
executionTime,
|
||||
context: {}
|
||||
};
|
||||
|
||||
// Initialize components
|
||||
const shadowLearner = new ShadowLearner();
|
||||
const sessionManager = new SessionStateManager();
|
||||
const contextMonitor = new ContextMonitor();
|
||||
|
||||
// Feed execution to shadow learner
|
||||
shadowLearner.learnFromExecution(execution);
|
||||
|
||||
// Update session state
|
||||
await sessionManager.updateFromToolUse(input);
|
||||
|
||||
// Update context monitor
|
||||
contextMonitor.updateFromToolUse(input);
|
||||
|
||||
// Save learned patterns periodically
|
||||
// (Only save every 10 executions to avoid too much disk I/O)
|
||||
if (contextMonitor.toolExecutions % 10 === 0) {
|
||||
await shadowLearner.saveDatabase();
|
||||
}
|
||||
|
||||
// Log execution to file for debugging (optional)
|
||||
await logExecution(execution);
|
||||
|
||||
// Always allow - this is a post-execution hook
|
||||
const response = {
|
||||
allow: true,
|
||||
message: `Logged ${tool} execution`
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
// Post-execution hooks should never block
|
||||
const response = {
|
||||
allow: true,
|
||||
message: `Logging error: ${error.message}`
|
||||
};
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', (error) => {
|
||||
const response = {
|
||||
allow: true,
|
||||
message: `Logging error: ${error.message}`
|
||||
};
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
main();
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Session Finalizer Hook - Stop hook
|
||||
Finalizes session, creates documentation, and saves state
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add lib directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
||||
|
||||
from session_state import SessionStateManager
|
||||
from shadow_learner import ShadowLearner
|
||||
from context_monitor import ContextMonitor
|
||||
|
||||
|
||||
def main():
|
||||
"""Main hook entry point"""
|
||||
try:
|
||||
# Read input from Claude Code (if any)
|
||||
try:
|
||||
input_data = json.loads(sys.stdin.read())
|
||||
except:
|
||||
input_data = {}
|
||||
|
||||
# Initialize components
|
||||
session_manager = SessionStateManager()
|
||||
shadow_learner = ShadowLearner()
|
||||
context_monitor = ContextMonitor()
|
||||
|
||||
# Create session documentation
|
||||
session_manager.create_continuation_docs()
|
||||
|
||||
# Save all learned patterns
|
||||
shadow_learner.save_database()
|
||||
|
||||
# Get session summary for logging
|
||||
session_summary = session_manager.get_session_summary()
|
||||
|
||||
# Create recovery guide if session was interrupted
|
||||
create_recovery_info(session_summary, context_monitor)
|
||||
|
||||
# Clean up session
|
||||
session_manager.cleanup_session()
|
||||
|
||||
# Log session completion
|
||||
log_session_completion(session_summary)
|
||||
|
||||
# Always allow - this is a cleanup hook
|
||||
response = {
|
||||
"allow": True,
|
||||
"message": f"Session finalized. Modified {len(session_summary.get('modified_files', []))} files, used {session_summary.get('session_stats', {}).get('total_tool_calls', 0)} tools."
|
||||
}
|
||||
|
||||
print(json.dumps(response))
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
# Session finalization should never block
|
||||
response = {
|
||||
"allow": True,
|
||||
"message": f"Session finalization error: {str(e)}"
|
||||
}
|
||||
print(json.dumps(response))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def create_recovery_info(session_summary: dict, context_monitor: ContextMonitor):
|
||||
"""Create recovery information if needed"""
|
||||
try:
|
||||
context_usage = context_monitor.get_context_usage_ratio()
|
||||
|
||||
# If context was high when session ended, create recovery guide
|
||||
if context_usage > 0.8:
|
||||
recovery_content = f"""# Session Recovery Information
|
||||
|
||||
## Context Status
|
||||
- **Context Usage**: {context_usage:.1%} when session ended
|
||||
- **Reason**: Session ended with high context usage
|
||||
|
||||
## What This Means
|
||||
Your Claude session ended while using a significant amount of context. This could mean:
|
||||
1. You were working on a complex task
|
||||
2. Context limits were approaching
|
||||
3. Session was interrupted
|
||||
|
||||
## Recovery Steps
|
||||
|
||||
### 1. Check Your Progress
|
||||
Review these recently modified files:
|
||||
"""
|
||||
|
||||
for file_path in session_summary.get('modified_files', []):
|
||||
recovery_content += f"- {file_path}\n"
|
||||
|
||||
recovery_content += f"""
|
||||
### 2. Review Last Actions
|
||||
Recent commands executed:
|
||||
"""
|
||||
|
||||
recent_commands = session_summary.get('commands_executed', [])[-5:]
|
||||
for cmd_info in recent_commands:
|
||||
recovery_content += f"- `{cmd_info['command']}`\n"
|
||||
|
||||
recovery_content += f"""
|
||||
### 3. Continue Your Work
|
||||
1. Check `ACTIVE_TODOS.md` for pending tasks
|
||||
2. Review `LAST_SESSION.md` for complete session history
|
||||
3. Use `git status` to see current file changes
|
||||
4. Consider committing your progress: `git add -A && git commit -m "Work in progress"`
|
||||
|
||||
### 4. Available Backups
|
||||
"""
|
||||
|
||||
for backup in session_summary.get('backup_history', []):
|
||||
status = "✅" if backup['success'] else "❌"
|
||||
recovery_content += f"- {status} {backup['backup_id']} - {backup['reason']}\n"
|
||||
|
||||
recovery_content += f"""
|
||||
## Quick Recovery Commands
|
||||
```bash
|
||||
# Check current status
|
||||
git status
|
||||
|
||||
# View recent changes
|
||||
git diff
|
||||
|
||||
# List available backups
|
||||
ls .claude_hooks/backups/
|
||||
|
||||
# View active todos
|
||||
cat ACTIVE_TODOS.md
|
||||
|
||||
# View last session summary
|
||||
cat LAST_SESSION.md
|
||||
```
|
||||
|
||||
*This recovery guide was created because your session ended with {context_usage:.1%} context usage.*
|
||||
"""
|
||||
|
||||
with open("RECOVERY_GUIDE.md", 'w') as f:
|
||||
f.write(recovery_content)
|
||||
|
||||
except Exception:
|
||||
pass # Don't let recovery guide creation break session finalization
|
||||
|
||||
|
||||
def log_session_completion(session_summary: dict):
|
||||
"""Log session completion for analysis"""
|
||||
try:
|
||||
log_dir = Path(".claude_hooks/logs")
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
completion_log = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"type": "session_completion",
|
||||
"session_id": session_summary.get("session_id", "unknown"),
|
||||
"duration_minutes": session_summary.get("session_stats", {}).get("duration_minutes", 0),
|
||||
"total_tools": session_summary.get("session_stats", {}).get("total_tool_calls", 0),
|
||||
"files_modified": len(session_summary.get("modified_files", [])),
|
||||
"commands_executed": session_summary.get("session_stats", {}).get("total_commands", 0),
|
||||
"backups_created": len(session_summary.get("backup_history", []))
|
||||
}
|
||||
|
||||
log_file = log_dir / "session_completions.jsonl"
|
||||
|
||||
with open(log_file, 'a') as f:
|
||||
f.write(json.dumps(completion_log) + "\n")
|
||||
|
||||
except Exception:
|
||||
pass # Don't let logging errors break finalization
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Session Logger Hook - PostToolUse[*] hook
|
||||
Logs all tool usage and feeds data to shadow learner
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Add lib directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
||||
|
||||
from shadow_learner import ShadowLearner
|
||||
from session_state import SessionStateManager
|
||||
from context_monitor import ContextMonitor
|
||||
from models import ToolExecution
|
||||
|
||||
|
||||
def main():
|
||||
"""Main hook entry point"""
|
||||
try:
|
||||
# Read input from Claude Code
|
||||
input_data = json.loads(sys.stdin.read())
|
||||
|
||||
# Extract tool execution data
|
||||
tool = input_data.get("tool", "")
|
||||
parameters = input_data.get("parameters", {})
|
||||
success = input_data.get("success", True)
|
||||
error = input_data.get("error", "")
|
||||
execution_time = input_data.get("execution_time", 0.0)
|
||||
|
||||
# Create tool execution record
|
||||
execution = ToolExecution(
|
||||
timestamp=datetime.now(),
|
||||
tool=tool,
|
||||
parameters=parameters,
|
||||
success=success,
|
||||
error_message=error if error else None,
|
||||
execution_time=execution_time,
|
||||
context={}
|
||||
)
|
||||
|
||||
# Initialize components
|
||||
shadow_learner = ShadowLearner()
|
||||
session_manager = SessionStateManager()
|
||||
context_monitor = ContextMonitor()
|
||||
|
||||
# Feed execution to shadow learner
|
||||
shadow_learner.learn_from_execution(execution)
|
||||
|
||||
# Update session state
|
||||
session_manager.update_from_tool_use(input_data)
|
||||
|
||||
# Update context monitor
|
||||
context_monitor.update_from_tool_use(input_data)
|
||||
|
||||
# Save learned patterns periodically
|
||||
# (Only save every 10 executions to avoid too much disk I/O)
|
||||
if context_monitor.tool_executions % 10 == 0:
|
||||
shadow_learner.save_database()
|
||||
|
||||
# Log execution to file for debugging (optional)
|
||||
log_execution(execution)
|
||||
|
||||
# Always allow - this is a post-execution hook
|
||||
response = {
|
||||
"allow": True,
|
||||
"message": f"Logged {tool} execution"
|
||||
}
|
||||
|
||||
print(json.dumps(response))
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
# Post-execution hooks should never block
|
||||
response = {
|
||||
"allow": True,
|
||||
"message": f"Logging error: {str(e)}"
|
||||
}
|
||||
print(json.dumps(response))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def log_execution(execution: ToolExecution):
|
||||
"""Log execution to file for debugging and analysis"""
|
||||
try:
|
||||
log_dir = Path(".claude_hooks/logs")
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create daily log file
|
||||
log_file = log_dir / f"executions_{datetime.now().strftime('%Y%m%d')}.jsonl"
|
||||
|
||||
# Append execution record
|
||||
with open(log_file, 'a') as f:
|
||||
f.write(json.dumps(execution.to_dict()) + "\n")
|
||||
|
||||
# Clean up old log files (keep last 7 days)
|
||||
cleanup_old_logs(log_dir)
|
||||
|
||||
except Exception:
|
||||
# Don't let logging errors break the hook
|
||||
pass
|
||||
|
||||
|
||||
def cleanup_old_logs(log_dir: Path):
|
||||
"""Clean up log files older than 7 days"""
|
||||
try:
|
||||
import time
|
||||
|
||||
cutoff_time = time.time() - (7 * 24 * 3600) # 7 days ago
|
||||
|
||||
for log_file in log_dir.glob("executions_*.jsonl"):
|
||||
if log_file.stat().st_mtime < cutoff_time:
|
||||
log_file.unlink()
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue