Skip to main content
Glama
MUSE-CODE-SPACE

Vibe Coding Documentation MCP (MUSE)

sessionStorage.ts8.18 kB
/** * 세션 스토리지 모듈 * 바이브 코딩 세션을 저장하고 조회하는 기능 제공 * v2.6: 세션 히스토리 기능 */ import { promises as fs } from 'fs'; import * as path from 'path'; import { logger } from './logger.js'; const storageLogger = logger.child({ module: 'sessionStorage' }); export interface StoredSession { id: string; title: string; summary: string; createdAt: string; updatedAt: string; tags: string[]; codeContextCount: number; designDecisionCount: number; metadata?: Record<string, unknown>; } export interface SessionData { id: string; title: string; summary: string; createdAt: string; updatedAt: string; tags: string[]; codeContexts: Array<{ sessionId: string; timestamp: string; codeBlocks: Array<{ language: string; code: string; filename?: string }>; conversationSummary: string; }>; designDecisions: Array<{ id: string; title: string; description: string; rationale: string; category: string; timestamp: string; }>; metadata?: Record<string, unknown>; } // Default storage path const DEFAULT_STORAGE_DIR = process.env.VIBE_CODING_STORAGE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '.', '.vibe-coding-mcp', 'sessions'); let storageDir = DEFAULT_STORAGE_DIR; /** * Initialize storage directory */ export async function initializeStorage(customDir?: string): Promise<void> { if (customDir) { storageDir = customDir; } try { await fs.mkdir(storageDir, { recursive: true }); storageLogger.info('Storage initialized', { path: storageDir }); } catch (error) { storageLogger.error('Failed to initialize storage', error as Error); throw error; } } /** * Generate unique session ID */ function generateSessionId(): string { const timestamp = Date.now().toString(36); const random = Math.random().toString(36).substring(2, 8); return `session_${timestamp}_${random}`; } /** * Get session file path */ function getSessionPath(sessionId: string): string { return path.join(storageDir, `${sessionId}.json`); } /** * Save a session */ export async function saveSession(data: Omit<SessionData, 'id' | 'createdAt' | 'updatedAt'>): Promise<SessionData> { await initializeStorage(); const sessionId = generateSessionId(); const now = new Date().toISOString(); const session: SessionData = { id: sessionId, createdAt: now, updatedAt: now, ...data }; const filePath = getSessionPath(sessionId); try { await fs.writeFile(filePath, JSON.stringify(session, null, 2), 'utf-8'); storageLogger.info('Session saved', { sessionId, title: session.title }); return session; } catch (error) { storageLogger.error('Failed to save session', error as Error); throw error; } } /** * Update an existing session */ export async function updateSession(sessionId: string, updates: Partial<Omit<SessionData, 'id' | 'createdAt'>>): Promise<SessionData> { const existing = await getSession(sessionId); if (!existing) { throw new Error(`Session not found: ${sessionId}`); } const updated: SessionData = { ...existing, ...updates, updatedAt: new Date().toISOString() }; const filePath = getSessionPath(sessionId); try { await fs.writeFile(filePath, JSON.stringify(updated, null, 2), 'utf-8'); storageLogger.info('Session updated', { sessionId }); return updated; } catch (error) { storageLogger.error('Failed to update session', error as Error); throw error; } } /** * Get a session by ID */ export async function getSession(sessionId: string): Promise<SessionData | null> { const filePath = getSessionPath(sessionId); try { const content = await fs.readFile(filePath, 'utf-8'); return JSON.parse(content) as SessionData; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return null; } storageLogger.error('Failed to read session', error as Error); throw error; } } /** * Delete a session */ export async function deleteSession(sessionId: string): Promise<boolean> { const filePath = getSessionPath(sessionId); try { await fs.unlink(filePath); storageLogger.info('Session deleted', { sessionId }); return true; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return false; } storageLogger.error('Failed to delete session', error as Error); throw error; } } /** * List all sessions (summary only) */ export async function listSessions(options?: { limit?: number; offset?: number; tags?: string[]; sortBy?: 'createdAt' | 'updatedAt' | 'title'; sortOrder?: 'asc' | 'desc'; }): Promise<{ sessions: StoredSession[]; total: number }> { await initializeStorage(); const { limit = 50, offset = 0, tags, sortBy = 'updatedAt', sortOrder = 'desc' } = options || {}; try { const files = await fs.readdir(storageDir); const sessionFiles = files.filter(f => f.endsWith('.json')); const sessions: StoredSession[] = []; for (const file of sessionFiles) { try { const content = await fs.readFile(path.join(storageDir, file), 'utf-8'); const data = JSON.parse(content) as SessionData; // Filter by tags if specified if (tags && tags.length > 0) { const hasTag = tags.some(tag => data.tags?.includes(tag)); if (!hasTag) continue; } sessions.push({ id: data.id, title: data.title, summary: data.summary, createdAt: data.createdAt, updatedAt: data.updatedAt, tags: data.tags || [], codeContextCount: data.codeContexts?.length || 0, designDecisionCount: data.designDecisions?.length || 0, metadata: data.metadata }); } catch { // Skip invalid files continue; } } // Sort sessions.sort((a, b) => { const aVal = a[sortBy] || ''; const bVal = b[sortBy] || ''; const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; return sortOrder === 'desc' ? -cmp : cmp; }); // Paginate const total = sessions.length; const paginated = sessions.slice(offset, offset + limit); return { sessions: paginated, total }; } catch (error) { storageLogger.error('Failed to list sessions', error as Error); throw error; } } /** * Search sessions by keyword */ export async function searchSessions(keyword: string, options?: { limit?: number; searchIn?: ('title' | 'summary' | 'tags')[]; }): Promise<StoredSession[]> { const { limit = 20, searchIn = ['title', 'summary', 'tags'] } = options || {}; const lowerKeyword = keyword.toLowerCase(); const { sessions } = await listSessions({ limit: 1000 }); const matches = sessions.filter(session => { if (searchIn.includes('title') && session.title.toLowerCase().includes(lowerKeyword)) { return true; } if (searchIn.includes('summary') && session.summary.toLowerCase().includes(lowerKeyword)) { return true; } if (searchIn.includes('tags') && session.tags.some(t => t.toLowerCase().includes(lowerKeyword))) { return true; } return false; }); return matches.slice(0, limit); } /** * Get storage statistics */ export async function getStorageStats(): Promise<{ totalSessions: number; totalCodeContexts: number; totalDesignDecisions: number; storageDir: string; oldestSession?: string; newestSession?: string; }> { const { sessions, total } = await listSessions({ limit: 1000 }); let totalCodeContexts = 0; let totalDesignDecisions = 0; for (const session of sessions) { totalCodeContexts += session.codeContextCount; totalDesignDecisions += session.designDecisionCount; } // Sort by createdAt to find oldest/newest const sorted = [...sessions].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() ); return { totalSessions: total, totalCodeContexts, totalDesignDecisions, storageDir, oldestSession: sorted[0]?.createdAt, newestSession: sorted[sorted.length - 1]?.createdAt }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MUSE-CODE-SPACE/vibe-coding-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server