UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

756 lines (755 loc) 32.6 kB
import { EventEmitter } from 'events'; import { promises as fs } from 'fs'; const { writeFile, unlink } = fs; import { join } from 'path'; import { getVibeTaskManagerOutputDir } from '../utils/config-loader.js'; import { ErrorFactory, createErrorContext } from '../utils/enhanced-errors.js'; import { createSuccess, createFailure } from './unified-lifecycle-manager.js'; import logger from '../../../logger.js'; export function createStorageId(id) { if (!id || id.trim().length === 0) { throw new Error('Storage ID cannot be empty'); } return id; } export function createTransactionId(id) { if (!id || id.trim().length === 0) { throw new Error('Transaction ID cannot be empty'); } return id; } export function createBackupId(id) { if (!id || id.trim().length === 0) { throw new Error('Backup ID cannot be empty'); } return id; } export function createCacheKey(key) { if (!key || key.trim().length === 0) { throw new Error('Cache key cannot be empty'); } return key; } export class UnifiedStorageEngine extends EventEmitter { static instance = null; config; dataDirectory; initialized = false; cache = new Map(); transactions = new Map(); backups = new Map(); taskIndex = { tasks: [], lastUpdated: '', version: '1.0.0' }; projectIndex = { projects: [], lastUpdated: '', version: '1.0.0' }; dependencyIndex = { dependencies: [], lastUpdated: '', version: '1.0.0' }; epicIndex = { epics: [], lastUpdated: '', version: '1.0.0' }; operationCount = 0; operationsByType = new Map(); operationsByEntity = new Map(); totalResponseTime = 0; cacheHits = 0; cacheRequests = 0; backupTimer = null; cacheCleanupTimer = null; metricsTimer = null; constructor(config) { super(); this.config = config; this.dataDirectory = config.dataDirectory; ['create', 'read', 'update', 'delete', 'list', 'search'].forEach(op => { this.operationsByType.set(op, 0); }); ['task', 'project', 'dependency', 'epic', 'graph'].forEach(entity => { this.operationsByEntity.set(entity, 0); }); logger.info('Unified Storage Engine initialized'); } static getInstance(config) { if (!UnifiedStorageEngine.instance) { if (!config) { throw new Error('Configuration required for first initialization'); } UnifiedStorageEngine.instance = new UnifiedStorageEngine(config); } return UnifiedStorageEngine.instance; } static resetInstance() { if (UnifiedStorageEngine.instance) { UnifiedStorageEngine.instance.dispose(); UnifiedStorageEngine.instance = null; } } async initialize() { if (this.initialized) { return createSuccess(undefined); } try { await this.createDirectoryStructure(); await this.loadIndexes(); this.startBackgroundProcesses(); this.initialized = true; this.emit('initialized'); logger.info('Storage engine initialized successfully'); return createSuccess(undefined); } catch (error) { return createFailure(ErrorFactory.createError('system', `Failed to initialize storage engine: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedStorageEngine', 'initialize').build(), { cause: error instanceof Error ? error : undefined })); } } async createDirectoryStructure() { const directories = [ this.dataDirectory, join(this.dataDirectory, 'tasks'), join(this.dataDirectory, 'projects'), join(this.dataDirectory, 'dependencies'), join(this.dataDirectory, 'epics'), join(this.dataDirectory, 'graphs'), join(this.dataDirectory, 'indexes'), join(this.dataDirectory, 'backups'), join(this.dataDirectory, 'cache'), join(this.dataDirectory, 'logs') ]; for (const dir of directories) { await fs.mkdir(dir, { recursive: true }); } } async loadIndexes() { const indexDir = join(this.dataDirectory, 'indexes'); try { const taskIndexPath = join(indexDir, 'tasks.json'); if (await this.fileExists(taskIndexPath)) { const taskIndexData = await fs.readFile(taskIndexPath, 'utf-8'); this.taskIndex = JSON.parse(taskIndexData); } const projectIndexPath = join(indexDir, 'projects.json'); if (await this.fileExists(projectIndexPath)) { const projectIndexData = await fs.readFile(projectIndexPath, 'utf-8'); this.projectIndex = JSON.parse(projectIndexData); } const dependencyIndexPath = join(indexDir, 'dependencies.json'); if (await this.fileExists(dependencyIndexPath)) { const dependencyIndexData = await fs.readFile(dependencyIndexPath, 'utf-8'); this.dependencyIndex = JSON.parse(dependencyIndexData); } const epicIndexPath = join(indexDir, 'epics.json'); if (await this.fileExists(epicIndexPath)) { const epicIndexData = await fs.readFile(epicIndexPath, 'utf-8'); this.epicIndex = JSON.parse(epicIndexData); } logger.info('Storage indexes loaded successfully'); } catch (error) { logger.warn('Failed to load some indexes, using defaults:', error); } } startBackgroundProcesses() { if (this.config.backup.enabled) { this.backupTimer = setInterval(() => { this.performBackup().catch(error => { logger.error('Backup process failed:', error); }); }, this.config.backup.intervalMinutes * 60 * 1000); } if (this.config.cache.enabled) { this.cacheCleanupTimer = setInterval(() => { this.cleanupCache(); }, 60 * 1000); } if (this.config.monitoring.enableMetrics) { this.metricsTimer = setInterval(() => { this.collectMetrics(); }, this.config.monitoring.metricsInterval * 1000); } logger.info('Background processes started'); } async createTask(task) { const startTime = Date.now(); try { if (await this.taskExists(task.id)) { return createFailure(ErrorFactory.createError('validation', `Task already exists: ${task.id}`, createErrorContext('UnifiedStorageEngine', 'createTask') .metadata({ taskId: task.id }) .build())); } const taskDir = join(this.dataDirectory, 'tasks'); await fs.mkdir(taskDir, { recursive: true }); const taskPath = join(taskDir, `${task.id}.json`); await fs.writeFile(taskPath, JSON.stringify(task, null, 2), 'utf-8'); this.taskIndex.tasks.push({ id: task.id, title: task.title, status: task.status, priority: task.priority, projectId: task.projectId, epicId: task.epicId, estimatedHours: task.estimatedHours, createdAt: task.createdAt, updatedAt: task.updatedAt }); await this.saveTaskIndex(); if (this.config.cache.enabled) { const cacheKey = createCacheKey(`task:${task.id}`); this.setCache(cacheKey, task); } this.trackOperation('create', 'task', Date.now() - startTime); this.emit('taskCreated', task); logger.info(`Task created: ${task.id}`); return createSuccess(task); } catch (error) { return createFailure(ErrorFactory.createError('system', `Failed to create task: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedStorageEngine', 'createTask') .metadata({ taskId: task.id }) .build(), { cause: error instanceof Error ? error : undefined })); } } async getTask(taskId) { const startTime = Date.now(); try { if (this.config.cache.enabled) { const cacheKey = createCacheKey(`task:${taskId}`); const cached = this.getCache(cacheKey); if (cached) { this.trackOperation('read', 'task', Date.now() - startTime); this.cacheHits++; return createSuccess(cached); } this.cacheRequests++; } const taskPath = join(this.dataDirectory, 'tasks', `${taskId}.json`); if (!await this.fileExists(taskPath)) { return createFailure(ErrorFactory.createError('validation', `Task not found: ${taskId}`, createErrorContext('UnifiedStorageEngine', 'getTask') .metadata({ taskId }) .build())); } const taskData = await fs.readFile(taskPath, 'utf-8'); const task = JSON.parse(taskData); if (this.config.cache.enabled) { const cacheKey = createCacheKey(`task:${taskId}`); this.setCache(cacheKey, task); } this.trackOperation('read', 'task', Date.now() - startTime); return createSuccess(task); } catch (error) { return createFailure(ErrorFactory.createError('system', `Failed to get task: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedStorageEngine', 'getTask') .metadata({ taskId }) .build(), { cause: error instanceof Error ? error : undefined })); } } async updateTask(taskId, updates) { const startTime = Date.now(); try { const getResult = await this.getTask(taskId); if (!getResult.success) { return getResult; } const existingTask = getResult.data; const updatedTask = { ...existingTask, ...updates, id: taskId, updatedAt: new Date() }; const taskPath = join(this.dataDirectory, 'tasks', `${taskId}.json`); await fs.writeFile(taskPath, JSON.stringify(updatedTask, null, 2), 'utf-8'); const indexEntry = this.taskIndex.tasks.find(t => t.id === taskId); if (indexEntry) { Object.assign(indexEntry, { title: updatedTask.title, status: updatedTask.status, priority: updatedTask.priority, projectId: updatedTask.projectId, epicId: updatedTask.epicId, estimatedHours: updatedTask.estimatedHours, updatedAt: updatedTask.updatedAt }); await this.saveTaskIndex(); } if (this.config.cache.enabled) { const cacheKey = createCacheKey(`task:${taskId}`); this.setCache(cacheKey, updatedTask); } this.trackOperation('update', 'task', Date.now() - startTime); this.emit('taskUpdated', updatedTask); logger.info(`Task updated: ${taskId}`); return createSuccess(updatedTask); } catch (error) { return createFailure(ErrorFactory.createError('system', `Failed to update task: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedStorageEngine', 'updateTask') .metadata({ taskId }) .build(), { cause: error instanceof Error ? error : undefined })); } } async deleteTask(taskId) { const startTime = Date.now(); try { if (!await this.taskExists(taskId)) { return createFailure(ErrorFactory.createError('validation', `Task not found: ${taskId}`, createErrorContext('UnifiedStorageEngine', 'deleteTask') .metadata({ taskId }) .build())); } const taskPath = join(this.dataDirectory, 'tasks', `${taskId}.json`); await fs.unlink(taskPath); this.taskIndex.tasks = this.taskIndex.tasks.filter(t => t.id !== taskId); await this.saveTaskIndex(); if (this.config.cache.enabled) { const cacheKey = createCacheKey(`task:${taskId}`); this.cache.delete(cacheKey); } this.trackOperation('delete', 'task', Date.now() - startTime); this.emit('taskDeleted', { taskId }); logger.info(`Task deleted: ${taskId}`); return createSuccess(undefined); } catch (error) { return createFailure(ErrorFactory.createError('system', `Failed to delete task: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedStorageEngine', 'deleteTask') .metadata({ taskId }) .build(), { cause: error instanceof Error ? error : undefined })); } } async listTasks(projectId) { const startTime = Date.now(); try { let tasks = this.taskIndex.tasks; if (projectId) { tasks = tasks.filter(t => t.projectId === projectId); } const fullTasks = []; for (const taskSummary of tasks) { const taskResult = await this.getTask(taskSummary.id); if (taskResult.success) { fullTasks.push(taskResult.data); } } this.trackOperation('list', 'task', Date.now() - startTime); return createSuccess(fullTasks); } catch (error) { return createFailure(ErrorFactory.createError('system', `Failed to list tasks: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedStorageEngine', 'listTasks') .metadata({ projectId }) .build(), { cause: error instanceof Error ? error : undefined })); } } async taskExists(taskId) { const taskPath = join(this.dataDirectory, 'tasks', `${taskId}.json`); return this.fileExists(taskPath); } async createProject(project) { const startTime = Date.now(); try { if (await this.projectExists(project.id)) { return createFailure(ErrorFactory.createError('validation', `Project already exists: ${project.id}`, createErrorContext('UnifiedStorageEngine', 'createProject') .metadata({ projectId: project.id }) .build())); } const projectDir = join(this.dataDirectory, 'projects'); await fs.mkdir(projectDir, { recursive: true }); const projectPath = join(projectDir, `${project.id}.json`); await fs.writeFile(projectPath, JSON.stringify(project, null, 2), 'utf-8'); this.projectIndex.projects.push({ id: project.id, name: project.name, createdAt: project.metadata.createdAt, updatedAt: project.metadata.updatedAt }); await this.saveProjectIndex(); if (this.config.cache.enabled) { const cacheKey = createCacheKey(`project:${project.id}`); this.setCache(cacheKey, project); } this.trackOperation('create', 'project', Date.now() - startTime); this.emit('projectCreated', project); logger.info(`Project created: ${project.id}`); return createSuccess(project); } catch (error) { return createFailure(ErrorFactory.createError('system', `Failed to create project: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedStorageEngine', 'createProject') .metadata({ projectId: project.id }) .build(), { cause: error instanceof Error ? error : undefined })); } } async getProject(projectId) { const startTime = Date.now(); try { if (this.config.cache.enabled) { const cacheKey = createCacheKey(`project:${projectId}`); const cached = this.getCache(cacheKey); if (cached) { this.trackOperation('read', 'project', Date.now() - startTime); this.cacheHits++; return createSuccess(cached); } this.cacheRequests++; } const projectPath = join(this.dataDirectory, 'projects', `${projectId}.json`); if (!await this.fileExists(projectPath)) { return createFailure(ErrorFactory.createError('validation', `Project not found: ${projectId}`, createErrorContext('UnifiedStorageEngine', 'getProject') .metadata({ projectId }) .build())); } const projectData = await fs.readFile(projectPath, 'utf-8'); const project = JSON.parse(projectData); if (this.config.cache.enabled) { const cacheKey = createCacheKey(`project:${projectId}`); this.setCache(cacheKey, project); } this.trackOperation('read', 'project', Date.now() - startTime); return createSuccess(project); } catch (error) { return createFailure(ErrorFactory.createError('system', `Failed to get project: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedStorageEngine', 'getProject') .metadata({ projectId }) .build(), { cause: error instanceof Error ? error : undefined })); } } async projectExists(projectId) { const projectPath = join(this.dataDirectory, 'projects', `${projectId}.json`); return this.fileExists(projectPath); } async updateProject(projectId, updates) { try { const existingResult = await this.getProject(projectId); if (!existingResult.success) { return existingResult; } const updatedProject = { ...existingResult.data, ...updates, metadata: { ...existingResult.data.metadata, ...updates.metadata, updatedAt: new Date() } }; const projectPath = join(this.dataDirectory, 'projects', `${projectId}.json`); await writeFile(projectPath, JSON.stringify(updatedProject, null, 2), 'utf-8'); const cacheKey = createCacheKey(`project:${projectId}`); this.setCache(cacheKey, updatedProject); const existingIndex = this.projectIndex.projects.findIndex(p => p.id === projectId); const projectIndexEntry = { id: projectId, name: updatedProject.name, createdAt: updatedProject.metadata.createdAt, updatedAt: updatedProject.metadata.updatedAt }; if (existingIndex >= 0) { this.projectIndex.projects[existingIndex] = projectIndexEntry; } else { this.projectIndex.projects.push(projectIndexEntry); } await this.saveProjectIndex(); logger.info({ projectId, updates }, 'Project updated successfully'); return createSuccess(updatedProject); } catch (error) { const enhancedError = ErrorFactory.createError('system', `Failed to update project: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedStorageEngine', 'updateProject').build(), { cause: error instanceof Error ? error : undefined }); logger.error({ error: enhancedError, projectId }, 'Failed to update project'); return createFailure(enhancedError); } } async deleteProject(projectId) { try { const exists = await this.projectExists(projectId); if (!exists) { const error = ErrorFactory.createError('validation', `Project not found: ${projectId}`, createErrorContext('UnifiedStorageEngine', 'deleteProject').build()); return createFailure(error); } const projectPath = join(this.dataDirectory, 'projects', `${projectId}.json`); await unlink(projectPath); const cacheKey = createCacheKey(`project:${projectId}`); this.cache.delete(cacheKey); this.projectIndex.projects = this.projectIndex.projects.filter(p => p.id !== projectId); await this.saveProjectIndex(); logger.info({ projectId }, 'Project deleted successfully'); return createSuccess(undefined); } catch (error) { const enhancedError = ErrorFactory.createError('system', `Failed to delete project: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedStorageEngine', 'deleteProject').build(), { cause: error instanceof Error ? error : undefined }); logger.error({ error: enhancedError, projectId }, 'Failed to delete project'); return createFailure(enhancedError); } } async listProjects() { try { const projects = []; for (const projectEntry of this.projectIndex.projects) { const projectId = projectEntry.id; const projectResult = await this.getProject(projectId); if (projectResult.success) { projects.push(projectResult.data); } } return createSuccess(projects); } catch (error) { const enhancedError = ErrorFactory.createError('system', `Failed to list projects: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedStorageEngine', 'listProjects').build(), { cause: error instanceof Error ? error : undefined }); logger.error({ error: enhancedError }, 'Failed to list projects'); return createFailure(enhancedError); } } setCache(key, value) { if (!this.config.cache.enabled) return; const now = new Date(); const expiresAt = new Date(now.getTime() + this.config.cache.ttlSeconds * 1000); const entry = { key, value, createdAt: now, expiresAt, accessCount: 0, lastAccessed: now, size: JSON.stringify(value).length, compressed: false }; this.cache.set(key, entry); if (this.cache.size > this.config.cache.maxSize) { this.evictOldestCacheEntries(); } } getCache(key) { if (!this.config.cache.enabled) return null; const entry = this.cache.get(key); if (!entry) return null; if (entry.expiresAt < new Date()) { this.cache.delete(key); return null; } entry.accessCount++; entry.lastAccessed = new Date(); return entry.value; } cleanupCache() { const now = new Date(); const expiredKeys = []; for (const [key, entry] of this.cache.entries()) { if (entry.expiresAt < now) { expiredKeys.push(key); } } for (const key of expiredKeys) { this.cache.delete(key); } if (expiredKeys.length > 0) { logger.debug(`Cleaned up ${expiredKeys.length} expired cache entries`); } } evictOldestCacheEntries() { const entries = Array.from(this.cache.entries()); entries.sort((a, b) => a[1].lastAccessed.getTime() - b[1].lastAccessed.getTime()); const toEvict = Math.ceil(this.cache.size * 0.1); for (let i = 0; i < toEvict && i < entries.length; i++) { this.cache.delete(entries[i][0]); } logger.debug(`Evicted ${toEvict} cache entries due to size limit`); } async performBackup() { try { const backupId = createBackupId(`backup_${Date.now()}`); const backupDir = join(this.dataDirectory, 'backups', backupId); await fs.mkdir(backupDir, { recursive: true }); const dataDirs = ['tasks', 'projects', 'dependencies', 'epics', 'graphs', 'indexes']; for (const dir of dataDirs) { const sourceDir = join(this.dataDirectory, dir); const targetDir = join(backupDir, dir); await this.copyDirectory(sourceDir, targetDir); } const backup = { id: backupId, createdAt: new Date(), size: await this.getDirectorySize(backupDir), entities: ['task', 'project', 'dependency', 'epic', 'graph'], compressed: false, encrypted: false, checksum: await this.calculateChecksum(backupDir), metadata: { version: '1.0.0', creator: 'UnifiedStorageEngine', description: 'Automated backup' } }; this.backups.set(backupId, backup); await this.cleanupOldBackups(); this.emit('backupCreated', backup); logger.info(`Backup created: ${backupId}`); } catch (error) { logger.error('Backup failed:', error); this.emit('backupFailed', error); } } async cleanupOldBackups() { const backups = Array.from(this.backups.values()); backups.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); if (backups.length > this.config.backup.maxBackups) { const toDelete = backups.slice(this.config.backup.maxBackups); for (const backup of toDelete) { try { const backupDir = join(this.dataDirectory, 'backups', backup.id); await this.removeDirectory(backupDir); this.backups.delete(backup.id); logger.info(`Old backup deleted: ${backup.id}`); } catch (error) { logger.error(`Failed to delete backup ${backup.id}:`, error); } } } } async saveTaskIndex() { this.taskIndex.lastUpdated = new Date().toISOString(); const indexDir = join(this.dataDirectory, 'indexes'); await fs.mkdir(indexDir, { recursive: true }); const indexPath = join(indexDir, 'tasks.json'); await fs.writeFile(indexPath, JSON.stringify(this.taskIndex, null, 2), 'utf-8'); } async saveProjectIndex() { this.projectIndex.lastUpdated = new Date().toISOString(); const indexDir = join(this.dataDirectory, 'indexes'); await fs.mkdir(indexDir, { recursive: true }); const indexPath = join(indexDir, 'projects.json'); await fs.writeFile(indexPath, JSON.stringify(this.projectIndex, null, 2), 'utf-8'); } async fileExists(path) { try { await fs.access(path); return true; } catch { return false; } } async copyDirectory(source, target) { await fs.mkdir(target, { recursive: true }); const entries = await fs.readdir(source, { withFileTypes: true }); for (const entry of entries) { const sourcePath = join(source, entry.name); const targetPath = join(target, entry.name); if (entry.isDirectory()) { await this.copyDirectory(sourcePath, targetPath); } else { await fs.copyFile(sourcePath, targetPath); } } } async removeDirectory(path) { try { await fs.rm(path, { recursive: true, force: true }); } catch (error) { logger.error(`Failed to remove directory ${path}:`, error); } } async getDirectorySize(path) { let size = 0; try { const entries = await fs.readdir(path, { withFileTypes: true }); for (const entry of entries) { const entryPath = join(path, entry.name); if (entry.isDirectory()) { size += await this.getDirectorySize(entryPath); } else { const stats = await fs.stat(entryPath); size += stats.size; } } } catch (error) { logger.error(`Failed to calculate directory size for ${path}:`, error); } return size; } async calculateChecksum(path) { const size = await this.getDirectorySize(path); return `checksum_${size}_${Date.now()}`; } trackOperation(operation, entity, responseTime) { this.operationCount++; this.operationsByType.set(operation, (this.operationsByType.get(operation) || 0) + 1); this.operationsByEntity.set(entity, (this.operationsByEntity.get(entity) || 0) + 1); this.totalResponseTime += responseTime; } collectMetrics() { const stats = { totalOperations: this.operationCount, operationsByType: Object.fromEntries(this.operationsByType), operationsByEntity: Object.fromEntries(this.operationsByEntity), averageResponseTime: this.operationCount > 0 ? this.totalResponseTime / this.operationCount : 0, cacheHitRate: this.cacheRequests > 0 ? this.cacheHits / this.cacheRequests : 0, storageSize: 0, activeTransactions: this.transactions.size, totalBackups: this.backups.size, errorRate: 0 }; this.emit('metricsCollected', stats); } getStatistics() { return { totalOperations: this.operationCount, operationsByType: Object.fromEntries(this.operationsByType), operationsByEntity: Object.fromEntries(this.operationsByEntity), averageResponseTime: this.operationCount > 0 ? this.totalResponseTime / this.operationCount : 0, cacheHitRate: this.cacheRequests > 0 ? this.cacheHits / this.cacheRequests : 0, storageSize: 0, activeTransactions: this.transactions.size, totalBackups: this.backups.size, errorRate: 0 }; } dispose() { if (this.backupTimer) { clearInterval(this.backupTimer); this.backupTimer = null; } if (this.cacheCleanupTimer) { clearInterval(this.cacheCleanupTimer); this.cacheCleanupTimer = null; } if (this.metricsTimer) { clearInterval(this.metricsTimer); this.metricsTimer = null; } this.cache.clear(); this.transactions.clear(); this.backups.clear(); this.removeAllListeners(); this.initialized = false; logger.info('Unified Storage Engine disposed'); } } export function createDefaultStorageConfig() { return { dataDirectory: getVibeTaskManagerOutputDir(), format: 'json', cache: { enabled: true, maxSize: 1000, ttlSeconds: 3600, compressionEnabled: false, persistToDisk: false }, backup: { enabled: true, intervalMinutes: 60, maxBackups: 10, compressionEnabled: true, encryptionEnabled: false, remoteBackupEnabled: false }, performance: { batchSize: 100, maxConcurrentOperations: 10, enableCompression: false, enableEncryption: false }, monitoring: { enableMetrics: true, metricsInterval: 60, enableAuditLog: true, enablePerformanceTracking: true } }; }