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.

745 lines (744 loc) 33.5 kB
import fs from 'fs/promises'; import path from 'path'; import logger from '../../../logger.js'; import { UnifiedSecurityEngine, createDefaultSecurityConfig } from '../core/unified-security-engine.js'; export class TaskListIntegrationService { static instance; config; taskListCache = new Map(); performanceMetrics = new Map(); securityEngine = null; constructor() { this.config = { maxAge: 24 * 60 * 60 * 1000, enableCaching: true, maxCacheSize: 50, enablePerformanceMonitoring: true }; logger.debug('Task List integration service initialized'); } async getSecurityEngine() { if (!this.securityEngine) { const config = createDefaultSecurityConfig(); this.securityEngine = UnifiedSecurityEngine.getInstance(config); await this.securityEngine.initialize(); } return this.securityEngine; } static getInstance() { if (!TaskListIntegrationService.instance) { TaskListIntegrationService.instance = new TaskListIntegrationService(); } return TaskListIntegrationService.instance; } async parseTaskList(taskListFilePath) { const startTime = Date.now(); try { logger.info({ taskListFilePath }, 'Starting task list parsing'); await this.validateTaskListPath(taskListFilePath); const taskListContent = await fs.readFile(taskListFilePath, 'utf-8'); const taskListData = await this.parseTaskListContent(taskListContent, taskListFilePath); const parsingTime = Date.now() - startTime; if (this.config.enableCaching) { await this.updateTaskListCache(taskListFilePath); } logger.info({ taskListFilePath, parsingTime, taskCount: taskListData.statistics.totalEstimatedHours }, 'Task list parsing completed successfully'); return { success: true, taskListData, parsingTime }; } catch (error) { const parsingTime = Date.now() - startTime; logger.error({ err: error, taskListFilePath }, 'Task list parsing failed with exception'); return { success: false, error: error instanceof Error ? error.message : String(error), parsingTime }; } } async detectExistingTaskList(projectPath) { try { if (this.config.enableCaching && projectPath && this.taskListCache.has(projectPath)) { const cached = this.taskListCache.get(projectPath); try { await fs.access(cached.filePath); return cached; } catch { this.taskListCache.delete(projectPath); } } const taskListFiles = await this.findTaskListFiles(projectPath); if (taskListFiles.length === 0) { return null; } const mostRecent = taskListFiles.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]; if (this.config.enableCaching && projectPath) { this.taskListCache.set(projectPath, mostRecent); } return mostRecent; } catch (error) { logger.warn({ err: error, projectPath }, 'Failed to detect existing task list'); return null; } } async validateTaskListPath(taskListFilePath) { try { const securityEngine = await this.getSecurityEngine(); const validationResponse = await securityEngine.validatePath(taskListFilePath, 'read'); if (!validationResponse.success) { throw new Error(`Security validation failed: ${validationResponse.error?.message || 'Unknown error'}`); } const validationResult = validationResponse.data; if (!validationResult.isValid) { throw new Error(`Security validation failed: ${validationResult.error || 'Path validation failed'}`); } if (validationResult.warnings && validationResult.warnings.length > 0) { logger.warn({ taskListFilePath, warnings: validationResult.warnings }, 'Task list path validation warnings'); } if (!taskListFilePath.endsWith('.md')) { throw new Error('Task list file must be a Markdown file (.md)'); } } catch (error) { logger.error({ err: error, taskListFilePath }, 'Task list path validation failed'); throw new Error(`Invalid task list file path: ${error instanceof Error ? error.message : String(error)}`); } } async updateTaskListCache(taskListFilePath) { try { const stats = await fs.stat(taskListFilePath); const fileName = path.basename(taskListFilePath); const { projectName, createdAt, listType } = this.extractTaskListMetadataFromFilename(fileName); const taskListInfo = { filePath: taskListFilePath, fileName, createdAt, projectName, fileSize: stats.size, isAccessible: true, lastModified: stats.mtime, listType }; this.taskListCache.set(projectName, taskListInfo); if (this.taskListCache.size > this.config.maxCacheSize) { const oldestKey = this.taskListCache.keys().next().value; if (oldestKey) { this.taskListCache.delete(oldestKey); } } } catch (error) { logger.warn({ err: error, taskListFilePath }, 'Failed to update task list cache'); } } extractTaskListMetadataFromFilename(fileName) { const match = fileName.match(/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)-(.+)-task-list-(.+)\.md$/); if (match) { const [, timestamp, projectSlug, listType] = match; const createdAt = new Date(timestamp.replace(/-/g, ':').replace(/T(\d{2}):(\d{2}):(\d{2}):(\d{3})Z/, 'T$1:$2:$3.$4Z')); const projectName = projectSlug.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); return { projectName, createdAt, listType }; } return { projectName: fileName.replace(/-task-list.*\.md$/, '').replace(/-/g, ' '), createdAt: new Date(), listType: 'detailed' }; } async findTaskListFiles(projectPath) { try { const outputBaseDir = process.env.VIBE_CODER_OUTPUT_DIR || path.join(process.cwd(), 'VibeCoderOutput'); const taskListOutputDir = path.join(outputBaseDir, 'generated_task_lists'); try { await fs.access(taskListOutputDir); } catch { return []; } const files = await fs.readdir(taskListOutputDir, { withFileTypes: true }); const taskListFiles = []; for (const file of files) { if (file.isFile() && file.name.endsWith('-task-list-detailed.md')) { const filePath = path.join(taskListOutputDir, file.name); try { const stats = await fs.stat(filePath); const { projectName, createdAt, listType } = this.extractTaskListMetadataFromFilename(file.name); if (projectPath) { const expectedProjectName = path.basename(projectPath).toLowerCase(); if (!projectName.toLowerCase().includes(expectedProjectName)) { continue; } } taskListFiles.push({ filePath, fileName: file.name, createdAt, projectName, fileSize: stats.size, isAccessible: true, lastModified: stats.mtime, listType }); } catch (error) { logger.warn({ err: error, fileName: file.name }, 'Failed to process task list file'); const { projectName, createdAt, listType } = this.extractTaskListMetadataFromFilename(file.name); taskListFiles.push({ filePath: path.join(taskListOutputDir, file.name), fileName: file.name, createdAt, projectName, fileSize: 0, isAccessible: false, lastModified: new Date(), listType }); } } } return taskListFiles; } catch (error) { logger.error({ err: error, projectPath }, 'Failed to find task list files'); return []; } } async parseTaskListContent(content, filePath) { const startTime = Date.now(); try { const securityEngine = await this.getSecurityEngine(); const validationResponse = await securityEngine.validatePath(filePath, 'read'); if (!validationResponse.success) { throw new Error(`Security validation failed: ${validationResponse.error?.message || 'Unknown error'}`); } const validationResult = validationResponse.data; if (!validationResult.isValid) { throw new Error(`Security validation failed: ${validationResult.error || 'Path validation failed'}`); } const lines = content.split('\n'); const fileName = path.basename(filePath); const { projectName, createdAt, listType } = this.extractTaskListMetadataFromFilename(fileName); const stats = await fs.stat(validationResult.normalizedPath || filePath); const parsedTaskList = { metadata: { filePath, projectName, createdAt, fileSize: stats.size, totalTasks: 0, phaseCount: 0, listType }, overview: { description: '', goals: [], techStack: [] }, phases: [], statistics: { totalEstimatedHours: 0, tasksByPriority: {}, tasksByPhase: {} } }; let currentPhase = ''; let currentPhaseDescription = ''; let currentTask = null; let currentSubTask = null; let inTaskBlock = false; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('## Phase:') || (line.startsWith('## ') && this.isActualPhase(line))) { if (currentTask && currentTask.id && currentTask.title) { const phase = parsedTaskList.phases[parsedTaskList.phases.length - 1]; if (phase) { phase.tasks.push(currentTask); } currentTask = null; inTaskBlock = false; } if (currentPhase && parsedTaskList.phases.length > 0) { const lastPhase = parsedTaskList.phases[parsedTaskList.phases.length - 1]; lastPhase.description = currentPhaseDescription.trim(); } currentPhase = line.startsWith('## Phase:') ? line.substring(9).trim() : line.substring(3).trim(); currentPhaseDescription = ''; parsedTaskList.phases.push({ name: currentPhase, description: '', tasks: [], estimatedDuration: '0 hours' }); continue; } if (line.startsWith('- **ID:**')) { if (currentTask && currentTask.id && currentTask.title) { const phase = parsedTaskList.phases[parsedTaskList.phases.length - 1]; if (phase) { phase.tasks.push(currentTask); } } const idMatch = line.match(/- \*\*ID:\*\*\s*(T-\d+)/); if (idMatch) { currentTask = { id: idMatch[1], title: '', description: '', userStory: '', priority: 'medium', dependencies: [], estimatedEffort: '', phase: currentPhase, markdownContent: line, subTasks: [] }; inTaskBlock = true; currentSubTask = null; } continue; } if (line.startsWith(' - **Sub-Task ID:**') && currentTask) { const subTaskIdMatch = line.match(/\s*- \*\*Sub-Task ID:\*\*\s*(T-[\d.]+)/); if (subTaskIdMatch) { if (currentSubTask) { currentTask.subTasks = currentTask.subTasks || []; const taskListItem = { id: currentSubTask.id, title: currentSubTask.task || currentSubTask.goal || 'Untitled Sub-task', description: currentSubTask.rationale || currentSubTask.expectedOutcome || '', userStory: currentSubTask.objectives?.join('; ') || '', priority: 'medium', dependencies: [], estimatedEffort: '1-2 hours', phase: currentPhase, markdownContent: `Sub-task: ${currentSubTask.task || currentSubTask.goal || ''}` }; currentTask.subTasks.push(taskListItem); } currentSubTask = { id: subTaskIdMatch[1], goal: '', task: '', rationale: '', expectedOutcome: '', objectives: [], implementationPrompt: '', exampleCode: '' }; } continue; } if (line.includes('**ID:**') && line.includes('**Title:**') && !inTaskBlock) { const idMatch = line.match(/\*\*ID:\*\*\s*(T-\d+)/); const titleMatch = line.match(/\*\*Title:\*\*\s*([^*]+?)(?:\s*\*|$)/); if (idMatch) { if (currentTask && currentTask.id && currentTask.title) { const phase = parsedTaskList.phases[parsedTaskList.phases.length - 1]; if (phase) { phase.tasks.push(currentTask); } } currentTask = { id: idMatch[1], title: titleMatch ? titleMatch[1].trim() : '', description: '', userStory: '', priority: 'medium', dependencies: [], estimatedEffort: '', phase: currentPhase, markdownContent: line, subTasks: [] }; inTaskBlock = true; } continue; } if (currentTask && inTaskBlock && !currentSubTask) { if (line.includes('**Title:**')) { const titleMatch = line.match(/\*\*Title:\*\*\s*(.*)/); if (titleMatch) { currentTask.title = titleMatch[1].trim(); } } else if (line.includes('*(Description):*')) { const descMatch = line.match(/\*\(Description\):\*\s*(.*)/); if (descMatch) { currentTask.description = descMatch[1].trim(); } } else if (line.includes('*(User Story):*')) { const storyMatch = line.match(/\*\(User Story\):\*\s*(.*)/); if (storyMatch) { currentTask.userStory = storyMatch[1].trim(); } } else if (line.includes('*(Priority):*')) { const priorityMatch = line.match(/\*\(Priority\):\*\s*(.*)/); if (priorityMatch) { const priority = priorityMatch[1].trim().toLowerCase(); currentTask.priority = ['low', 'medium', 'high', 'critical'].includes(priority) ? priority : 'medium'; } } else if (line.includes('*(Dependencies):*')) { const depMatch = line.match(/\*\(Dependencies\):\*\s*(.*)/); if (depMatch) { const deps = depMatch[1].trim(); currentTask.dependencies = deps === 'None' ? [] : deps.split(',').map(d => d.trim()); } } else if (line.includes('*(Est. Effort):*')) { const effortMatch = line.match(/\*\(Est\. Effort\):\*\s*(.*)/); if (effortMatch) { currentTask.estimatedEffort = effortMatch[1].trim(); } } } if (currentSubTask && inTaskBlock) { if (line.includes('**Goal:**')) { const goalMatch = line.match(/\*\*Goal:\*\*\s*(.*)/); if (goalMatch) { currentSubTask.goal = goalMatch[1].trim(); } } else if (line.includes('**Task:**')) { const taskMatch = line.match(/\*\*Task:\*\*\s*(.*)/); if (taskMatch) { currentSubTask.task = taskMatch[1].trim(); } } else if (line.includes('**Rationale:**')) { const rationaleMatch = line.match(/\*\*Rationale:\*\*\s*(.*)/); if (rationaleMatch) { currentSubTask.rationale = rationaleMatch[1].trim(); } } else if (line.includes('**Expected Outcome:**')) { const outcomeMatch = line.match(/\*\*Expected Outcome:\*\*\s*(.*)/); if (outcomeMatch) { currentSubTask.expectedOutcome = outcomeMatch[1].trim(); } } else if (line.includes('**Implementation Prompt:**')) { const promptMatch = line.match(/\*\*Implementation Prompt:\*\*\s*(.*)/); if (promptMatch) { currentSubTask.implementationPrompt = promptMatch[1].trim(); } } else if (line.includes('**Objectives:**')) { currentSubTask.objectives = []; } else if (line.trim().startsWith('* ') && currentSubTask.objectives !== undefined) { const objective = line.trim().substring(2).trim(); if (objective) { currentSubTask.objectives.push(objective); } } } if (currentPhase && !line.startsWith('- **') && !line.startsWith('#') && line.length > 0 && !inTaskBlock) { currentPhaseDescription += line + ' '; } } if (currentSubTask && currentTask) { currentTask.subTasks = currentTask.subTasks || []; const taskListItem = { id: currentSubTask.id, title: currentSubTask.task || currentSubTask.goal || 'Untitled Sub-task', description: currentSubTask.rationale || currentSubTask.expectedOutcome || '', userStory: currentSubTask.objectives?.join('; ') || '', priority: 'medium', dependencies: [], estimatedEffort: '1-2 hours', phase: currentPhase, markdownContent: `Sub-task: ${currentSubTask.task || currentSubTask.goal || ''}` }; currentTask.subTasks.push(taskListItem); } if (currentTask && currentTask.id && currentTask.title) { const phase = parsedTaskList.phases[parsedTaskList.phases.length - 1]; if (phase) { phase.tasks.push(currentTask); } } this.calculateTaskListStatistics(parsedTaskList); if (this.config.enablePerformanceMonitoring) { const parsingTime = Date.now() - startTime; this.performanceMetrics.set(filePath, { parsingTime, fileSize: stats.size, taskCount: parsedTaskList.metadata.totalTasks, phaseCount: parsedTaskList.metadata.phaseCount }); } return parsedTaskList; } catch (error) { logger.error({ err: error, filePath }, 'Failed to parse task list content'); throw error; } } calculateTaskListStatistics(parsedTaskList) { let totalTasks = 0; let totalEstimatedHours = 0; const tasksByPriority = {}; const tasksByPhase = {}; for (const phase of parsedTaskList.phases) { tasksByPhase[phase.name] = phase.tasks.length; totalTasks += phase.tasks.length; for (const task of phase.tasks) { tasksByPriority[task.priority] = (tasksByPriority[task.priority] || 0) + 1; const hours = this.extractHoursFromEffort(task.estimatedEffort); totalEstimatedHours += hours; } } parsedTaskList.metadata.totalTasks = totalTasks; parsedTaskList.metadata.phaseCount = parsedTaskList.phases.length; parsedTaskList.statistics.totalEstimatedHours = totalEstimatedHours; parsedTaskList.statistics.tasksByPriority = tasksByPriority; parsedTaskList.statistics.tasksByPhase = tasksByPhase; } extractHoursFromEffort(effort) { const match = effort.match(/(\d+(?:\.\d+)?)\s*(?:hours?|hrs?|h)/i); return match ? parseFloat(match[1]) : 0; } isActualPhase(line) { const phaseKeywords = [ 'phase:', 'setup', 'planning', 'development', 'backend', 'frontend', 'testing', 'deployment', 'operations', 'maintenance' ]; const lineContent = line.toLowerCase(); return phaseKeywords.some(keyword => lineContent.includes(keyword)); } async convertToAtomicTasks(parsedTaskList, projectId, epicId, createdBy) { try { const atomicTasks = []; for (const phase of parsedTaskList.phases) { for (const taskItem of phase.tasks) { const atomicTask = { id: taskItem.id, title: taskItem.title, description: taskItem.description, status: 'pending', priority: taskItem.priority, type: this.inferTaskType(taskItem.title, taskItem.description), functionalArea: 'data-management', estimatedHours: this.extractHoursFromEffort(taskItem.estimatedEffort), epicId, projectId, dependencies: taskItem.dependencies, dependents: [], filePaths: this.inferFilePaths(taskItem.description), acceptanceCriteria: this.extractAcceptanceCriteria(taskItem.userStory), testingRequirements: { unitTests: [], integrationTests: [], performanceTests: [], coverageTarget: 80 }, performanceCriteria: { responseTime: '<200ms', memoryUsage: '<100MB', throughput: '>1000 req/s' }, qualityCriteria: { codeQuality: ['ESLint compliant', 'TypeScript strict mode'], documentation: ['JSDoc comments', 'README updates'], typeScript: true, eslint: true }, integrationCriteria: { compatibility: ['Existing API', 'Database schema'], patterns: ['Singleton pattern', 'Error handling'] }, validationMethods: { automated: ['Unit tests', 'Integration tests'], manual: ['Code review', 'Manual testing'] }, createdAt: new Date(), updatedAt: new Date(), createdBy, tags: [phase.name.toLowerCase(), taskItem.priority], metadata: { createdAt: new Date(), updatedAt: new Date(), createdBy, tags: [phase.name.toLowerCase(), taskItem.priority, 'imported-from-task-list'] } }; atomicTasks.push(atomicTask); } } logger.info({ taskListPath: parsedTaskList.metadata.filePath, atomicTaskCount: atomicTasks.length, projectId, epicId }, 'Successfully converted task list to atomic tasks'); return atomicTasks; } catch (error) { logger.error({ err: error, parsedTaskList: parsedTaskList.metadata }, 'Failed to convert task list to atomic tasks'); throw error; } } inferTaskType(title, description) { const content = (title + ' ' + description).toLowerCase(); if (content.includes('test') || content.includes('spec')) { return 'testing'; } else if (content.includes('doc') || content.includes('readme')) { return 'documentation'; } else if (content.includes('deploy') || content.includes('release')) { return 'deployment'; } else if (content.includes('research') || content.includes('investigate')) { return 'research'; } else if (content.includes('review') || content.includes('audit')) { return 'review'; } else { return 'development'; } } inferFilePaths(description) { const filePaths = []; const pathMatches = description.match(/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+\.[a-zA-Z]{2,4}/g); if (pathMatches) { filePaths.push(...pathMatches); } const componentMatches = description.match(/`([a-zA-Z0-9_.-]+\.[a-zA-Z]{2,4})`/g); if (componentMatches) { filePaths.push(...componentMatches.map(m => m.replace(/`/g, ''))); } return filePaths; } extractAcceptanceCriteria(userStory) { const criteria = []; const parts = userStory.split(/(?:so that|when|then|and|given)/i); for (const part of parts) { const trimmed = part.trim(); if (trimmed.length > 10 && !trimmed.toLowerCase().startsWith('as a')) { criteria.push(trimmed); } } return criteria.length > 0 ? criteria : [userStory]; } clearCache() { this.taskListCache.clear(); this.performanceMetrics.clear(); logger.info('Task list integration cache cleared'); } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; logger.debug({ config: this.config }, 'Task list integration configuration updated'); } getConfig() { return { ...this.config }; } getPerformanceMetrics() { return new Map(this.performanceMetrics); } async getTaskListMetadata(taskListFilePath) { try { const stats = await fs.stat(taskListFilePath); const fileName = path.basename(taskListFilePath); const { projectName, createdAt, listType } = this.extractTaskListMetadataFromFilename(fileName); const performanceMetrics = this.performanceMetrics.get(taskListFilePath) || { parsingTime: 0, fileSize: stats.size, taskCount: 0, phaseCount: 0 }; return { filePath: taskListFilePath, projectName, createdAt, fileSize: stats.size, totalTasks: performanceMetrics.taskCount, phaseCount: performanceMetrics.phaseCount, listType }; } catch (error) { logger.error({ err: error, taskListFilePath }, 'Failed to get task list metadata'); throw error; } } async validateTaskList(taskListFilePath) { try { const content = await fs.readFile(taskListFilePath, 'utf-8'); const errors = []; const warnings = []; if (content.length < 100) { errors.push('Task list content is too short'); } if (!content.includes('## ')) { errors.push('No phase headers found'); } if (!content.includes('- **ID:**')) { errors.push('No task items found'); } const phaseCount = (content.match(/## /g) || []).length; const taskCount = (content.match(/- \*\*ID:\*\*/g) || []).length; if (phaseCount === 0) { errors.push('No phases defined'); } if (taskCount === 0) { errors.push('No tasks defined'); } if (taskCount < phaseCount) { warnings.push('Some phases may not have tasks'); } let completenessScore = 1.0; if (errors.length > 0) { completenessScore -= errors.length * 0.2; } if (warnings.length > 0) { completenessScore -= warnings.length * 0.1; } completenessScore = Math.max(0, completenessScore); return { isValid: errors.length === 0, errors, warnings, completenessScore, validatedAt: new Date() }; } catch (error) { return { isValid: false, errors: [`Failed to validate task list: ${error instanceof Error ? error.message : String(error)}`], warnings: [], completenessScore: 0, validatedAt: new Date() }; } } }