claude-hooks/hooks/session-finalizer.js

210 lines
6.2 KiB
JavaScript
Executable File

#!/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();