UNPKG

ultimate-mcp-server

Version:

The definitive all-in-one Model Context Protocol server for AI-assisted coding across 30+ platforms

290 lines 10.4 kB
import fs from 'fs/promises'; import path from 'path'; import crypto from 'crypto'; import { Logger } from '../utils/logger.js'; import { getHomeDir, joinPath, getPlatformInfo } from '../utils/platform-utils.js'; export class SessionStorage { logger; sessionDir; currentSession = null; sessionFile; autoSaveInterval = null; isDirty = false; constructor(sessionId) { this.logger = new Logger('SessionStorage'); // Determine session directory (similar to Claude Code's approach) const baseDir = process.env.MCP_SESSION_DIR || joinPath(getHomeDir(), '.ultimate-mcp', 'sessions'); this.sessionDir = baseDir; // Generate or use provided session ID const id = sessionId || this.generateSessionId(); this.sessionFile = path.join(this.sessionDir, `${id}.json`); } async initialize() { try { // Ensure session directory exists await fs.mkdir(this.sessionDir, { recursive: true }); // Try to load existing session await this.loadSession(); // Start auto-save interval (every 30 seconds) this.startAutoSave(); this.logger.info(`Session initialized: ${this.currentSession?.id}`); } catch (error) { this.logger.error('Failed to initialize session storage:', error); // Create new session if loading fails await this.createNewSession(); } } generateSessionId() { const timestamp = Date.now(); const random = crypto.randomBytes(8).toString('hex'); return `session-${timestamp}-${random}`; } async createNewSession() { const sessionId = path.basename(this.sessionFile, '.json'); this.currentSession = { id: sessionId, created: new Date().toISOString(), lastAccessed: new Date().toISOString(), conversations: [], metadata: { ...getPlatformInfo(), nodeVersion: process.version, mcpVersion: '2.0.0', startTime: Date.now() }, tools: [], files: [] }; await this.saveSession(); } async loadSession() { try { const data = await fs.readFile(this.sessionFile, 'utf-8'); this.currentSession = JSON.parse(data); // Update last accessed time if (this.currentSession) { this.currentSession.lastAccessed = new Date().toISOString(); this.isDirty = true; } } catch (error) { // Session file doesn't exist or is corrupted await this.createNewSession(); } } async saveSession() { if (!this.currentSession) return; try { const data = JSON.stringify(this.currentSession, null, 2); await fs.writeFile(this.sessionFile, data, 'utf-8'); this.isDirty = false; this.logger.debug('Session saved successfully'); } catch (error) { this.logger.error('Failed to save session:', error); } } startAutoSave() { this.autoSaveInterval = setInterval(async () => { if (this.isDirty) { await this.saveSession(); } }, 30000); // Save every 30 seconds if there are changes } async addMessage(conversationId, message) { if (!this.currentSession) return; // Find or create conversation let conversation = this.currentSession.conversations.find(c => c.id === conversationId); if (!conversation) { conversation = { id: conversationId, created: new Date().toISOString(), updated: new Date().toISOString(), messages: [], context: {}, tokens: 0 }; this.currentSession.conversations.push(conversation); } // Add message conversation.messages.push(message); conversation.updated = new Date().toISOString(); // Estimate tokens (rough calculation) const messageTokens = Math.ceil(message.content.length / 4); conversation.tokens += messageTokens; // Keep conversation size manageable (max 100 messages) if (conversation.messages.length > 100) { // Keep first 10 and last 90 messages for context conversation.messages = [ ...conversation.messages.slice(0, 10), { role: 'system', content: '[... earlier messages truncated for space ...]' }, ...conversation.messages.slice(-89) ]; } this.isDirty = true; } async addToolUsage(tool, input, output, duration) { if (!this.currentSession) return; this.currentSession.tools.push({ tool, timestamp: new Date().toISOString(), input, output, duration }); // Keep only last 500 tool usages if (this.currentSession.tools.length > 500) { this.currentSession.tools = this.currentSession.tools.slice(-500); } this.isDirty = true; } async addFileAccess(filePath, operation, content) { if (!this.currentSession) return; this.currentSession.files.push({ path: filePath, operation, timestamp: new Date().toISOString(), content: content ? content.substring(0, 1000) : undefined // Store first 1000 chars }); // Keep only last 200 file accesses if (this.currentSession.files.length > 200) { this.currentSession.files = this.currentSession.files.slice(-200); } this.isDirty = true; } async getConversationHistory(conversationId, limit = 50) { if (!this.currentSession) return []; const conversation = this.currentSession.conversations.find(c => c.id === conversationId); if (!conversation) return []; return conversation.messages.slice(-limit); } async getAllConversations() { if (!this.currentSession) return []; return this.currentSession.conversations; } async getRecentTools(limit = 20) { if (!this.currentSession) return []; return this.currentSession.tools.slice(-limit); } async getRecentFiles(limit = 20) { if (!this.currentSession) return []; return this.currentSession.files.slice(-limit); } async searchMessages(query, limit = 20) { if (!this.currentSession) return []; const results = []; const lowerQuery = query.toLowerCase(); for (const conversation of this.currentSession.conversations) { for (const message of conversation.messages) { if (message.content.toLowerCase().includes(lowerQuery)) { results.push(message); if (results.length >= limit) return results; } } } return results; } async getSessionMetadata() { if (!this.currentSession) return {}; return { ...this.currentSession.metadata, sessionId: this.currentSession.id, created: this.currentSession.created, lastAccessed: this.currentSession.lastAccessed, conversationCount: this.currentSession.conversations.length, totalMessages: this.currentSession.conversations.reduce((sum, c) => sum + c.messages.length, 0), totalTokens: this.currentSession.conversations.reduce((sum, c) => sum + c.tokens, 0), toolUsageCount: this.currentSession.tools.length, fileAccessCount: this.currentSession.files.length }; } async listSessions() { try { const files = await fs.readdir(this.sessionDir); return files .filter(f => f.endsWith('.json')) .map(f => path.basename(f, '.json')); } catch (error) { return []; } } async switchSession(sessionId) { // Save current session await this.saveSession(); // Switch to new session this.sessionFile = path.join(this.sessionDir, `${sessionId}.json`); await this.loadSession(); } async deleteSession(sessionId) { const targetId = sessionId || this.currentSession?.id; if (!targetId) return; const targetFile = path.join(this.sessionDir, `${targetId}.json`); try { await fs.unlink(targetFile); // If deleting current session, create a new one if (targetId === this.currentSession?.id) { await this.createNewSession(); } } catch (error) { this.logger.error('Failed to delete session:', error); } } async exportSession() { if (!this.currentSession) return '{}'; return JSON.stringify(this.currentSession, null, 2); } async importSession(data) { try { const sessionData = JSON.parse(data); // Validate session data structure if (!sessionData.id || !sessionData.conversations) { throw new Error('Invalid session data format'); } this.currentSession = sessionData; await this.saveSession(); } catch (error) { this.logger.error('Failed to import session:', error); throw error; } } async cleanup() { // Stop auto-save if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval); this.autoSaveInterval = null; } // Save final state await this.saveSession(); } // Get current session ID getCurrentSessionId() { return this.currentSession?.id || null; } // Check if session has unsaved changes hasUnsavedChanges() { return this.isDirty; } } //# sourceMappingURL=session-storage.js.map