UNPKG

bc-code-intelligence-mcp

Version:

BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows

229 lines โ€ข 9.31 kB
/** * File-Based Session Storage * * Provides persistent session storage using JSON files in a configurable directory. * Supports both local user directories and shared team/company directories. */ import { promises as fs } from 'fs'; import { join } from 'path'; export class FileSessionStorage { config; sessionDirectory; initialized = false; constructor(config) { this.config = config || { type: 'file' }; // Determine storage directory this.sessionDirectory = this.getSessionDirectory(); } getSessionDirectory() { const baseDir = this.config.config?.directory; if (baseDir) { // Use configured directory (could be shared team/company location) return baseDir; } // Default to user-local directory const userHome = process.env.HOME || process.env.USERPROFILE || '.'; return join(userHome, '.bc-code-intel', 'sessions'); } async ensureInitialized() { if (this.initialized) return; try { // Create sessions directory if it doesn't exist await fs.mkdir(this.sessionDirectory, { recursive: true }); // Create .gitignore to exclude sessions from version control const gitignorePath = join(this.sessionDirectory, '.gitignore'); try { await fs.access(gitignorePath); } catch { await fs.writeFile(gitignorePath, '# Session files - exclude from version control\n*.json\n'); } this.initialized = true; console.error(`๐Ÿ“ Session storage initialized: ${this.sessionDirectory}`); } catch (error) { throw new Error(`Failed to initialize file session storage: ${error instanceof Error ? error.message : 'Unknown error'}`); } } getSessionFilePath(sessionId) { return join(this.sessionDirectory, `${sessionId}.json`); } async writeSessionFile(session) { const filePath = this.getSessionFilePath(session.sessionId); // Create safe serializable version of session const serializable = { ...session, startTime: session.startTime.toISOString(), lastActivity: session.lastActivity.toISOString(), messages: session.messages.map(msg => ({ ...msg, timestamp: msg.timestamp.toISOString() })) }; await fs.writeFile(filePath, JSON.stringify(serializable, null, 2), 'utf8'); } async readSessionFile(sessionId) { try { const filePath = this.getSessionFilePath(sessionId); const content = await fs.readFile(filePath, 'utf8'); const data = JSON.parse(content); // Convert ISO strings back to Date objects return { ...data, startTime: new Date(data.startTime), lastActivity: new Date(data.lastActivity), messages: data.messages.map((msg) => ({ ...msg, timestamp: new Date(msg.timestamp) })) }; } catch (error) { if (error.code === 'ENOENT') { return null; // File doesn't exist } throw error; } } // SessionStorage interface implementation async createSession(session) { await this.ensureInitialized(); await this.writeSessionFile(session); // Auto-cleanup if enabled if (this.config.retention?.autoCleanup) { // Run cleanup in background to avoid blocking session creation this.cleanupExpiredSessions().catch(error => { console.warn('Background session cleanup failed:', error); }); } } async getSession(sessionId) { await this.ensureInitialized(); return await this.readSessionFile(sessionId); } async updateSession(session) { await this.ensureInitialized(); await this.writeSessionFile(session); } async deleteSession(sessionId) { await this.ensureInitialized(); try { const filePath = this.getSessionFilePath(sessionId); await fs.unlink(filePath); } catch (error) { if (error.code !== 'ENOENT') { throw error; // Only throw if not "file not found" } } } async getUserSessions(userId) { await this.ensureInitialized(); return await this.getSessions(session => session.userId === userId); } async getActiveSessions(userId) { await this.ensureInitialized(); return await this.getSessions(session => session.userId === userId && session.status === 'active'); } async getSpecialistSessions(specialistId) { await this.ensureInitialized(); return await this.getSessions(session => session.specialistId === specialistId); } async getSessions(filter) { try { const files = await fs.readdir(this.sessionDirectory); const sessionFiles = files.filter(file => file.endsWith('.json')); const summaries = []; for (const file of sessionFiles) { try { const sessionId = file.replace('.json', ''); const session = await this.readSessionFile(sessionId); if (session && filter(session)) { summaries.push(this.sessionToSummary(session)); } } catch (error) { // Skip corrupted session files console.warn(`Warning: Could not read session file ${file}:`, error); } } return summaries.sort((a, b) => b.lastActivity.getTime() - a.lastActivity.getTime()); } catch (error) { console.error('Error reading session directory:', error); return []; } } sessionToSummary(session) { return { sessionId: session.sessionId, specialistId: session.specialistId, startTime: session.startTime, lastActivity: session.lastActivity, status: session.status, messageCount: session.messageCount, primaryTopics: session.context.solutions.slice(0, 3), // Use first few solutions as topics keyInsights: session.context.recommendations.slice(0, 3) // Use first few recommendations as insights }; } async cleanupExpiredSessions() { await this.ensureInitialized(); const maxAgeMs = (this.config.retention?.maxAge || 30) * 24 * 60 * 60 * 1000; // Default 30 days const maxSessions = this.config.retention?.maxSessions || 1000; // Default 1000 sessions const cutoffTime = new Date(Date.now() - maxAgeMs); try { const files = await fs.readdir(this.sessionDirectory); const sessionFiles = files.filter(file => file.endsWith('.json')); let deletedCount = 0; // Step 1: Clean up by age const remainingSessions = []; for (const file of sessionFiles) { try { const sessionId = file.replace('.json', ''); const session = await this.readSessionFile(sessionId); if (session) { if (session.lastActivity < cutoffTime) { await this.deleteSession(sessionId); deletedCount++; } else { remainingSessions.push({ sessionId: session.sessionId, lastActivity: session.lastActivity }); } } } catch (error) { // Skip files that can't be read console.warn(`Warning: Could not process session file ${file} during cleanup:`, error); } } // Step 2: Clean up by session count limit (keep most recent) if (remainingSessions.length > maxSessions) { // Sort by last activity (most recent first) remainingSessions.sort((a, b) => b.lastActivity.getTime() - a.lastActivity.getTime()); // Delete sessions beyond the limit const sessionsToDelete = remainingSessions.slice(maxSessions); for (const session of sessionsToDelete) { await this.deleteSession(session.sessionId); deletedCount++; } } if (deletedCount > 0) { console.error(`๐Ÿงน Cleaned up ${deletedCount} expired sessions`); } return deletedCount; } catch (error) { console.error('Error during session cleanup:', error); return 0; } } async getUserSessionCount(userId) { const sessions = await this.getUserSessions(userId); return sessions.length; } } //# sourceMappingURL=file-storage.js.map