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
JavaScript
/**
* 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