UNPKG

@endlessblink/like-i-said-v2

Version:

Task Management & Memory for Claude - Track tasks, remember context, and maintain continuity across sessions with 27 powerful tools. Works with Claude Desktop and Claude Code.

845 lines (715 loc) 23.8 kB
/** * Session Tracker * Monitors work sessions and creates contextual memories based on activity patterns */ import { EventEmitter } from 'events'; import fs from 'fs'; import path from 'path'; export class SessionTracker extends EventEmitter { constructor(storage, options = {}) { super(); this.storage = storage; // Configuration this.config = { sessionTimeout: options.sessionTimeout || 1800000, // 30 minutes minSessionDuration: options.minSessionDuration || 300000, // 5 minutes autoSaveInterval: options.autoSaveInterval || 300000, // 5 minutes maxBufferSize: options.maxBufferSize || 1000, dataPath: options.dataPath || 'data' }; // Session state this.currentSession = null; this.sessionHistory = []; this.activityBuffer = []; // Activity tracking this.lastActivityTime = Date.now(); this.sessionFile = path.join(this.config.dataPath, 'session-history.json'); // Load previous sessions this.loadSessionHistory(); // Start auto-save timer this.autoSaveTimer = setInterval(() => { this.checkSessionTimeout(); this.saveSessionHistory(); }, this.config.autoSaveInterval); } /** * Start a new session */ startSession(metadata = {}) { // End previous session if exists if (this.currentSession) { this.endSession(); } this.currentSession = { id: `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, startTime: Date.now(), metadata, activities: [], errors: [], solutions: [], discoveries: [], files: new Set(), searches: new Map(), tools: new Map(), keyMoments: [], context: { project: metadata.project || 'default', goal: metadata.goal || null, tags: metadata.tags || [] } }; this.emit('session-started', this.currentSession); return this.currentSession.id; } /** * End the current session and create summary */ async endSession(reason = 'manual') { if (!this.currentSession) return null; const duration = Date.now() - this.currentSession.startTime; // Skip very short sessions if (duration < this.config.minSessionDuration && reason !== 'manual') { this.currentSession = null; return null; } // Generate session summary const summary = await this.generateSessionSummary(); // Create session memory if (summary && summary.isSignificant) { const memory = await this.createSessionMemory(summary); this.currentSession.memoryId = memory.id; } // Save to history this.sessionHistory.push({ ...this.currentSession, endTime: Date.now(), duration, summary, reason }); // Emit event this.emit('session-ended', { session: this.currentSession, summary, reason }); this.currentSession = null; this.saveSessionHistory(); return summary; } /** * Track an activity */ trackActivity(type, data) { this.lastActivityTime = Date.now(); // Start session if needed if (!this.currentSession) { this.startSession({ autoStarted: true }); } const activity = { type, timestamp: Date.now(), data: this.sanitizeData(data) }; // Add to current session this.currentSession.activities.push(activity); // Add to buffer for pattern detection this.activityBuffer.push(activity); if (this.activityBuffer.length > this.config.maxBufferSize) { this.activityBuffer.shift(); } // Process based on type switch (type) { case 'search': this.trackSearch(data); break; case 'tool_use': this.trackToolUse(data); break; case 'file_access': this.trackFileAccess(data); break; case 'error': this.trackError(data); break; case 'solution': this.trackSolution(data); break; case 'discovery': this.trackDiscovery(data); break; } // Check for patterns this.detectPatterns(); } /** * Track search activity */ trackSearch(data) { const { query, results, project } = data; const key = query.toLowerCase(); if (!this.currentSession.searches.has(key)) { this.currentSession.searches.set(key, { query, count: 0, firstSeen: Date.now(), results: [] }); } const searchData = this.currentSession.searches.get(key); searchData.count++; searchData.lastSeen = Date.now(); searchData.results.push({ timestamp: Date.now(), resultCount: results }); // Mark as key moment if searched multiple times if (searchData.count === 3) { this.addKeyMoment('repeated_search', { query, count: searchData.count, reason: 'Searched 3+ times - likely important' }); } } /** * Track tool usage */ trackToolUse(data) { const { tool, args, result } = data; if (!this.currentSession.tools.has(tool)) { this.currentSession.tools.set(tool, { count: 0, firstUsed: Date.now(), uses: [] }); } const toolData = this.currentSession.tools.get(tool); toolData.count++; toolData.lastUsed = Date.now(); toolData.uses.push({ timestamp: Date.now(), args: this.sanitizeData(args), success: !result?.error }); } /** * Track file access */ trackFileAccess(data) { const { file, action } = data; this.currentSession.files.add(file); // Detect focus areas based on file patterns if (this.currentSession.files.size === 1) { this.currentSession.context.focusFile = file; } } /** * Track errors */ trackError(data) { const error = { timestamp: Date.now(), ...data, resolved: false }; this.currentSession.errors.push(error); // Mark as key moment for significant errors if (data.severity === 'high' || this.currentSession.errors.length > 5) { this.addKeyMoment('error_spike', { errorCount: this.currentSession.errors.length, latestError: data.message }); } } /** * Track solutions */ trackSolution(data) { const solution = { timestamp: Date.now(), ...data }; this.currentSession.solutions.push(solution); // Link to recent errors const recentErrors = this.currentSession.errors .filter(e => !e.resolved && Date.now() - e.timestamp < 600000); // 10 minutes if (recentErrors.length > 0) { solution.resolvedErrors = recentErrors.map(e => e.message); recentErrors.forEach(e => e.resolved = true); this.addKeyMoment('problem_solved', { solution: data.description, errorsResolved: solution.resolvedErrors }); } } /** * Track discoveries */ trackDiscovery(data) { const discovery = { timestamp: Date.now(), ...data }; this.currentSession.discoveries.push(discovery); this.addKeyMoment('discovery', { description: data.description, impact: data.impact || 'unknown' }); } /** * Add a key moment */ addKeyMoment(type, data) { this.currentSession.keyMoments.push({ type, timestamp: Date.now(), data }); } /** * Detect patterns in activity */ detectPatterns() { // Detect debugging pattern const recentActivities = this.activityBuffer.slice(-20); const errorCount = recentActivities.filter(a => a.type === 'error').length; const searchCount = recentActivities.filter(a => a.type === 'search').length; if (errorCount > 5 && searchCount > 10) { if (!this.currentSession.context.debuggingDetected) { this.currentSession.context.debuggingDetected = true; this.addKeyMoment('debugging_session', { errorCount, searchCount, duration: Date.now() - recentActivities[0].timestamp }); } } // Detect exploration pattern const uniqueFiles = new Set( recentActivities .filter(a => a.type === 'file_access') .map(a => a.data.file) ); if (uniqueFiles.size > 10) { if (!this.currentSession.context.explorationDetected) { this.currentSession.context.explorationDetected = true; this.addKeyMoment('exploration_session', { filesExplored: uniqueFiles.size, purpose: 'Understanding codebase structure' }); } } } /** * Generate session summary */ async generateSessionSummary() { if (!this.currentSession) return null; const duration = Date.now() - this.currentSession.startTime; const activities = this.currentSession.activities; // Calculate metrics const metrics = { duration, totalActivities: activities.length, searches: this.currentSession.searches.size, uniqueSearches: this.currentSession.searches.size, toolsUsed: this.currentSession.tools.size, filesAccessed: this.currentSession.files.size, errorsEncountered: this.currentSession.errors.length, errorsResolved: this.currentSession.errors.filter(e => e.resolved).length, solutionsFound: this.currentSession.solutions.length, discoveries: this.currentSession.discoveries.length, keyMoments: this.currentSession.keyMoments.length }; // Determine session type const sessionType = this.determineSessionType(metrics); // Build narrative const narrative = this.buildSessionNarrative(metrics, sessionType); // Determine significance const isSignificant = this.isSessionSignificant(metrics, sessionType); return { metrics, sessionType, narrative, isSignificant, highlights: this.getSessionHighlights(), recommendations: this.getSessionRecommendations(metrics) }; } /** * Determine session type based on activities */ determineSessionType(metrics) { const types = []; if (metrics.errorsEncountered > 5 && metrics.solutionsFound > 0) { types.push('debugging'); } if (metrics.filesAccessed > 20) { types.push('exploration'); } if (metrics.discoveries > 0) { types.push('research'); } if (metrics.toolsUsed > 5 && metrics.duration > 1800000) { types.push('development'); } if (this.currentSession.context.goal) { types.push('focused'); } return types.length > 0 ? types : ['general']; } /** * Build session narrative */ buildSessionNarrative(metrics, sessionType) { const parts = []; // Opening parts.push(`${this.formatDuration(metrics.duration)} session with ${metrics.totalActivities} activities.`); // Main activities if (sessionType.includes('debugging')) { parts.push(`Debugging session: Encountered ${metrics.errorsEncountered} errors, resolved ${metrics.errorsResolved}.`); } if (sessionType.includes('exploration')) { parts.push(`Explored ${metrics.filesAccessed} files across the codebase.`); } if (metrics.discoveries > 0) { parts.push(`Made ${metrics.discoveries} important discoveries.`); } // Key moments const keyMomentTypes = this.currentSession.keyMoments.map(km => km.type); const uniqueTypes = [...new Set(keyMomentTypes)]; if (uniqueTypes.length > 0) { parts.push(`Key moments: ${uniqueTypes.join(', ')}.`); } return parts.join(' '); } /** * Determine if session is significant */ isSessionSignificant(metrics, sessionType) { // Significant if: // - Has discoveries or solutions // - Long duration with substantial activity // - Debugging session that resolved issues // - Has key moments return ( metrics.discoveries > 0 || metrics.solutionsFound > 0 || (metrics.duration > 3600000 && metrics.totalActivities > 50) || (sessionType.includes('debugging') && metrics.errorsResolved > 0) || metrics.keyMoments > 2 ); } /** * Get session highlights */ getSessionHighlights() { const highlights = []; // Top searches const topSearches = Array.from(this.currentSession.searches.entries()) .sort(([,a], [,b]) => b.count - a.count) .slice(0, 3) .map(([query, data]) => ({ query, count: data.count })); if (topSearches.length > 0) { highlights.push({ type: 'top_searches', data: topSearches }); } // Important discoveries if (this.currentSession.discoveries.length > 0) { highlights.push({ type: 'discoveries', data: this.currentSession.discoveries }); } // Solutions found if (this.currentSession.solutions.length > 0) { highlights.push({ type: 'solutions', data: this.currentSession.solutions }); } // Most used tools const topTools = Array.from(this.currentSession.tools.entries()) .sort(([,a], [,b]) => b.count - a.count) .slice(0, 3) .map(([tool, data]) => ({ tool, count: data.count })); if (topTools.length > 0) { highlights.push({ type: 'tools', data: topTools }); } return highlights; } /** * Get session recommendations */ getSessionRecommendations(metrics) { const recommendations = []; // Unresolved errors const unresolvedErrors = this.currentSession.errors.filter(e => !e.resolved); if (unresolvedErrors.length > 0) { recommendations.push({ type: 'unresolved_errors', priority: 'high', message: `${unresolvedErrors.length} errors remain unresolved`, errors: unresolvedErrors.map(e => e.message) }); } // Repeated searches without results const failedSearches = Array.from(this.currentSession.searches.entries()) .filter(([,data]) => data.results.every(r => r.resultCount === 0)) .filter(([,data]) => data.count >= 2); if (failedSearches.length > 0) { recommendations.push({ type: 'document_searches', priority: 'medium', message: 'Document frequently searched terms', queries: failedSearches.map(([query,]) => query) }); } // Long debugging session if (metrics.duration > 7200000 && metrics.errorsEncountered > 10) { recommendations.push({ type: 'break_suggested', priority: 'low', message: 'Consider taking a break after extended debugging' }); } return recommendations; } /** * Create session memory */ async createSessionMemory(summary) { const content = this.buildSessionMemoryContent(summary); const memory = await this.storage.addMemory({ content, category: 'work', tags: [ 'session-summary', ...summary.sessionType, `duration-${Math.round(summary.metrics.duration / 60000)}min` ], project: this.currentSession.context.project, priority: summary.isSignificant ? 'high' : 'medium', metadata: { sessionId: this.currentSession.id, duration: summary.metrics.duration, sessionType: summary.sessionType, auto_created: true } }); return memory; } /** * Build session memory content */ buildSessionMemoryContent(summary) { let content = `## Session Summary: ${new Date(this.currentSession.startTime).toLocaleString()}\n\n`; if (this.currentSession.context.goal) { content += `**Goal**: ${this.currentSession.context.goal}\n\n`; } content += `### Overview\n${summary.narrative}\n\n`; // Highlights if (summary.highlights.length > 0) { content += `### Highlights\n`; summary.highlights.forEach(highlight => { switch (highlight.type) { case 'discoveries': content += `\n#### Discoveries\n`; highlight.data.forEach(d => { content += `- ${d.description}\n`; }); break; case 'solutions': content += `\n#### Solutions Found\n`; highlight.data.forEach(s => { content += `- ${s.description}\n`; if (s.resolvedErrors) { content += ` - Resolved: ${s.resolvedErrors.join(', ')}\n`; } }); break; case 'top_searches': content += `\n#### Frequent Searches\n`; highlight.data.forEach(s => { content += `- "${s.query}" (${s.count} times)\n`; }); break; case 'tools': content += `\n#### Most Used Tools\n`; highlight.data.forEach(t => { content += `- ${t.tool} (${t.count} times)\n`; }); break; } }); } // Key moments if (this.currentSession.keyMoments.length > 0) { content += `\n### Key Moments\n`; this.currentSession.keyMoments.forEach(moment => { const time = new Date(moment.timestamp).toLocaleTimeString(); content += `- **${time}** - ${this.formatKeyMoment(moment)}\n`; }); } // Metrics content += `\n### Session Metrics\n`; content += `- Duration: ${this.formatDuration(summary.metrics.duration)}\n`; content += `- Files accessed: ${summary.metrics.filesAccessed}\n`; content += `- Searches performed: ${summary.metrics.searches}\n`; content += `- Tools used: ${summary.metrics.toolsUsed}\n`; if (summary.metrics.errorsEncountered > 0) { content += `- Errors: ${summary.metrics.errorsResolved}/${summary.metrics.errorsEncountered} resolved\n`; } // Recommendations if (summary.recommendations.length > 0) { content += `\n### Follow-up Recommendations\n`; summary.recommendations.forEach(rec => { content += `- **${rec.priority}**: ${rec.message}\n`; if (rec.queries) { rec.queries.forEach(q => content += ` - "${q}"\n`); } if (rec.errors) { rec.errors.forEach(e => content += ` - ${e}\n`); } }); } return content; } /** * Helper methods */ checkSessionTimeout() { if (!this.currentSession) return; const timeSinceLastActivity = Date.now() - this.lastActivityTime; if (timeSinceLastActivity > this.config.sessionTimeout) { this.endSession('timeout'); } } formatDuration(ms) { const hours = Math.floor(ms / 3600000); const minutes = Math.floor((ms % 3600000) / 60000); if (hours > 0) { return `${hours}h ${minutes}m`; } return `${minutes} minutes`; } formatKeyMoment(moment) { switch (moment.type) { case 'repeated_search': return `Repeated search for "${moment.data.query}" (${moment.data.count} times)`; case 'error_spike': return `Error spike detected (${moment.data.errorCount} errors)`; case 'problem_solved': return `Problem solved: ${moment.data.solution}`; case 'discovery': return `Discovery: ${moment.data.description}`; case 'debugging_session': return `Debugging session detected (${moment.data.errorCount} errors, ${moment.data.searchCount} searches)`; case 'exploration_session': return `Code exploration (${moment.data.filesExplored} files)`; default: return JSON.stringify(moment.data); } } sanitizeData(data) { if (!data) return data; const sanitized = { ...data }; delete sanitized.password; delete sanitized.token; delete sanitized.secret; delete sanitized.key; return sanitized; } loadSessionHistory() { try { if (fs.existsSync(this.sessionFile)) { this.sessionHistory = JSON.parse(fs.readFileSync(this.sessionFile, 'utf8')); } } catch (error) { console.error('Error loading session history:', error); this.sessionHistory = []; } } saveSessionHistory() { try { if (!fs.existsSync(this.config.dataPath)) { fs.mkdirSync(this.config.dataPath, { recursive: true }); } // Keep only last 100 sessions if (this.sessionHistory.length > 100) { this.sessionHistory = this.sessionHistory.slice(-100); } fs.writeFileSync(this.sessionFile, JSON.stringify(this.sessionHistory, null, 2)); } catch (error) { console.error('Error saving session history:', error); } } /** * Get session insights */ getSessionInsights(days = 7) { const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000); const recentSessions = this.sessionHistory.filter(s => s.startTime > cutoff); const insights = { totalSessions: recentSessions.length, totalDuration: recentSessions.reduce((sum, s) => sum + s.duration, 0), averageDuration: 0, sessionTypes: {}, commonSearches: {}, productivePeriods: [], errorPatterns: {} }; if (recentSessions.length > 0) { insights.averageDuration = insights.totalDuration / recentSessions.length; // Analyze session types recentSessions.forEach(session => { if (session.summary?.sessionType) { session.summary.sessionType.forEach(type => { insights.sessionTypes[type] = (insights.sessionTypes[type] || 0) + 1; }); } }); // Find productive periods const productiveSessions = recentSessions.filter(s => s.summary?.metrics?.solutionsFound > 0 || s.summary?.metrics?.discoveries > 0 ); if (productiveSessions.length > 0) { insights.productivePeriods = this.analyzeProductivePeriods(productiveSessions); } } return insights; } analyzeProductivePeriods(sessions) { const hourCounts = {}; sessions.forEach(session => { const hour = new Date(session.startTime).getHours(); hourCounts[hour] = (hourCounts[hour] || 0) + 1; }); return Object.entries(hourCounts) .sort(([,a], [,b]) => b - a) .slice(0, 3) .map(([hour, count]) => ({ hour: parseInt(hour), count, timeRange: `${hour}:00 - ${(parseInt(hour) + 1) % 24}:00` })); } /** * Cleanup */ destroy() { if (this.autoSaveTimer) { clearInterval(this.autoSaveTimer); } this.endSession('shutdown'); this.saveSessionHistory(); } } export default SessionTracker;