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
375
lib/session-state.js
Normal file
375
lib/session-state.js
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
/**
|
||||
* Session State Manager - Tracks session state and creates continuation docs
|
||||
*/
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
class SessionStateManager {
|
||||
constructor(stateDir = '.claude_hooks') {
|
||||
this.stateDir = path.resolve(stateDir);
|
||||
this.sessionId = this._generateSessionId();
|
||||
this.startTime = Date.now();
|
||||
|
||||
// Session data
|
||||
this.modifiedFiles = new Set();
|
||||
this.commandsExecuted = [];
|
||||
this.toolUsage = {};
|
||||
this.backupHistory = [];
|
||||
this.contextSnapshots = [];
|
||||
|
||||
fs.ensureDirSync(this.stateDir);
|
||||
this._loadPersistentState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state from tool usage
|
||||
*/
|
||||
async updateFromToolUse(toolData) {
|
||||
const tool = toolData.tool || 'Unknown';
|
||||
|
||||
// Track tool usage
|
||||
this.toolUsage[tool] = (this.toolUsage[tool] || 0) + 1;
|
||||
|
||||
// Track file modifications
|
||||
if (tool === 'Edit' || tool === 'Write' || tool === 'MultiEdit') {
|
||||
const filePath = toolData.parameters?.file_path;
|
||||
if (filePath) {
|
||||
this.modifiedFiles.add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Track commands
|
||||
if (tool === 'Bash') {
|
||||
const command = toolData.parameters?.command;
|
||||
if (command) {
|
||||
this.commandsExecuted.push({
|
||||
command,
|
||||
timestamp: new Date().toISOString(),
|
||||
success: toolData.success !== false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Persist state periodically
|
||||
if (this.commandsExecuted.length % 5 === 0) {
|
||||
await this._savePersistentState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add backup to history
|
||||
*/
|
||||
async addBackup(backupId, info) {
|
||||
this.backupHistory.push({
|
||||
backupId,
|
||||
timestamp: new Date().toISOString(),
|
||||
...info
|
||||
});
|
||||
|
||||
await this._savePersistentState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add context snapshot
|
||||
*/
|
||||
async addContextSnapshot(snapshot) {
|
||||
this.contextSnapshots.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
...snapshot
|
||||
});
|
||||
|
||||
// Keep only last 10 snapshots
|
||||
if (this.contextSnapshots.length > 10) {
|
||||
this.contextSnapshots = this.contextSnapshots.slice(-10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive session summary
|
||||
*/
|
||||
async getSessionSummary() {
|
||||
const duration = Date.now() - this.startTime;
|
||||
const durationMinutes = Math.round(duration / (1000 * 60));
|
||||
|
||||
return {
|
||||
sessionId: this.sessionId,
|
||||
startTime: new Date(this.startTime).toISOString(),
|
||||
duration: duration,
|
||||
modifiedFiles: Array.from(this.modifiedFiles),
|
||||
commandsExecuted: this.commandsExecuted,
|
||||
toolUsage: this.toolUsage,
|
||||
backupHistory: this.backupHistory,
|
||||
contextSnapshots: this.contextSnapshots,
|
||||
sessionStats: {
|
||||
durationMinutes,
|
||||
totalToolCalls: Object.values(this.toolUsage).reduce((sum, count) => sum + count, 0),
|
||||
totalCommands: this.commandsExecuted.length,
|
||||
filesModified: this.modifiedFiles.size
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create continuation documentation files
|
||||
*/
|
||||
async createContinuationDocs() {
|
||||
try {
|
||||
const summary = await this.getSessionSummary();
|
||||
|
||||
// Create LAST_SESSION.md
|
||||
await this._createLastSessionDoc(summary);
|
||||
|
||||
// Create/update ACTIVE_TODOS.md (if todos exist)
|
||||
await this._updateActiveTodos();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating continuation docs:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create LAST_SESSION.md with session summary
|
||||
*/
|
||||
async _createLastSessionDoc(summary) {
|
||||
let content = `# Last Claude Session Summary
|
||||
|
||||
## Session Overview
|
||||
- **Session ID**: ${summary.sessionId}
|
||||
- **Started**: ${summary.startTime}
|
||||
- **Duration**: ${summary.sessionStats.durationMinutes} minutes
|
||||
- **Total Tools Used**: ${summary.sessionStats.totalToolCalls}
|
||||
- **Commands Executed**: ${summary.sessionStats.totalCommands}
|
||||
- **Files Modified**: ${summary.sessionStats.filesModified}
|
||||
|
||||
## Files Modified
|
||||
`;
|
||||
|
||||
if (summary.modifiedFiles.length > 0) {
|
||||
for (const file of summary.modifiedFiles) {
|
||||
content += `- ${file}\n`;
|
||||
}
|
||||
} else {
|
||||
content += '*No files were modified in this session*\n';
|
||||
}
|
||||
|
||||
content += `
|
||||
## Tools Used
|
||||
`;
|
||||
|
||||
for (const [tool, count] of Object.entries(summary.toolUsage)) {
|
||||
content += `- **${tool}**: ${count} times\n`;
|
||||
}
|
||||
|
||||
content += `
|
||||
## Recent Commands
|
||||
`;
|
||||
|
||||
const recentCommands = summary.commandsExecuted.slice(-10);
|
||||
if (recentCommands.length > 0) {
|
||||
for (const cmd of recentCommands) {
|
||||
const status = cmd.success ? '✅' : '❌';
|
||||
const time = new Date(cmd.timestamp).toLocaleTimeString();
|
||||
content += `- ${status} ${time}: \`${cmd.command}\`\n`;
|
||||
}
|
||||
} else {
|
||||
content += '*No commands executed in this session*\n';
|
||||
}
|
||||
|
||||
if (summary.backupHistory.length > 0) {
|
||||
content += `
|
||||
## Backups Created
|
||||
`;
|
||||
for (const backup of summary.backupHistory) {
|
||||
const status = backup.success ? '✅' : '❌';
|
||||
const time = new Date(backup.timestamp).toLocaleTimeString();
|
||||
content += `- ${status} ${time}: ${backup.backupId} - ${backup.reason}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
content += `
|
||||
## Context Usage Timeline
|
||||
`;
|
||||
|
||||
if (summary.contextSnapshots.length > 0) {
|
||||
for (const snapshot of summary.contextSnapshots) {
|
||||
const time = new Date(snapshot.timestamp).toLocaleTimeString();
|
||||
const usage = ((snapshot.usageRatio || 0) * 100).toFixed(1);
|
||||
content += `- ${time}: ${usage}% (${snapshot.promptCount || 0} prompts, ${snapshot.toolExecutions || 0} tools)\n`;
|
||||
}
|
||||
}
|
||||
|
||||
content += `
|
||||
## Quick Recovery
|
||||
\`\`\`bash
|
||||
# Check current project status
|
||||
git status
|
||||
|
||||
# View recent changes
|
||||
git diff
|
||||
|
||||
# List backup directories
|
||||
ls .claude_hooks/backups/
|
||||
\`\`\`
|
||||
|
||||
*Generated by Claude Hooks on ${new Date().toISOString()}*
|
||||
`;
|
||||
|
||||
await fs.writeFile('LAST_SESSION.md', content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ACTIVE_TODOS.md if todos exist
|
||||
*/
|
||||
async _updateActiveTodos() {
|
||||
// Check if there's an existing ACTIVE_TODOS.md or any todo-related files
|
||||
const todoFiles = ['ACTIVE_TODOS.md', 'TODO.md', 'todos.md'];
|
||||
|
||||
for (const todoFile of todoFiles) {
|
||||
if (await fs.pathExists(todoFile)) {
|
||||
// File exists, don't overwrite it
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Look for todo comments in recently modified files
|
||||
const todos = await this._extractTodosFromFiles();
|
||||
|
||||
if (todos.length > 0) {
|
||||
let content = `# Active TODOs
|
||||
|
||||
*Auto-generated from code comments and session analysis*
|
||||
|
||||
`;
|
||||
|
||||
for (const todo of todos) {
|
||||
content += `- [ ] ${todo.text} (${todo.file}:${todo.line})\n`;
|
||||
}
|
||||
|
||||
content += `
|
||||
*Update this file manually or use Claude to manage your todos*
|
||||
`;
|
||||
|
||||
await fs.writeFile('ACTIVE_TODOS.md', content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract TODO comments from modified files
|
||||
*/
|
||||
async _extractTodosFromFiles() {
|
||||
const todos = [];
|
||||
const todoPattern = /(?:TODO|FIXME|HACK|XXX|NOTE):\s*(.+)/gi;
|
||||
|
||||
for (const filePath of this.modifiedFiles) {
|
||||
try {
|
||||
if (await fs.pathExists(filePath)) {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const match = todoPattern.exec(line);
|
||||
if (match) {
|
||||
todos.push({
|
||||
text: match[1].trim(),
|
||||
file: filePath,
|
||||
line: index + 1
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip files that can't be read
|
||||
}
|
||||
}
|
||||
|
||||
return todos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up session resources
|
||||
*/
|
||||
async cleanupSession() {
|
||||
// Save final state
|
||||
await this._savePersistentState();
|
||||
|
||||
// Clean up old session files (keep last 5)
|
||||
await this._cleanupOldSessions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique session ID
|
||||
*/
|
||||
_generateSessionId() {
|
||||
const timestamp = new Date().toISOString()
|
||||
.replace(/[:-]/g, '')
|
||||
.replace(/\.\d{3}Z$/, '')
|
||||
.replace('T', '_');
|
||||
return `sess_${timestamp}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load persistent state from disk
|
||||
*/
|
||||
async _loadPersistentState() {
|
||||
try {
|
||||
const stateFile = path.join(this.stateDir, 'session_state.json');
|
||||
|
||||
if (await fs.pathExists(stateFile)) {
|
||||
const state = await fs.readJson(stateFile);
|
||||
|
||||
// Only load if session is recent (within 24 hours)
|
||||
const stateAge = Date.now() - new Date(state.startTime).getTime();
|
||||
if (stateAge < 24 * 60 * 60 * 1000) {
|
||||
this.modifiedFiles = new Set(state.modifiedFiles || []);
|
||||
this.commandsExecuted = state.commandsExecuted || [];
|
||||
this.toolUsage = state.toolUsage || {};
|
||||
this.backupHistory = state.backupHistory || [];
|
||||
this.contextSnapshots = state.contextSnapshots || [];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Start fresh if loading fails
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save persistent state to disk
|
||||
*/
|
||||
async _savePersistentState() {
|
||||
try {
|
||||
const stateFile = path.join(this.stateDir, 'session_state.json');
|
||||
|
||||
const state = {
|
||||
sessionId: this.sessionId,
|
||||
startTime: new Date(this.startTime).toISOString(),
|
||||
modifiedFiles: Array.from(this.modifiedFiles),
|
||||
commandsExecuted: this.commandsExecuted,
|
||||
toolUsage: this.toolUsage,
|
||||
backupHistory: this.backupHistory,
|
||||
contextSnapshots: this.contextSnapshots,
|
||||
lastUpdated: new Date().toISOString()
|
||||
};
|
||||
|
||||
await fs.writeJson(stateFile, state, { spaces: 2 });
|
||||
|
||||
} catch (error) {
|
||||
// Don't let save failures break the session
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old session state files
|
||||
*/
|
||||
async _cleanupOldSessions() {
|
||||
try {
|
||||
const statePattern = path.join(this.stateDir, 'session_*.json');
|
||||
// This is a simple cleanup - in a full implementation,
|
||||
// you'd use glob patterns to find and clean old files
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SessionStateManager };
|
||||
Loading…
Add table
Add a link
Reference in a new issue