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.

491 lines (490 loc) 23.2 kB
import { Command } from 'commander'; import { DecompositionService } from '../../services/decomposition-service.js'; import { getTaskOperations } from '../../core/operations/task-operations.js'; import { getProjectOperations } from '../../core/operations/project-operations.js'; import { ProjectAnalyzer } from '../../utils/project-analyzer.js'; import { CLIUtils } from './index.js'; import { ValidationError } from '../../../../utils/errors.js'; import { OpenRouterConfigManager } from '../../../../utils/openrouter-config-manager.js'; import logger from '../../../../logger.js'; async function resolveEpicIdForTask(partialTask) { try { if (partialTask.epicId && partialTask.epicId !== 'default-epic') { return partialTask.epicId; } const { getEpicContextResolver } = await import('../../services/epic-context-resolver.js'); const contextResolver = getEpicContextResolver(); const taskContext = partialTask.title && partialTask.description ? { title: partialTask.title, description: partialTask.description, type: partialTask.type || 'development', tags: partialTask.tags || [] } : undefined; const resolverParams = { projectId: partialTask.projectId || 'default-project', taskContext }; const contextResult = await contextResolver.resolveEpicContext(resolverParams); return contextResult.epicId; } catch (error) { logger.warn({ err: error, partialTask }, 'Failed to resolve epic ID for task, using fallback'); return `${partialTask.projectId || 'default-project'}-main-epic`; } } async function resolveEpicIdForProject(projectId, projectName) { try { const { getEpicContextResolver } = await import('../../services/epic-context-resolver.js'); const contextResolver = getEpicContextResolver(); const taskContext = { title: `Complete ${projectName}`, description: `Project implementation for ${projectName}`, type: 'development', tags: ['project-decomposition'] }; const resolverParams = { projectId, taskContext }; const contextResult = await contextResolver.resolveEpicContext(resolverParams); return contextResult.epicId; } catch (error) { logger.warn({ err: error, projectId, projectName }, 'Failed to resolve epic ID for project, using fallback'); return `${projectId}-main-epic`; } } async function createCompleteAtomicTask(partialTask) { const now = new Date(); return { id: partialTask.id, title: partialTask.title, description: partialTask.description, status: partialTask.status || 'pending', priority: partialTask.priority || 'medium', type: partialTask.type || 'development', functionalArea: partialTask.functionalArea || 'data-management', estimatedHours: partialTask.estimatedHours || 4, actualHours: partialTask.actualHours, epicId: await resolveEpicIdForTask(partialTask), projectId: partialTask.projectId || 'default-project', dependencies: partialTask.dependencies || [], dependents: partialTask.dependents || [], filePaths: partialTask.filePaths || [], acceptanceCriteria: partialTask.acceptanceCriteria || [], testingRequirements: partialTask.testingRequirements || { unitTests: [], integrationTests: [], performanceTests: [], coverageTarget: 80 }, performanceCriteria: partialTask.performanceCriteria || {}, qualityCriteria: partialTask.qualityCriteria || { codeQuality: [], documentation: [], typeScript: true, eslint: true }, integrationCriteria: partialTask.integrationCriteria || { compatibility: [], patterns: [] }, validationMethods: partialTask.validationMethods || { automated: [], manual: [] }, assignedAgent: partialTask.assignedAgent, executionContext: partialTask.executionContext, createdAt: partialTask.createdAt || now, updatedAt: partialTask.updatedAt || now, startedAt: partialTask.startedAt, completedAt: partialTask.completedAt, createdBy: partialTask.createdBy || 'system', tags: partialTask.tags || [], metadata: partialTask.metadata || { createdAt: now, updatedAt: now, createdBy: 'system', tags: [] } }; } export function createDecomposeCommand() { const decomposeCmd = new Command('decompose'); decomposeCmd .description('Decompose tasks or projects into atomic components') .configureOutput({ writeOut: (str) => process.stdout.write(str), writeErr: (str) => process.stderr.write(str) }); decomposeCmd.addCommand(createTaskDecomposeCommand()); decomposeCmd.addCommand(createProjectDecomposeCommand()); return decomposeCmd; } function createTaskDecomposeCommand() { return new Command('task') .description('Decompose a task into atomic subtasks') .argument('<taskId>', 'Task ID to decompose') .option('-d, --description <description>', 'Additional context for decomposition') .option('-f, --force', 'Force decomposition even if task appears atomic') .option('--max-depth <depth>', 'Maximum decomposition depth', '3') .option('--min-hours <hours>', 'Minimum hours for atomic tasks', '0.5') .option('--max-hours <hours>', 'Maximum hours for atomic tasks', '8') .option('--format <format>', 'Output format (table|json|yaml)', 'table') .action(async (taskId, options) => { try { logger.info({ command: 'decompose task', taskId, options }, 'Starting task decomposition'); const taskOperations = getTaskOperations(); const taskResult = await taskOperations.getTask(taskId); if (!taskResult.success) { CLIUtils.error(`Task not found: ${taskResult.error}`); return; } const task = taskResult.data; CLIUtils.info(`Decomposing task: ${task.title}`); const configManager = OpenRouterConfigManager.getInstance(); await configManager.initialize(); const openRouterConfig = await configManager.getOpenRouterConfig(); const decompositionService = new DecompositionService(openRouterConfig); const projectAnalyzer = ProjectAnalyzer.getInstance(); const projectPath = process.cwd(); let languages; let frameworks; let tools; try { languages = await projectAnalyzer.detectProjectLanguages(projectPath); } catch (error) { logger.warn({ error, projectPath }, 'Language detection failed in CLI, using fallback'); languages = ['typescript', 'javascript']; } try { frameworks = await projectAnalyzer.detectProjectFrameworks(projectPath); } catch (error) { logger.warn({ error, projectPath }, 'Framework detection failed in CLI, using fallback'); frameworks = ['node.js']; } try { tools = await projectAnalyzer.detectProjectTools(projectPath); } catch (error) { logger.warn({ error, projectPath }, 'Tools detection failed in CLI, using fallback'); tools = ['vscode', 'git']; } const decompositionRequest = { task: await createCompleteAtomicTask({ id: task.id, title: task.title, description: options.description || task.description, type: task.type, priority: task.priority, estimatedHours: task.estimatedHours, acceptanceCriteria: task.acceptanceCriteria, tags: task.tags, filePaths: task.filePaths || [], projectId: task.projectId, epicId: task.epicId, status: task.status, createdBy: task.createdBy, createdAt: task.createdAt, updatedAt: task.updatedAt }), context: { projectId: task.projectId, projectPath: process.cwd(), projectName: task.projectId, description: `CLI task decomposition for ${task.title}`, languages, frameworks, buildTools: [], tools, configFiles: [], entryPoints: [], architecturalPatterns: [], existingTasks: [], codebaseSize: 'medium', teamSize: 1, complexity: 'medium', codebaseContext: { relevantFiles: [], contextSummary: `CLI task decomposition context for ${task.title}`, gatheringMetrics: { searchTime: 0, readTime: 0, scoringTime: 0, totalTime: 0, cacheHitRate: 0 }, totalContextSize: 0, averageRelevance: 0 }, structure: { sourceDirectories: ['src'], testDirectories: ['test', 'tests', '__tests__'], docDirectories: ['docs', 'documentation'], buildDirectories: ['dist', 'build', 'lib'] }, dependencies: { production: [], development: [], external: [] }, metadata: { createdAt: new Date(), updatedAt: new Date(), version: '1.0.0', source: 'manual' } }, sessionId: `cli-decompose-${Date.now()}`, options: { maxDepth: parseInt(options.maxDepth, 10), minHours: parseFloat(options.minHours), maxHours: parseFloat(options.maxHours), forceDecomposition: options.force } }; const session = await decompositionService.startDecomposition(decompositionRequest); CLIUtils.info(`Decomposition session started: ${session.id}`); CLIUtils.info('Analyzing task complexity and gathering context...'); const timeout = 30000; const startTime = Date.now(); while ((session.status === 'pending' || session.status === 'in_progress') && (Date.now() - startTime) < timeout) { await new Promise(resolve => setTimeout(resolve, 1000)); CLIUtils.info('Decomposition in progress...'); const updatedSession = decompositionService.getSession(session.id); if (updatedSession) { Object.assign(session, updatedSession); } } if (session.status === 'pending' || session.status === 'in_progress') { CLIUtils.warning('Decomposition is taking longer than expected. Check logs for progress.'); return; } if (session.status === 'failed') { CLIUtils.error(`Decomposition failed: ${session.error || 'Unknown error'}`); return; } if (session.results && session.results.length > 0 && session.results[0].subTasks.length > 0) { CLIUtils.success(`Task decomposed into ${session.results[0].subTasks.length} atomic tasks`); const displayData = session.results[0].subTasks.map((atomicTask, index) => ({ '#': index + 1, 'Task ID': atomicTask.id, 'Title': CLIUtils.truncate(atomicTask.title, 40), 'Type': atomicTask.type, 'Priority': atomicTask.priority, 'Hours': atomicTask.estimatedHours, 'Files': atomicTask.filePaths?.length || 0 })); console.log('\n' + CLIUtils.formatOutput(displayData, options.format)); const totalHours = session.results[0].subTasks.reduce((sum, task) => sum + task.estimatedHours, 0); CLIUtils.info(`Total estimated hours: ${totalHours}`); CLIUtils.info(`Average task size: ${(totalHours / session.results[0].subTasks.length).toFixed(1)} hours`); } else { CLIUtils.info('Task is already atomic - no decomposition needed'); } } catch (error) { logger.error({ err: error, taskId, options }, 'Task decomposition failed'); if (error instanceof ValidationError) { CLIUtils.error(error.message); } else { CLIUtils.error('Failed to decompose task. Check logs for details.'); } } }); } function createProjectDecomposeCommand() { return new Command('project') .description('Decompose a project into epics and tasks') .argument('<projectId>', 'Project ID or name to decompose') .option('-d, --description <description>', 'Additional project context') .option('--scope <scope>', 'Decomposition scope (full|incremental)', 'full') .option('--epic-size <hours>', 'Target epic size in hours', '40') .option('--task-size <hours>', 'Target task size in hours', '4') .option('--format <format>', 'Output format (table|json|yaml)', 'table') .action(async (projectId, options) => { try { logger.info({ command: 'decompose project', projectId, options }, 'Starting project decomposition'); const projectOperations = getProjectOperations(); const projectResult = await projectOperations.getProject(projectId); if (!projectResult.success) { CLIUtils.error(`Project not found: ${projectResult.error}`); return; } const project = projectResult.data; CLIUtils.info(`Decomposing project: ${project.name}`); const configManager = OpenRouterConfigManager.getInstance(); await configManager.initialize(); const openRouterConfig = await configManager.getOpenRouterConfig(); const decompositionService = new DecompositionService(openRouterConfig); const projectTask = await createCompleteAtomicTask({ id: `project-${project.id}`, title: `Complete ${project.name}`, description: options.description || project.description, type: 'development', priority: 'high', estimatedHours: parseFloat(options.epicSize) * 3, acceptanceCriteria: [`Project ${project.name} should be fully implemented and tested`], tags: ['project-decomposition', ...project.metadata.tags], filePaths: [], projectId: project.id, epicId: await resolveEpicIdForProject(project.id, project.name), createdBy: 'system' }); const projectAnalyzer = ProjectAnalyzer.getInstance(); const projectPath = process.cwd(); let languages; let frameworks; let tools; try { languages = project.techStack.languages?.length ? project.techStack.languages : await projectAnalyzer.detectProjectLanguages(projectPath); } catch (error) { logger.warn({ error, projectPath }, 'Language detection failed for CLI project, using fallback'); languages = ['typescript']; } try { frameworks = project.techStack.frameworks?.length ? project.techStack.frameworks : await projectAnalyzer.detectProjectFrameworks(projectPath); } catch (error) { logger.warn({ error, projectPath }, 'Framework detection failed for CLI project, using fallback'); frameworks = ['node.js']; } try { tools = project.techStack.tools?.length ? project.techStack.tools : await projectAnalyzer.detectProjectTools(projectPath); } catch (error) { logger.warn({ error, projectPath }, 'Tools detection failed for CLI project, using fallback'); tools = ['vscode', 'git']; } const decompositionRequest = { task: projectTask, context: { projectId: project.id, projectPath: process.cwd(), projectName: project.name, description: options.description || project.description, languages, frameworks, buildTools: [], tools, configFiles: [], entryPoints: [], architecturalPatterns: [], existingTasks: [], codebaseSize: 'large', teamSize: 1, complexity: 'high', codebaseContext: { relevantFiles: [], contextSummary: options.description || project.description, gatheringMetrics: { searchTime: 0, readTime: 0, scoringTime: 0, totalTime: 0, cacheHitRate: 0 }, totalContextSize: 0, averageRelevance: 0 }, structure: { sourceDirectories: ['src'], testDirectories: ['test', 'tests', '__tests__'], docDirectories: ['docs', 'documentation'], buildDirectories: ['dist', 'build', 'lib'] }, dependencies: { production: [], development: [], external: [] }, metadata: { createdAt: new Date(), updatedAt: new Date(), version: '1.0.0', source: 'manual' } }, sessionId: `cli-project-decompose-${Date.now()}`, options: { maxDepth: 2, minHours: 1, maxHours: parseFloat(options.taskSize), forceDecomposition: true } }; const session = await decompositionService.startDecomposition(decompositionRequest); CLIUtils.info(`Project decomposition session started: ${session.id}`); CLIUtils.info('Analyzing project scope and creating breakdown...'); const timeout = 60000; const startTime = Date.now(); while ((session.status === 'pending' || session.status === 'in_progress') && (Date.now() - startTime) < timeout) { await new Promise(resolve => setTimeout(resolve, 2000)); CLIUtils.info('Project decomposition in progress...'); const updatedSession = decompositionService.getSession(session.id); if (updatedSession) { Object.assign(session, updatedSession); } } if (session.status === 'pending' || session.status === 'in_progress') { CLIUtils.warning('Project decomposition is taking longer than expected. Check logs for progress.'); return; } if (session.status === 'failed') { CLIUtils.error(`Project decomposition failed: ${session.error || 'Unknown error'}`); return; } if (session.results && session.results.length > 0 && session.results[0].subTasks.length > 0) { CLIUtils.success(`Project decomposed into ${session.results[0].subTasks.length} tasks`); const displayData = session.results[0].subTasks.map((task, index) => ({ '#': index + 1, 'Task ID': task.id, 'Title': CLIUtils.truncate(task.title, 50), 'Type': task.type, 'Priority': task.priority, 'Hours': task.estimatedHours, 'Epic': task.tags.find(tag => tag.startsWith('epic:')) || 'General' })); console.log('\n' + CLIUtils.formatOutput(displayData, options.format)); const totalHours = session.results[0].subTasks.reduce((sum, task) => sum + task.estimatedHours, 0); const tasksByType = session.results[0].subTasks.reduce((acc, task) => { acc[task.type] = (acc[task.type] || 0) + 1; return acc; }, {}); CLIUtils.info(`Total estimated hours: ${totalHours}`); CLIUtils.info(`Task breakdown: ${Object.entries(tasksByType).map(([type, count]) => `${type}: ${count}`).join(', ')}`); } else { CLIUtils.warning('No tasks generated from project decomposition'); } } catch (error) { logger.error({ err: error, projectId, options }, 'Project decomposition failed'); if (error instanceof ValidationError) { CLIUtils.error(error.message); } else { CLIUtils.error('Failed to decompose project. Check logs for details.'); } } }); } export const decomposeCommand = createDecomposeCommand(); decomposeCommand.addHelpText('after', ` Examples: $ vibe-tasks decompose task T001 --description "Focus on authentication flow" $ vibe-tasks decompose task T001 --force --max-hours 6 $ vibe-tasks decompose project PID-WEBAPP-001 --scope incremental $ vibe-tasks decompose project "My Web App" --epic-size 60 --task-size 8 `);