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.

674 lines (673 loc) 27.8 kB
import path from 'path'; import { FileUtils } from '../../utils/file-utils.js'; import { getVibeTaskManagerOutputDir } from '../../utils/config-loader.js'; import logger from '../../../../logger.js'; function isDependencyIndex(data) { if (!data || typeof data !== 'object') return false; const index = data; return Array.isArray(index.dependencies) && typeof index.lastUpdated === 'string' && typeof index.version === 'string'; } export class DependencyStorage { dataDirectory; dependenciesDirectory; graphsDirectory; dependencyIndexFile; constructor(dataDirectory) { this.dataDirectory = dataDirectory || getVibeTaskManagerOutputDir(); this.dependenciesDirectory = path.join(this.dataDirectory, 'dependencies'); this.graphsDirectory = path.join(this.dataDirectory, 'dependency-graphs'); this.dependencyIndexFile = path.join(this.dataDirectory, 'dependencies-index.json'); } async initialize() { try { const depDirResult = await FileUtils.ensureDirectory(this.dependenciesDirectory); if (!depDirResult.success) { return depDirResult; } const graphDirResult = await FileUtils.ensureDirectory(this.graphsDirectory); if (!graphDirResult.success) { return graphDirResult; } if (!await FileUtils.fileExists(this.dependencyIndexFile)) { const indexData = { dependencies: [], lastUpdated: new Date().toISOString(), version: '1.0.0' }; const indexResult = await FileUtils.writeJsonFile(this.dependencyIndexFile, indexData); if (!indexResult.success) { return indexResult; } } logger.debug({ dataDirectory: this.dataDirectory }, 'Dependency storage initialized'); return { success: true, metadata: { filePath: this.dataDirectory, operation: 'initialize', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, dataDirectory: this.dataDirectory }, 'Failed to initialize dependency storage'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.dataDirectory, operation: 'initialize', timestamp: new Date() } }; } } async createDependency(dependency) { try { logger.info({ dependencyId: dependency.id, from: dependency.fromTaskId, to: dependency.toTaskId }, 'Creating dependency'); const validationResult = this.validateDependency(dependency); if (!validationResult.valid) { return { success: false, error: `Dependency validation failed: ${validationResult.errors.join(', ')}`, metadata: { filePath: this.getDependencyFilePath(dependency.id), operation: 'create_dependency', timestamp: new Date() } }; } if (await this.dependencyExists(dependency.id)) { return { success: false, error: `Dependency with ID ${dependency.id} already exists`, metadata: { filePath: this.getDependencyFilePath(dependency.id), operation: 'create_dependency', timestamp: new Date() } }; } const initResult = await this.initialize(); if (!initResult.success) { return { success: false, error: `Failed to initialize storage: ${initResult.error}`, metadata: initResult.metadata }; } const dependencyToSave = { ...dependency, metadata: { ...dependency.metadata, createdAt: new Date() } }; const dependencyFilePath = this.getDependencyFilePath(dependency.id); const saveResult = await FileUtils.writeYamlFile(dependencyFilePath, dependencyToSave); if (!saveResult.success) { return { success: false, error: `Failed to save dependency: ${saveResult.error}`, metadata: saveResult.metadata }; } const indexUpdateResult = await this.updateIndex('add', dependency.id, { id: dependency.id, fromTaskId: dependency.fromTaskId, toTaskId: dependency.toTaskId, type: dependency.type, critical: dependency.critical, createdAt: dependencyToSave.metadata.createdAt }); if (!indexUpdateResult.success) { await FileUtils.deleteFile(dependencyFilePath); return { success: false, error: `Failed to update index: ${indexUpdateResult.error}`, metadata: indexUpdateResult.metadata }; } logger.info({ dependencyId: dependency.id }, 'Dependency created successfully'); return { success: true, data: dependencyToSave, metadata: { filePath: dependencyFilePath, operation: 'create_dependency', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, dependencyId: dependency.id }, 'Failed to create dependency'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getDependencyFilePath(dependency.id), operation: 'create_dependency', timestamp: new Date() } }; } } async getDependency(dependencyId) { try { logger.debug({ dependencyId }, 'Getting dependency'); const dependencyFilePath = this.getDependencyFilePath(dependencyId); if (!await FileUtils.fileExists(dependencyFilePath)) { return { success: false, error: `Dependency ${dependencyId} not found`, metadata: { filePath: dependencyFilePath, operation: 'get_dependency', timestamp: new Date() } }; } const loadResult = await FileUtils.readYamlFile(dependencyFilePath); if (!loadResult.success) { return { success: false, error: `Failed to load dependency: ${loadResult.error}`, metadata: loadResult.metadata }; } return { success: true, data: loadResult.data, metadata: { filePath: dependencyFilePath, operation: 'get_dependency', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, dependencyId }, 'Failed to get dependency'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getDependencyFilePath(dependencyId), operation: 'get_dependency', timestamp: new Date() } }; } } async updateDependency(dependencyId, updates) { try { logger.info({ dependencyId, updates: Object.keys(updates) }, 'Updating dependency'); const getResult = await this.getDependency(dependencyId); if (!getResult.success) { return getResult; } const existingDependency = getResult.data; const updatedDependency = { ...existingDependency, ...updates, id: dependencyId, metadata: { ...existingDependency.metadata, ...updates.metadata, createdAt: existingDependency.metadata.createdAt } }; const validationResult = this.validateDependency(updatedDependency); if (!validationResult.valid) { return { success: false, error: `Dependency validation failed: ${validationResult.errors.join(', ')}`, metadata: { filePath: this.getDependencyFilePath(dependencyId), operation: 'update_dependency', timestamp: new Date() } }; } const dependencyFilePath = this.getDependencyFilePath(dependencyId); const saveResult = await FileUtils.writeYamlFile(dependencyFilePath, updatedDependency); if (!saveResult.success) { return { success: false, error: `Failed to save updated dependency: ${saveResult.error}`, metadata: saveResult.metadata }; } const indexUpdateResult = await this.updateIndex('update', dependencyId, { id: updatedDependency.id, fromTaskId: updatedDependency.fromTaskId, toTaskId: updatedDependency.toTaskId, type: updatedDependency.type, critical: updatedDependency.critical, createdAt: updatedDependency.metadata.createdAt }); if (!indexUpdateResult.success) { logger.warn({ dependencyId, error: indexUpdateResult.error }, 'Failed to update index, but dependency was saved'); } logger.info({ dependencyId }, 'Dependency updated successfully'); return { success: true, data: updatedDependency, metadata: { filePath: dependencyFilePath, operation: 'update_dependency', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, dependencyId }, 'Failed to update dependency'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getDependencyFilePath(dependencyId), operation: 'update_dependency', timestamp: new Date() } }; } } async deleteDependency(dependencyId) { try { logger.info({ dependencyId }, 'Deleting dependency'); if (!await this.dependencyExists(dependencyId)) { return { success: false, error: `Dependency ${dependencyId} not found`, metadata: { filePath: this.getDependencyFilePath(dependencyId), operation: 'delete_dependency', timestamp: new Date() } }; } const dependencyFilePath = this.getDependencyFilePath(dependencyId); const deleteResult = await FileUtils.deleteFile(dependencyFilePath); if (!deleteResult.success) { return { success: false, error: `Failed to delete dependency file: ${deleteResult.error}`, metadata: deleteResult.metadata }; } const indexUpdateResult = await this.updateIndex('remove', dependencyId); if (!indexUpdateResult.success) { logger.warn({ dependencyId, error: indexUpdateResult.error }, 'Failed to update index, but dependency file was deleted'); } logger.info({ dependencyId }, 'Dependency deleted successfully'); return { success: true, metadata: { filePath: dependencyFilePath, operation: 'delete_dependency', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, dependencyId }, 'Failed to delete dependency'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getDependencyFilePath(dependencyId), operation: 'delete_dependency', timestamp: new Date() } }; } } async listDependencies(projectId) { try { logger.debug({ projectId }, 'Listing dependencies'); const indexResult = await this.loadIndex(); if (!indexResult.success) { return { success: false, error: `Failed to load dependency index: ${indexResult.error}`, metadata: indexResult.metadata }; } const index = indexResult.data; const dependencies = []; for (const dependencyInfo of index.dependencies) { const dependencyResult = await this.getDependency(dependencyInfo.id); if (dependencyResult.success) { const dependency = dependencyResult.data; if (!projectId || this.isDependencyInProject(dependency, projectId)) { dependencies.push(dependency); } } else { logger.warn({ dependencyId: dependencyInfo.id, error: dependencyResult.error }, 'Failed to load dependency from index'); } } return { success: true, data: dependencies, metadata: { filePath: this.dependencyIndexFile, operation: 'list_dependencies', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, projectId }, 'Failed to list dependencies'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.dependencyIndexFile, operation: 'list_dependencies', timestamp: new Date() } }; } } async getDependenciesForTask(taskId) { const listResult = await this.listDependencies(); if (!listResult.success) { return listResult; } const dependencies = listResult.data.filter(dep => dep.fromTaskId === taskId); return { success: true, data: dependencies, metadata: { filePath: this.dependencyIndexFile, operation: 'get_dependencies_for_task', timestamp: new Date() } }; } async getDependentsForTask(taskId) { const listResult = await this.listDependencies(); if (!listResult.success) { return listResult; } const dependents = listResult.data.filter(dep => dep.toTaskId === taskId); return { success: true, data: dependents, metadata: { filePath: this.dependencyIndexFile, operation: 'get_dependents_for_task', timestamp: new Date() } }; } async dependencyExists(dependencyId) { const dependencyFilePath = this.getDependencyFilePath(dependencyId); return await FileUtils.fileExists(dependencyFilePath); } async saveDependencyGraph(projectId, graph) { try { logger.info({ projectId }, 'Saving dependency graph'); const graphFilePath = this.getGraphFilePath(projectId); const initResult = await this.initialize(); if (!initResult.success) { return initResult; } const graphToSave = { ...graph, nodes: Object.fromEntries(graph.nodes), metadata: { ...graph.metadata, generatedAt: new Date() } }; const saveResult = await FileUtils.writeJsonFile(graphFilePath, graphToSave); if (!saveResult.success) { return saveResult; } logger.info({ projectId }, 'Dependency graph saved successfully'); return { success: true, metadata: { filePath: graphFilePath, operation: 'save_dependency_graph', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, projectId }, 'Failed to save dependency graph'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getGraphFilePath(projectId), operation: 'save_dependency_graph', timestamp: new Date() } }; } } async loadDependencyGraph(projectId) { try { logger.debug({ projectId }, 'Loading dependency graph'); const graphFilePath = this.getGraphFilePath(projectId); if (!await FileUtils.fileExists(graphFilePath)) { return { success: false, error: `Dependency graph for project ${projectId} not found`, metadata: { filePath: graphFilePath, operation: 'load_dependency_graph', timestamp: new Date() } }; } const loadResult = await FileUtils.readJsonFile(graphFilePath); if (!loadResult.success) { return { success: false, error: `Failed to load dependency graph: ${loadResult.error}`, metadata: loadResult.metadata }; } const graphData = loadResult.data; const graphData_ = graphData; const edges = Array.isArray(graphData_.edges) ? graphData_.edges : []; const executionOrder = Array.isArray(graphData_.executionOrder) ? graphData_.executionOrder : []; const criticalPath = Array.isArray(graphData_.criticalPath) ? graphData_.criticalPath : []; const statisticsData = graphData_.statistics && typeof graphData_.statistics === 'object' ? graphData_.statistics : {}; const statistics = { totalTasks: typeof statisticsData.totalTasks === 'number' ? statisticsData.totalTasks : 0, totalDependencies: typeof statisticsData.totalDependencies === 'number' ? statisticsData.totalDependencies : 0, maxDepth: typeof statisticsData.maxDepth === 'number' ? statisticsData.maxDepth : 0, cyclicDependencies: Array.isArray(statisticsData.cyclicDependencies) ? statisticsData.cyclicDependencies : [], orphanedTasks: Array.isArray(statisticsData.orphanedTasks) ? statisticsData.orphanedTasks : [] }; const metadataData = graphData_.metadata && typeof graphData_.metadata === 'object' ? graphData_.metadata : {}; const metadata = { generatedAt: typeof metadataData.generatedAt === 'string' || typeof metadataData.generatedAt === 'number' ? new Date(metadataData.generatedAt) : new Date(), version: typeof metadataData.version === 'string' ? metadataData.version : '1.0.0', isValid: typeof metadataData.isValid === 'boolean' ? metadataData.isValid : true, validationErrors: Array.isArray(metadataData.validationErrors) ? metadataData.validationErrors : [] }; const graph = { projectId: graphData_.projectId || '', nodes: new Map(Object.entries(graphData_.nodes || {})), edges, executionOrder, criticalPath, statistics, metadata }; return { success: true, data: graph, metadata: { filePath: graphFilePath, operation: 'load_dependency_graph', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, projectId }, 'Failed to load dependency graph'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getGraphFilePath(projectId), operation: 'load_dependency_graph', timestamp: new Date() } }; } } async deleteDependencyGraph(projectId) { try { logger.info({ projectId }, 'Deleting dependency graph'); const graphFilePath = this.getGraphFilePath(projectId); if (!await FileUtils.fileExists(graphFilePath)) { return { success: true, metadata: { filePath: graphFilePath, operation: 'delete_dependency_graph', timestamp: new Date() } }; } const deleteResult = await FileUtils.deleteFile(graphFilePath); if (!deleteResult.success) { return deleteResult; } logger.info({ projectId }, 'Dependency graph deleted successfully'); return { success: true, metadata: { filePath: graphFilePath, operation: 'delete_dependency_graph', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, projectId }, 'Failed to delete dependency graph'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getGraphFilePath(projectId), operation: 'delete_dependency_graph', timestamp: new Date() } }; } } getDependencyFilePath(dependencyId) { return path.join(this.dependenciesDirectory, `${dependencyId}.yaml`); } getGraphFilePath(projectId) { return path.join(this.graphsDirectory, `${projectId}-graph.json`); } validateDependency(dependency) { const errors = []; if (!dependency.id || typeof dependency.id !== 'string') { errors.push('Dependency ID is required and must be a string'); } if (!dependency.fromTaskId || typeof dependency.fromTaskId !== 'string') { errors.push('From task ID is required and must be a string'); } if (!dependency.toTaskId || typeof dependency.toTaskId !== 'string') { errors.push('To task ID is required and must be a string'); } if (dependency.fromTaskId === dependency.toTaskId) { errors.push('A task cannot depend on itself'); } if (!['blocks', 'enables', 'requires', 'suggests'].includes(dependency.type)) { errors.push('Dependency type must be one of: blocks, enables, requires, suggests'); } if (!dependency.description || typeof dependency.description !== 'string') { errors.push('Dependency description is required and must be a string'); } if (typeof dependency.critical !== 'boolean') { errors.push('Critical flag must be a boolean'); } return { valid: errors.length === 0, errors }; } isDependencyInProject(_dependency, _projectId) { return true; } async loadIndex() { if (!await FileUtils.fileExists(this.dependencyIndexFile)) { const initResult = await this.initialize(); if (!initResult.success) { return initResult; } } const result = await FileUtils.readJsonFile(this.dependencyIndexFile); if (!result.success) { return result; } if (!isDependencyIndex(result.data)) { return { success: false, error: 'Invalid dependency index format', metadata: result.metadata }; } return { success: true, data: result.data, metadata: result.metadata }; } async updateIndex(operation, dependencyId, dependencyInfo) { try { const indexResult = await this.loadIndex(); if (!indexResult.success) { return indexResult; } const index = indexResult.data; switch (operation) { case 'add': if (!index.dependencies.find(d => d.id === dependencyId) && dependencyInfo) { index.dependencies.push(dependencyInfo); } break; case 'update': { const updateIndex = index.dependencies.findIndex(d => d.id === dependencyId); if (updateIndex !== -1 && dependencyInfo) { index.dependencies[updateIndex] = dependencyInfo; } break; } case 'remove': index.dependencies = index.dependencies.filter(d => d.id !== dependencyId); break; } index.lastUpdated = new Date().toISOString(); return await FileUtils.writeJsonFile(this.dependencyIndexFile, index); } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.dependencyIndexFile, operation: 'update_index', timestamp: new Date() } }; } } }