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.

953 lines (952 loc) 37.3 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 isTaskIndex(data) { if (!data || typeof data !== 'object') return false; const index = data; return Array.isArray(index.tasks) && typeof index.lastUpdated === 'string' && typeof index.version === 'string'; } function isEpicIndex(data) { if (!data || typeof data !== 'object') return false; const index = data; return Array.isArray(index.epics) && typeof index.lastUpdated === 'string' && typeof index.version === 'string'; } export class TaskStorage { dataDirectory; tasksDirectory; epicsDirectory; taskIndexFile; epicIndexFile; constructor(dataDirectory) { this.dataDirectory = dataDirectory || getVibeTaskManagerOutputDir(); this.tasksDirectory = path.join(this.dataDirectory, 'tasks'); this.epicsDirectory = path.join(this.dataDirectory, 'epics'); this.taskIndexFile = path.join(this.dataDirectory, 'tasks-index.json'); this.epicIndexFile = path.join(this.dataDirectory, 'epics-index.json'); } async initialize() { try { const tasksDirResult = await FileUtils.ensureDirectory(this.tasksDirectory); if (!tasksDirResult.success) { return tasksDirResult; } const epicsDirResult = await FileUtils.ensureDirectory(this.epicsDirectory); if (!epicsDirResult.success) { return epicsDirResult; } if (!await FileUtils.fileExists(this.taskIndexFile)) { const taskIndexData = { tasks: [], lastUpdated: new Date().toISOString(), version: '1.0.0' }; const taskIndexResult = await FileUtils.writeJsonFile(this.taskIndexFile, taskIndexData); if (!taskIndexResult.success) { return taskIndexResult; } } if (!await FileUtils.fileExists(this.epicIndexFile)) { const epicIndexData = { epics: [], lastUpdated: new Date().toISOString(), version: '1.0.0' }; const epicIndexResult = await FileUtils.writeJsonFile(this.epicIndexFile, epicIndexData); if (!epicIndexResult.success) { return epicIndexResult; } } logger.debug({ dataDirectory: this.dataDirectory }, 'Task 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 task storage'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.dataDirectory, operation: 'initialize', timestamp: new Date() } }; } } async createTask(task) { try { logger.info({ taskId: task.id, title: task.title }, 'Creating task'); const validationResult = this.validateTask(task); if (!validationResult.valid) { return { success: false, error: `Task validation failed: ${validationResult.errors.join(', ')}`, metadata: { filePath: this.getTaskFilePath(task.id), operation: 'create_task', timestamp: new Date() } }; } if (await this.taskExists(task.id)) { return { success: false, error: `Task with ID ${task.id} already exists`, metadata: { filePath: this.getTaskFilePath(task.id), operation: 'create_task', 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 taskToSave = { ...task, metadata: { ...task.metadata, createdAt: new Date(), updatedAt: new Date() } }; const taskFilePath = this.getTaskFilePath(task.id); const saveResult = await FileUtils.writeYamlFile(taskFilePath, taskToSave); if (!saveResult.success) { return { success: false, error: `Failed to save task: ${saveResult.error}`, metadata: saveResult.metadata }; } const indexUpdateResult = await this.updateTaskIndex('add', task.id, { id: task.id, title: task.title, status: task.status, priority: task.priority, projectId: task.projectId, epicId: task.epicId, estimatedHours: task.estimatedHours, createdAt: taskToSave.metadata.createdAt, updatedAt: taskToSave.metadata.updatedAt }); if (!indexUpdateResult.success) { await FileUtils.deleteFile(taskFilePath); return { success: false, error: `Failed to update index: ${indexUpdateResult.error}`, metadata: indexUpdateResult.metadata }; } logger.info({ taskId: task.id }, 'Task created successfully'); return { success: true, data: taskToSave, metadata: { filePath: taskFilePath, operation: 'create_task', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, taskId: task.id }, 'Failed to create task'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getTaskFilePath(task.id), operation: 'create_task', timestamp: new Date() } }; } } async getTask(taskId) { try { logger.debug({ taskId }, 'Getting task'); const taskFilePath = this.getTaskFilePath(taskId); if (!await FileUtils.fileExists(taskFilePath)) { return { success: false, error: `Task ${taskId} not found`, metadata: { filePath: taskFilePath, operation: 'get_task', timestamp: new Date() } }; } const loadResult = await FileUtils.readYamlFile(taskFilePath); if (!loadResult.success) { return { success: false, error: `Failed to load task: ${loadResult.error}`, metadata: loadResult.metadata }; } return { success: true, data: loadResult.data, metadata: { filePath: taskFilePath, operation: 'get_task', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, taskId }, 'Failed to get task'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getTaskFilePath(taskId), operation: 'get_task', timestamp: new Date() } }; } } async updateTask(taskId, updates) { try { logger.info({ taskId, updates: Object.keys(updates) }, 'Updating task'); const getResult = await this.getTask(taskId); if (!getResult.success) { return getResult; } const existingTask = getResult.data; const updatedTask = { ...existingTask, ...updates, id: taskId, metadata: { ...existingTask.metadata, ...updates.metadata, createdAt: existingTask.metadata.createdAt, updatedAt: new Date() } }; const validationResult = this.validateTask(updatedTask); if (!validationResult.valid) { return { success: false, error: `Task validation failed: ${validationResult.errors.join(', ')}`, metadata: { filePath: this.getTaskFilePath(taskId), operation: 'update_task', timestamp: new Date() } }; } const taskFilePath = this.getTaskFilePath(taskId); const saveResult = await FileUtils.writeYamlFile(taskFilePath, updatedTask); if (!saveResult.success) { return { success: false, error: `Failed to save updated task: ${saveResult.error}`, metadata: saveResult.metadata }; } const indexUpdateResult = await this.updateTaskIndex('update', taskId, { id: updatedTask.id, title: updatedTask.title, status: updatedTask.status, priority: updatedTask.priority, projectId: updatedTask.projectId, epicId: updatedTask.epicId, estimatedHours: updatedTask.estimatedHours, createdAt: updatedTask.metadata.createdAt, updatedAt: updatedTask.metadata.updatedAt }); if (!indexUpdateResult.success) { logger.warn({ taskId, error: indexUpdateResult.error }, 'Failed to update index, but task was saved'); } logger.info({ taskId }, 'Task updated successfully'); return { success: true, data: updatedTask, metadata: { filePath: taskFilePath, operation: 'update_task', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, taskId }, 'Failed to update task'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getTaskFilePath(taskId), operation: 'update_task', timestamp: new Date() } }; } } async deleteTask(taskId) { try { logger.info({ taskId }, 'Deleting task'); if (!await this.taskExists(taskId)) { return { success: false, error: `Task ${taskId} not found`, metadata: { filePath: this.getTaskFilePath(taskId), operation: 'delete_task', timestamp: new Date() } }; } const taskFilePath = this.getTaskFilePath(taskId); const deleteResult = await FileUtils.deleteFile(taskFilePath); if (!deleteResult.success) { return { success: false, error: `Failed to delete task file: ${deleteResult.error}`, metadata: deleteResult.metadata }; } const indexUpdateResult = await this.updateTaskIndex('remove', taskId); if (!indexUpdateResult.success) { logger.warn({ taskId, error: indexUpdateResult.error }, 'Failed to update index, but task file was deleted'); } logger.info({ taskId }, 'Task deleted successfully'); return { success: true, metadata: { filePath: taskFilePath, operation: 'delete_task', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, taskId }, 'Failed to delete task'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getTaskFilePath(taskId), operation: 'delete_task', timestamp: new Date() } }; } } async listTasks(projectId, epicId) { try { logger.debug({ projectId, epicId }, 'Listing tasks'); const indexResult = await this.loadTaskIndex(); if (!indexResult.success) { return { success: false, error: `Failed to load task index: ${indexResult.error}`, metadata: indexResult.metadata }; } const index = indexResult.data; let taskInfos = index.tasks; if (projectId) { taskInfos = taskInfos.filter(task => task.projectId === projectId); } if (epicId) { taskInfos = taskInfos.filter(task => task.epicId === epicId); } const tasks = []; for (const taskInfo of taskInfos) { const taskResult = await this.getTask(taskInfo.id); if (taskResult.success) { tasks.push(taskResult.data); } else { logger.warn({ taskId: taskInfo.id, error: taskResult.error }, 'Failed to load task from index'); } } return { success: true, data: tasks, metadata: { filePath: this.taskIndexFile, operation: 'list_tasks', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, projectId, epicId }, 'Failed to list tasks'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.taskIndexFile, operation: 'list_tasks', timestamp: new Date() } }; } } async getTasksByStatus(status, projectId) { const listResult = await this.listTasks(projectId); if (!listResult.success) { return listResult; } const filteredTasks = listResult.data.filter(task => task.status === status); return { success: true, data: filteredTasks, metadata: { filePath: this.taskIndexFile, operation: 'get_tasks_by_status', timestamp: new Date() } }; } async getTasksByPriority(priority, projectId) { const listResult = await this.listTasks(projectId); if (!listResult.success) { return listResult; } const filteredTasks = listResult.data.filter(task => task.priority === priority); return { success: true, data: filteredTasks, metadata: { filePath: this.taskIndexFile, operation: 'get_tasks_by_priority', timestamp: new Date() } }; } async searchTasks(query, projectId) { const listResult = await this.listTasks(projectId); if (!listResult.success) { return listResult; } const searchTerm = query.toLowerCase(); const filteredTasks = listResult.data.filter(task => task.title.toLowerCase().includes(searchTerm) || task.description.toLowerCase().includes(searchTerm) || task.metadata.tags.some(tag => tag.toLowerCase().includes(searchTerm))); return { success: true, data: filteredTasks, metadata: { filePath: this.taskIndexFile, operation: 'search_tasks', timestamp: new Date() } }; } async taskExists(taskId) { const taskFilePath = this.getTaskFilePath(taskId); return await FileUtils.fileExists(taskFilePath); } async createEpic(epic) { try { logger.info({ epicId: epic.id, title: epic.title }, 'Creating epic'); const validationResult = this.validateEpic(epic); if (!validationResult.valid) { return { success: false, error: `Epic validation failed: ${validationResult.errors.join(', ')}`, metadata: { filePath: this.getEpicFilePath(epic.id), operation: 'create_epic', timestamp: new Date() } }; } const epicExists = await this.epicExists(epic.id); if (epicExists) { return { success: false, error: `Epic ${epic.id} already exists`, metadata: { filePath: this.getEpicFilePath(epic.id), operation: 'create_epic', timestamp: new Date() } }; } const epicFilePath = this.getEpicFilePath(epic.id); const saveResult = await FileUtils.writeYamlFile(epicFilePath, epic); if (!saveResult.success) { return { success: false, error: `Failed to save epic: ${saveResult.error}`, metadata: saveResult.metadata }; } await this.updateEpicIndex(epic.id, 'create'); logger.info({ epicId: epic.id }, 'Epic created successfully'); return { success: true, data: epic, metadata: { filePath: epicFilePath, operation: 'create_epic', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, epicId: epic.id }, 'Failed to create epic'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getEpicFilePath(epic.id), operation: 'create_epic', timestamp: new Date() } }; } } async getEpic(epicId) { try { logger.debug({ epicId }, 'Getting epic'); const epicFilePath = this.getEpicFilePath(epicId); const readResult = await FileUtils.readYamlFile(epicFilePath); if (!readResult.success) { return { success: false, error: `Epic ${epicId} not found: ${readResult.error}`, metadata: { filePath: epicFilePath, operation: 'get_epic', timestamp: new Date() } }; } const epic = readResult.data; const validationResult = this.validateEpic(epic); if (!validationResult.valid) { logger.warn({ epicId, errors: validationResult.errors }, 'Epic data validation failed'); return { success: false, error: `Epic data is corrupted: ${validationResult.errors.join(', ')}`, metadata: { filePath: epicFilePath, operation: 'get_epic', timestamp: new Date() } }; } return { success: true, data: epic, metadata: { filePath: epicFilePath, operation: 'get_epic', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, epicId }, 'Failed to get epic'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getEpicFilePath(epicId), operation: 'get_epic', timestamp: new Date() } }; } } async updateEpic(epicId, updates) { try { logger.info({ epicId, updates: Object.keys(updates) }, 'Updating epic'); const existingResult = await this.getEpic(epicId); if (!existingResult.success) { return { success: false, error: `Epic not found: ${existingResult.error}`, metadata: existingResult.metadata }; } const existingEpic = existingResult.data; const updatedEpic = { ...existingEpic, ...updates, id: epicId, metadata: { ...existingEpic.metadata, ...updates.metadata, updatedAt: new Date() } }; const validationResult = this.validateEpic(updatedEpic); if (!validationResult.valid) { return { success: false, error: `Epic update validation failed: ${validationResult.errors.join(', ')}`, metadata: { filePath: this.getEpicFilePath(epicId), operation: 'update_epic', timestamp: new Date() } }; } const epicFilePath = this.getEpicFilePath(epicId); const saveResult = await FileUtils.writeYamlFile(epicFilePath, updatedEpic); if (!saveResult.success) { return { success: false, error: `Failed to save updated epic: ${saveResult.error}`, metadata: saveResult.metadata }; } logger.info({ epicId }, 'Epic updated successfully'); return { success: true, data: updatedEpic, metadata: { filePath: epicFilePath, operation: 'update_epic', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, epicId }, 'Failed to update epic'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getEpicFilePath(epicId), operation: 'update_epic', timestamp: new Date() } }; } } async deleteEpic(epicId) { try { logger.info({ epicId }, 'Deleting epic'); const epicExists = await this.epicExists(epicId); if (!epicExists) { return { success: false, error: `Epic ${epicId} not found`, metadata: { filePath: this.getEpicFilePath(epicId), operation: 'delete_epic', timestamp: new Date() } }; } const epicResult = await this.getEpic(epicId); if (epicResult.success && epicResult.data.taskIds.length > 0) { return { success: false, error: `Cannot delete epic ${epicId}: it has ${epicResult.data.taskIds.length} associated tasks`, metadata: { filePath: this.getEpicFilePath(epicId), operation: 'delete_epic', timestamp: new Date() } }; } const epicFilePath = this.getEpicFilePath(epicId); const deleteResult = await FileUtils.deleteFile(epicFilePath); if (!deleteResult.success) { return { success: false, error: `Failed to delete epic file: ${deleteResult.error}`, metadata: deleteResult.metadata }; } await this.updateEpicIndex(epicId, 'delete'); logger.info({ epicId }, 'Epic deleted successfully'); return { success: true, metadata: { filePath: epicFilePath, operation: 'delete_epic', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, epicId }, 'Failed to delete epic'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.getEpicFilePath(epicId), operation: 'delete_epic', timestamp: new Date() } }; } } async listEpics(projectId) { try { logger.debug({ projectId }, 'Listing epics'); const indexResult = await FileUtils.readJsonFile(this.epicIndexFile); if (!indexResult.success) { return { success: false, error: `Failed to read epic index: ${indexResult.error}`, metadata: { filePath: this.epicIndexFile, operation: 'list_epics', timestamp: new Date() } }; } if (!isEpicIndex(indexResult.data)) { return { success: false, error: 'Invalid epic index format', metadata: { filePath: this.epicIndexFile, operation: 'list_epics', timestamp: new Date() } }; } const epicIndex = indexResult.data; const epicIds = epicIndex.epics; const epics = []; for (const epicId of epicIds) { const epicResult = await this.getEpic(epicId); if (epicResult.success) { const epic = epicResult.data; if (!projectId || epic.projectId === projectId) { epics.push(epic); } } else { logger.warn({ epicId }, 'Failed to load epic from index'); } } return { success: true, data: epics, metadata: { filePath: this.epicIndexFile, operation: 'list_epics', timestamp: new Date() } }; } catch (error) { logger.error({ err: error, projectId }, 'Failed to list epics'); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.epicIndexFile, operation: 'list_epics', timestamp: new Date() } }; } } async epicExists(epicId) { const epicFilePath = this.getEpicFilePath(epicId); return await FileUtils.fileExists(epicFilePath); } getTaskFilePath(taskId) { return path.join(this.tasksDirectory, `${taskId}.yaml`); } getEpicFilePath(epicId) { return path.join(this.epicsDirectory, `${epicId}.yaml`); } validateTask(task) { const errors = []; if (!task.id || typeof task.id !== 'string') { errors.push('Task ID is required and must be a string'); } if (!task.title || typeof task.title !== 'string') { errors.push('Task title is required and must be a string'); } if (!task.description || typeof task.description !== 'string') { errors.push('Task description is required and must be a string'); } if (!['pending', 'in_progress', 'completed', 'blocked', 'cancelled'].includes(task.status)) { errors.push('Task status must be one of: pending, in_progress, completed, blocked, cancelled'); } if (!['low', 'medium', 'high', 'critical'].includes(task.priority)) { errors.push('Task priority must be one of: low, medium, high, critical'); } if (!task.projectId || typeof task.projectId !== 'string') { errors.push('Project ID is required and must be a string'); } if (!task.epicId || typeof task.epicId !== 'string') { errors.push('Epic ID is required and must be a string'); } if (typeof task.estimatedHours !== 'number' || task.estimatedHours < 0) { errors.push('Estimated hours must be a non-negative number'); } return { valid: errors.length === 0, errors }; } async loadTaskIndex() { if (!await FileUtils.fileExists(this.taskIndexFile)) { const initResult = await this.initialize(); if (!initResult.success) { return initResult; } } const result = await FileUtils.readJsonFile(this.taskIndexFile); if (!result.success) { return result; } if (!isTaskIndex(result.data)) { return { success: false, error: 'Invalid task index format', metadata: result.metadata }; } return { success: true, data: result.data, metadata: result.metadata }; } async updateTaskIndex(operation, taskId, taskInfo) { try { const indexResult = await this.loadTaskIndex(); if (!indexResult.success) { return indexResult; } const index = indexResult.data; switch (operation) { case 'add': if (!index.tasks.find(t => t.id === taskId) && taskInfo) { index.tasks.push(taskInfo); } break; case 'update': { const updateIndex = index.tasks.findIndex(t => t.id === taskId); if (updateIndex !== -1 && taskInfo) { index.tasks[updateIndex] = taskInfo; } break; } case 'remove': index.tasks = index.tasks.filter(t => t.id !== taskId); break; } index.lastUpdated = new Date().toISOString(); return await FileUtils.writeJsonFile(this.taskIndexFile, index); } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { filePath: this.taskIndexFile, operation: 'update_task_index', timestamp: new Date() } }; } } validateEpic(epic) { const errors = []; if (!epic.id || typeof epic.id !== 'string') { errors.push('Epic ID is required and must be a string'); } if (!epic.title || typeof epic.title !== 'string' || epic.title.trim().length === 0) { errors.push('Epic title is required and must be a non-empty string'); } if (epic.title && epic.title.length > 200) { errors.push('Epic title must be 200 characters or less'); } if (!epic.description || typeof epic.description !== 'string' || epic.description.trim().length === 0) { errors.push('Epic description is required and must be a non-empty string'); } if (!['pending', 'in_progress', 'completed', 'blocked', 'cancelled'].includes(epic.status)) { errors.push('Epic status must be one of: pending, in_progress, completed, blocked, cancelled'); } if (!['low', 'medium', 'high', 'critical'].includes(epic.priority)) { errors.push('Epic priority must be one of: low, medium, high, critical'); } if (!epic.projectId || typeof epic.projectId !== 'string') { errors.push('Project ID is required and must be a string'); } if (typeof epic.estimatedHours !== 'number' || epic.estimatedHours < 0) { errors.push('Estimated hours must be a non-negative number'); } if (!Array.isArray(epic.taskIds)) { errors.push('Task IDs must be an array'); } if (!Array.isArray(epic.dependencies)) { errors.push('Dependencies must be an array'); } if (!Array.isArray(epic.dependents)) { errors.push('Dependents must be an array'); } if (!epic.metadata || typeof epic.metadata !== 'object') { errors.push('Metadata is required and must be an object'); } else { if (!epic.metadata.createdAt || !(epic.metadata.createdAt instanceof Date)) { errors.push('Metadata must include a valid createdAt date'); } if (!epic.metadata.updatedAt || !(epic.metadata.updatedAt instanceof Date)) { errors.push('Metadata must include a valid updatedAt date'); } if (!epic.metadata.createdBy || typeof epic.metadata.createdBy !== 'string') { errors.push('Metadata must include createdBy as a string'); } if (!Array.isArray(epic.metadata.tags)) { errors.push('Metadata tags must be an array'); } } return { valid: errors.length === 0, errors }; } async updateEpicIndex(epicId, operation) { try { const indexResult = await FileUtils.readJsonFile(this.epicIndexFile); const epicIndexData = indexResult.success ? indexResult.data : { epics: [], lastUpdated: new Date().toISOString(), version: '1.0.0' }; const epicIndex = isEpicIndex(epicIndexData) ? epicIndexData : { epics: [], lastUpdated: new Date().toISOString(), version: '1.0.0' }; if (operation === 'create') { if (!epicIndex.epics.includes(epicId)) { epicIndex.epics.push(epicId); } } else if (operation === 'delete') { epicIndex.epics = epicIndex.epics.filter(id => id !== epicId); } epicIndex.lastUpdated = new Date().toISOString(); await FileUtils.writeJsonFile(this.epicIndexFile, epicIndex); } catch (error) { logger.error({ err: error, epicId, operation }, 'Failed to update epic index'); } } }