/** * 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 };