UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

232 lines 8.74 kB
/** * Persistent Storage for Session Registry * * Provides filesystem-based persistence for debugging sessions so they survive * across STDIO server restarts. Critical for STDIO mode where each call * creates a fresh server instance. */ import * as fs from 'fs/promises'; import * as path from 'path'; import { existsSync } from 'fs'; export class PersistentSessionStorage { storageFile; lockFile; version = '1.0.0'; constructor(storageDirectory = '~/.cache/ai-debug-sessions') { // Expand ~ to home directory const expandedDir = storageDirectory.replace('~', process.env.HOME || ''); this.storageFile = path.join(expandedDir, 'sessions.json'); this.lockFile = path.join(expandedDir, 'sessions.lock'); } /** * Load sessions from persistent storage */ async loadSessions() { try { // Ensure storage directory exists await this.ensureStorageDirectory(); // Check for lock file (another process is writing) if (existsSync(this.lockFile)) { const lockAge = Date.now() - (await fs.stat(this.lockFile)).mtimeMs; if (lockAge > 30000) { // Lock older than 30 seconds, assume stale await this.removeLock(); } else { // Wait briefly for lock to be released await new Promise(resolve => setTimeout(resolve, 100)); } } if (!existsSync(this.storageFile)) { return { sessions: new Map(), workingDirToSession: new Map() }; } const data = await fs.readFile(this.storageFile, 'utf8'); const parsed = JSON.parse(data); // Validate version compatibility if (parsed.version !== this.version) { console.warn('⚠️ Session storage version mismatch, starting fresh'); return { sessions: new Map(), workingDirToSession: new Map() }; } // Convert to Maps and restore Date objects const sessions = new Map(); const workingDirToSession = new Map(); Object.entries(parsed.sessions).forEach(([sessionId, sessionData]) => { // Restore Date objects const session = { ...sessionData, createdAt: new Date(sessionData.createdAt), lastActivity: new Date(sessionData.lastActivity) }; // Only load sessions that are still valid (not too old) const age = Date.now() - session.lastActivity.getTime(); if (age < 24 * 60 * 60 * 1000) { // Less than 24 hours old sessions.set(sessionId, session); } }); Object.entries(parsed.workingDirToSession).forEach(([workingDir, sessionId]) => { // Only restore mapping if session still exists if (sessions.has(sessionId)) { workingDirToSession.set(workingDir, sessionId); } }); console.log(`📂 Loaded ${sessions.size} persistent sessions`); return { sessions, workingDirToSession }; } catch (error) { console.warn('⚠️ Failed to load persistent sessions:', error instanceof Error ? error.message : String(error)); return { sessions: new Map(), workingDirToSession: new Map() }; } } /** * Save sessions to persistent storage */ async saveSessions(sessions, workingDirToSession) { try { // Acquire lock await this.acquireLock(); // Convert Maps to objects for JSON serialization const sessionsObj = {}; sessions.forEach((session, sessionId) => { sessionsObj[sessionId] = session; }); const workingDirObj = {}; workingDirToSession.forEach((sessionId, workingDir) => { workingDirObj[workingDir] = sessionId; }); const data = { sessions: sessionsObj, workingDirToSession: workingDirObj, lastUpdated: Date.now(), version: this.version }; // Write atomically (write to temp file, then rename) const tempFile = this.storageFile + '.tmp'; await fs.writeFile(tempFile, JSON.stringify(data, null, 2), 'utf8'); await fs.rename(tempFile, this.storageFile); // Release lock await this.removeLock(); } catch (error) { console.warn('⚠️ Failed to save persistent sessions:', error instanceof Error ? error.message : String(error)); await this.removeLock(); // Ensure lock is released } } /** * Clean up old sessions from storage */ async cleanupOldSessions(maxAgeMs = 7 * 24 * 60 * 60 * 1000) { try { const { sessions, workingDirToSession } = await this.loadSessions(); const now = Date.now(); let cleanedCount = 0; // Remove old sessions sessions.forEach((session, sessionId) => { const age = now - session.lastActivity.getTime(); if (age > maxAgeMs) { sessions.delete(sessionId); cleanedCount++; // Remove from working directory mapping workingDirToSession.forEach((mappedSessionId, workingDir) => { if (mappedSessionId === sessionId) { workingDirToSession.delete(workingDir); } }); } }); if (cleanedCount > 0) { await this.saveSessions(sessions, workingDirToSession); console.log(`🧹 Cleaned up ${cleanedCount} old sessions`); } return cleanedCount; } catch (error) { console.warn('⚠️ Failed to cleanup old sessions:', error instanceof Error ? error.message : String(error)); return 0; } } /** * Get storage statistics */ async getStorageStats() { try { if (!existsSync(this.storageFile)) { return { fileExists: false, fileSizeBytes: 0, sessionCount: 0, lastUpdated: null }; } const stats = await fs.stat(this.storageFile); const data = await fs.readFile(this.storageFile, 'utf8'); const parsed = JSON.parse(data); return { fileExists: true, fileSizeBytes: stats.size, sessionCount: Object.keys(parsed.sessions).length, lastUpdated: new Date(parsed.lastUpdated) }; } catch (error) { return { fileExists: false, fileSizeBytes: 0, sessionCount: 0, lastUpdated: null }; } } /** * Remove all persistent sessions (nuclear option) */ async clearAllSessions() { try { if (existsSync(this.storageFile)) { await fs.unlink(this.storageFile); } if (existsSync(this.lockFile)) { await fs.unlink(this.lockFile); } console.log('🧹 Cleared all persistent sessions'); } catch (error) { console.warn('⚠️ Failed to clear persistent sessions:', error instanceof Error ? error.message : String(error)); } } async ensureStorageDirectory() { const dir = path.dirname(this.storageFile); try { await fs.access(dir); } catch { await fs.mkdir(dir, { recursive: true }); } } async acquireLock() { const lockData = { pid: process.pid, timestamp: Date.now() }; await this.ensureStorageDirectory(); await fs.writeFile(this.lockFile, JSON.stringify(lockData), 'utf8'); } async removeLock() { try { if (existsSync(this.lockFile)) { await fs.unlink(this.lockFile); } } catch { // Ignore errors when removing lock } } } //# sourceMappingURL=persistent-storage.js.map