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.

455 lines (439 loc) 26.6 kB
import fs from 'fs-extra'; import path from 'path'; import { z } from 'zod'; import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { performFormatAwareLlmCallWithCentralizedConfig } from '../../utils/llmHelper.js'; import { performResearchQuery } from '../../utils/researchHelper.js'; import logger from '../../logger.js'; import { registerTool } from '../../services/routing/toolRegistry.js'; import { AppError, ToolExecutionError } from '../../utils/errors.js'; import { jobManager, JobStatus } from '../../services/job-manager/index.js'; import { sseNotifier } from '../../services/sse-notifier/index.js'; import { formatBackgroundJobInitiationResponse } from '../../services/job-response-formatter/index.js'; import { getToolOutputDirectory, ensureToolOutputDirectory } from '../vibe-task-manager/security/unified-security-config.js'; const TASK_LIST_DIR_NAME = 'generated_task_lists'; function getBaseOutputDir() { try { return getToolOutputDirectory(); } catch { return process.env.VIBE_CODER_OUTPUT_DIR ? path.resolve(process.env.VIBE_CODER_OUTPUT_DIR) : path.join(process.cwd(), 'VibeCoderOutput'); } } const TASK_LIST_DIR = path.join(getBaseOutputDir(), TASK_LIST_DIR_NAME); export async function initDirectories() { try { const toolDir = await ensureToolOutputDirectory(TASK_LIST_DIR_NAME); logger.debug(`Ensured task list directory exists: ${toolDir}`); } catch (error) { logger.error({ err: error }, `Failed to ensure base output directory exists for task-list-generator.`); const baseOutputDir = getBaseOutputDir(); try { await fs.ensureDir(baseOutputDir); const toolDir = path.join(baseOutputDir, TASK_LIST_DIR_NAME); await fs.ensureDir(toolDir); logger.debug(`Ensured task list directory exists (fallback): ${toolDir}`); } catch (fallbackError) { logger.error({ err: fallbackError, path: baseOutputDir }, `Fallback directory creation also failed.`); } } } const INITIAL_TASK_LIST_SYSTEM_PROMPT = ` # Task List Generator - High-Level Tasks # ROLE & GOAL You are an expert Project Manager AI. Your goal is to generate ONLY the high-level development tasks (typically corresponding to Epics or major features) based on user stories and research context. DO NOT decompose into sub-tasks yet. # CORE TASK Generate a high-level, hierarchical development task list based on the user's product description, provided user stories, and research context. Focus on major phases and features. # INPUT HANDLING - Analyze 'productDescription' and 'userStories'. - Analyze 'Pre-Generation Research Context' for lifecycle phases and key areas. # RESEARCH CONTEXT INTEGRATION - Use research insights (Lifecycle, Estimation, Team Structure) to: - Structure the list logically by standard phases (e.g., Setup, Backend, Frontend, Testing, Deployment). - Define realistic 'Dependencies' between these high-level tasks. - Apply appropriate relative 'Estimated Effort' (Small, Medium, Large). # OUTPUT FORMAT & STRUCTURE (Strict Markdown) - Your entire response **MUST** be valid Markdown. - Start **directly** with the main title: '# Task List: [Inferred Product Name]' - Organize tasks hierarchically using Markdown headings and nested lists: - \`## Phase: [Phase Name]\` - \`### Epic/Feature: [Related Epic or Feature]\` (Optional grouping) - Use a single level of bullet points (\`-\`) for the high-level tasks. - For **each High-Level Task**, include ONLY the following details: - **ID:** T-[auto-incrementing number, e.g., T-101] - **Title:** [Clear, Action-Oriented Task Title] - *(Description):* [Brief explanation.] - *(User Story):* [ID(s) of related User Story | N/A] - *(Priority):* [High | Medium | Low] - *(Dependencies):* [List of Task IDs | None] - *(Est. Effort):* [Small | Medium | Large] **Example High-Level Task Format:** \`\`\`markdown - **ID:** T-201 **Title:** Implement User Authentication Backend *(Description):* Set up API endpoints and logic for user registration, login, and session management. *(User Story):* US-101, US-102 *(Priority):* High *(Dependencies):* T-101 *(Est. Effort):* Large \`\`\` # CONSTRAINTS - **NO SUB-TASKS:** Do NOT break tasks down further in this step. - **NO Conversational Filler.** Start directly with '# Task List: ...'. - **Strict Formatting:** Use \`##\` for Phases, \`###\` for Epics, \`-\` for tasks. Use exact field names in bold. - **IMPORTANT:** Your thought must contain ONLY the properly formatted Markdown task list. `; const TASK_DECOMPOSITION_SYSTEM_PROMPT = ` # Task Decomposition Specialist # ROLE & GOAL You are an expert Technical Lead specializing in breaking down a single software development task into its smallest, most actionable sub-components. Your goal is to take ONE high-level task and decompose it into detailed sub-tasks suitable for individual assignment, including specific implementation guidance. # CORE TASK Decompose the provided high-level task into the smallest possible, independently executable sub-tasks. For each sub-task, provide detailed implementation guidance. # INPUT You will receive the details of a SINGLE high-level task (the Parent Task), including its ID, Title, Description, etc. # OUTPUT FORMAT & STRUCTURE (Strict Markdown List of Sub-Tasks ONLY) - Your entire response **MUST** be a Markdown list containing **only** the sub-tasks derived from the single Parent Task provided. - **DO NOT** repeat the Parent Task details in your output. - **DO NOT** include any introductory text, concluding text, or phase/epic headings. Just the flat list of sub-tasks for the *one* parent task. - For **each sub-task**, use the following precise format as a list item. **Adhere EXACTLY to this structure, field names, bolding, and indentation.** - **Sub-Task ID:** {Parent Task ID}.[auto-incrementing number starting from 1, e.g., T-101.1, T-101.2] **Goal:** [Briefly state the specific objective of this sub-task.] **Task:** [Clear, highly specific action for the developer to perform.] **Rationale:** [Explain *why* this sub-task is necessary and how it contributes to the parent task.] **Expected Outcome:** [Describe the concrete, verifiable result of completing this sub-task.] **Objectives:** [Bulleted list of specific, measurable mini-goals or checks for this sub-task. Each objective MUST start with '* '. ] * Objective 1 * Objective 2 **Implementation Prompt:** [A detailed, guiding prompt for an AI coding assistant (like Cline) to implement this specific sub-task. Be language/framework specific if possible based on context from the Parent Task Description/Title.] **Example Code:** \`\`\`[language, e.g., python, typescript, jsx] // Provide a concise, relevant code snippet or structure example // illustrating the expected implementation approach. // Keep it focused on the sub-task's core logic. Use placeholders. \`\`\` # DECOMPOSITION GUIDELINES - Break the parent task down until sub-tasks represent roughly 1-4 hours of focused work, if possible. - Sub-tasks should be logically sequential where necessary. - Ensure sub-tasks collectively fulfill the parent task's description and objectives. - Focus on technical implementation steps (e.g., "Define database schema", "Create API endpoint", "Implement UI component", "Write unit test"). # IMPLEMENTATION PROMPT & EXAMPLE CODE GUIDELINES - The **Implementation Prompt** should be clear enough for another AI to take it and generate the required code. Include necessary context (e.g., function names, variable types, expected inputs/outputs). - The **Example Code** should be a minimal, illustrative snippet demonstrating the pattern or key part of the implementation, not the full solution. Infer the likely language/framework if possible. # CONSTRAINTS (MANDATORY) - **ONLY output the Markdown list of sub-tasks.** No other text, explanations, or summaries before or after the list. - Adhere **STRICTLY** to the sub-task format provided above. Double-check field names, bolding, indentation, and the bullet points for Objectives. - Ensure sub-task IDs correctly follow the parent ID (e.g., T-101.1, T-101.2 for parent T-101). Start numbering from .1 for each parent. - The **Example Code** section MUST use triple backticks (\`\`\`) with a language identifier. - **Before finishing, review your generated list one last time to ensure it perfectly matches the required format.** - **IMPORTANT:** Your thought must contain ONLY the properly formatted Markdown sub-task list. `; const taskListInputSchemaShape = { productDescription: z.string().min(10, { message: "Product description must be at least 10 characters." }).describe("Description of the product"), userStories: z.string().min(20, { message: "User stories must be provided and be at least 20 characters." }).describe("User stories (in Markdown format) to use for task list generation") }; function parseHighLevelTasks(markdownContent) { const tasks = []; const lines = markdownContent.split('\n'); let currentTask = null; let currentTaskLines = []; const idRegex = /^\s*-\s+\*\*ID:\*\*\s*(T-\d+)/; const titleRegex = /^\s+\*\*Title:\*\*\s*(.*)/; const fieldRegex = /^\s+\*\(([\w\s.]+)\):\*\s*(.*)/; function finalizeCurrentTask() { if (currentTask && currentTask.id && currentTask.title) { currentTask.markdownLine = currentTaskLines.join('\n'); tasks.push(currentTask); } else if (currentTask) { logger.warn({ task: currentTask }, "Discarding incomplete task block during parsing."); } currentTask = null; currentTaskLines = []; } for (const line of lines) { const idMatch = line.match(idRegex); if (idMatch) { finalizeCurrentTask(); currentTask = { id: idMatch[1] }; currentTaskLines.push(line); continue; } if (currentTask) { currentTaskLines.push(line); const titleMatch = line.match(titleRegex); const fieldMatch = line.match(fieldRegex); if (titleMatch) { currentTask.title = titleMatch[1].trim(); } else if (fieldMatch) { const key = fieldMatch[1].trim().toLowerCase(); const value = fieldMatch[2].trim(); switch (key) { case 'description': currentTask.description = value; break; case 'user story': currentTask.userStory = value; break; case 'priority': currentTask.priority = value; break; case 'dependencies': currentTask.dependencies = value; break; case 'est. effort': currentTask.effort = value; break; default: logger.warn(`Unknown field key found in task ${currentTask.id}: ${key}`); } } } } finalizeCurrentTask(); return tasks; } function extractFallbackTasks(markdownContent) { logger.warn("Primary task parser failed. Attempting fallback parsing..."); const tasks = []; const lines = markdownContent.split('\n'); let taskCounter = 1; const potentialTaskRegex = /^\s*[-*]\s+(.*)|^\s*\d+\.\s+(.*)/; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); const match = line.match(potentialTaskRegex); if (match) { const title = (match[1] || match[2] || 'Untitled Fallback Task').trim(); if (title.startsWith('**ID:**') || title.startsWith('*(Description):*')) continue; const taskId = `T-FALLBACK-${taskCounter++}`; logger.debug(`Fallback parser found potential task: ${taskId} - ${title}`); tasks.push({ id: taskId, title, description: `(Fallback Parsed) ${title}`, userStory: 'N/A', priority: 'Medium', dependencies: 'None', effort: 'Medium', markdownLine: lines[i], subTasksMarkdown: undefined }); } } if (tasks.length > 0) logger.warn(`Fallback parser extracted ${tasks.length} potential tasks.`); else logger.error("Fallback parser also failed to extract any tasks."); return tasks; } function reconstructMarkdown(originalMarkdown, decomposedTasks) { let finalMarkdown = ""; const lines = originalMarkdown.split('\n'); let currentTaskBlock = ""; let currentTaskId = null; for (const line of lines) { const idMatch = line.match(/^\s*-\s+\*\*ID:\*\*\s*(T-\d+)/); if (idMatch) { if (currentTaskId && currentTaskBlock) { finalMarkdown += currentTaskBlock.trimEnd() + '\n'; if (decomposedTasks.has(currentTaskId)) { const subTasks = decomposedTasks.get(currentTaskId); if (subTasks) { const indentedSubTasks = subTasks.split('\n').map(subLine => subLine.trim() ? ` ${subLine}` : '').join('\n'); finalMarkdown += indentedSubTasks.trimEnd() + '\n'; } } } currentTaskId = idMatch[1]; currentTaskBlock = line + '\n'; } else if (currentTaskId) { currentTaskBlock += line + '\n'; } else { finalMarkdown += line + '\n'; } } if (currentTaskId && currentTaskBlock) { finalMarkdown += currentTaskBlock.trimEnd() + '\n'; if (decomposedTasks.has(currentTaskId)) { const subTasks = decomposedTasks.get(currentTaskId); if (subTasks) { const indentedSubTasks = subTasks.split('\n').map(subLine => subLine.trim() ? ` ${subLine}` : '').join('\n'); finalMarkdown += indentedSubTasks.trimEnd() + '\n'; } } } return finalMarkdown.trim(); } async function decomposeSingleTaskWithRetry(task, config, jobId, sessionId, maxRetries = 2) { const decompositionPrompt = `Decompose the following high-level Parent Task into detailed, actionable sub-tasks:\n\nParent Task ID: ${task.id}\nParent Title: ${task.title}\nParent Description: ${task.description}\nRelated User Story: ${task.userStory || 'N/A'}\nPriority: ${task.priority || 'N/A'}\nDependencies: ${task.dependencies || 'None'}\nEst. Effort: ${task.effort || 'N/A'}`; let attempts = 0; while (attempts <= maxRetries) { attempts++; try { logger.debug({ taskId: task.id, attempt: attempts }, `Attempting decomposition (attempt ${attempts}/${maxRetries + 1})`); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, `Decomposing task ${task.id} (attempt ${attempts})...`); const subTasksMarkdown = await performFormatAwareLlmCallWithCentralizedConfig(decompositionPrompt, TASK_DECOMPOSITION_SYSTEM_PROMPT, 'task_list_decomposition', 'markdown', undefined, 0.1); if (subTasksMarkdown && subTasksMarkdown.trim().startsWith('- **Sub-Task ID:**')) { logger.debug(`Successfully decomposed task ${task.id} on attempt ${attempts}`); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, `Task ${task.id} decomposed.`); return { taskId: task.id, markdown: subTasksMarkdown.trim() }; } else { logger.warn({ taskId: task.id, attempt: attempts, response: subTasksMarkdown }, `Decomposition attempt ${attempts} for task ${task.id} returned unexpected format.`); if (attempts > maxRetries) { throw new Error('Unexpected format received after maximum retries.'); } } } catch (error) { logger.warn({ err: error, taskId: task.id, attempt: attempts }, `Decomposition attempt ${attempts} for task ${task.id} failed.`); if (attempts > maxRetries) { throw error; } await new Promise(resolve => setTimeout(resolve, 1000 * attempts)); } } throw new Error(`Decomposition failed for task ${task.id} after ${maxRetries + 1} attempts.`); } export const generateTaskList = async (params, config, context) => { const sessionId = context?.sessionId || 'unknown-session'; if (sessionId === 'unknown-session') { logger.warn({ tool: 'generateTaskList' }, 'Executing tool without a valid sessionId. SSE progress updates will not be sent.'); } const productDescription = params.productDescription; const userStories = params.userStories; const jobId = jobManager.createJob('generate-task-list', params); logger.info({ jobId, tool: 'generateTaskList', sessionId }, 'Starting background job.'); const initialResponse = formatBackgroundJobInitiationResponse(jobId, 'generate-task-list', 'Task List Generator'); setImmediate(async () => { const decomposedTasks = new Map(); try { await initDirectories(); jobManager.updateJobStatus(jobId, JobStatus.RUNNING, 'Starting high-level task generation...'); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Starting high-level task generation...'); logger.info("Task List Generator: Starting Step 1 - High-Level Task Generation..."); let researchContext = ''; try { const query1 = `Software development lifecycle tasks and milestones for: ${productDescription}`; const query2 = `Task estimation and dependency management best practices for software projects`; const query3 = `Development team structures and work breakdown for projects similar to: ${productDescription}`; sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Performing pre-generation research...'); const researchResults = await Promise.allSettled([ performResearchQuery(query1, config), performResearchQuery(query2, config), performResearchQuery(query3, config) ]); researchContext = "## Pre-Generation Research Context (From Perplexity Sonar Deep Research):\n\n"; researchResults.forEach((result, index) => { const queryLabels = ["Development Lifecycle & Milestones", "Task Estimation & Dependencies", "Team Structure & Work Breakdown"]; if (result.status === "fulfilled") { researchContext += `### ${queryLabels[index]}:\n${result.value.trim()}\n\n`; } else { logger.warn({ error: result.reason }, `Research query ${index + 1} failed`); researchContext += `### ${queryLabels[index]}:\n*Research on this topic failed.*\n\n`; } }); logger.info("Task List Generator: Pre-generation research completed."); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Research complete. Generating high-level tasks...'); } catch (researchError) { logger.error({ err: researchError }, "Task List Generator: Error during research aggregation"); researchContext = "## Pre-Generation Research Context:\n*Error occurred during research phase.*\n\n"; sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Research phase failed. Proceeding with generation...'); } const initialGenerationPrompt = `Create a detailed task list for the following product:\n\n${productDescription}\n\nBased on these user stories:\n\n${userStories}\n\n${researchContext}`; const highLevelTaskListMarkdown = await performFormatAwareLlmCallWithCentralizedConfig(initialGenerationPrompt, INITIAL_TASK_LIST_SYSTEM_PROMPT, 'task_list_initial_generation', 'markdown', undefined, 0.1); logger.debug({ rawOutput: highLevelTaskListMarkdown }, "Raw output from Step 1 (High-Level Task Generation - Direct Call):"); logger.info("Task List Generator: Step 1 - High-Level Task Generation completed."); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'High-level tasks generated. Starting decomposition...'); logger.info("Task List Generator: Starting Step 2 - Task Decomposition..."); jobManager.updateJobStatus(jobId, JobStatus.RUNNING, 'Starting parallel task decomposition...'); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Starting parallel task decomposition...'); let parsedTasks = parseHighLevelTasks(highLevelTaskListMarkdown); if (parsedTasks.length === 0 && highLevelTaskListMarkdown.trim().length > 0) { parsedTasks = extractFallbackTasks(highLevelTaskListMarkdown); } if (parsedTasks.length === 0) { logger.error("Both primary and fallback parsers failed to extract any tasks. Cannot proceed with decomposition."); throw new Error("Failed to parse any high-level tasks from LLM response."); } else { logger.info(`Proceeding with decomposition for ${parsedTasks.length} tasks.`); const decompositionPromises = parsedTasks .filter(task => task.id && task.title && task.description) .map(task => decomposeSingleTaskWithRetry(task, config, jobId, sessionId)); const decompositionResults = await Promise.allSettled(decompositionPromises); decompositionResults.forEach((result, index) => { const originalTask = parsedTasks.filter(t => t.id && t.title && t.description)[index]; if (!originalTask) return; if (result.status === 'fulfilled') { decomposedTasks.set(result.value.taskId, result.value.markdown); } else { const error = result.reason; const errorMessage = error instanceof Error ? error.message : String(error); logger.error({ err: error, taskId: originalTask.id }, `Final decomposition failed for task ${originalTask.id} after retries.`); decomposedTasks.set(originalTask.id, `- *(Error: Final decomposition failed: ${errorMessage})*`); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, `Failed to decompose task ${originalTask.id}: ${errorMessage}`); } }); logger.info(`Task List Generator: Step 2 - Parallel decomposition finished.`); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Decomposition finished. Reconstructing output...'); } logger.info("Task List Generator: Reconstructing final Markdown output..."); jobManager.updateJobStatus(jobId, JobStatus.RUNNING, 'Reconstructing final output...'); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Reconstructing final output...'); const finalMarkdown = reconstructMarkdown(highLevelTaskListMarkdown, decomposedTasks); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Reconstruction complete. Saving file...'); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const sanitizedName = productDescription.substring(0, 30).toLowerCase().replace(/[^a-z0-9]+/g, '-'); const filename = `${timestamp}-${sanitizedName}-task-list-detailed.md`; const filePath = path.join(TASK_LIST_DIR, filename); try { await fs.writeFile(filePath, finalMarkdown, 'utf8'); logger.info(`Detailed task list generated and saved to ${filePath}`); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, `File saved to ${filePath}. Job complete.`); } catch (saveError) { logger.error({ err: saveError, filePath }, "Failed to save the final detailed task list."); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, `Failed to save output file to ${filePath}.`); } const finalResult = { content: [{ type: "text", text: `Detailed task list saved to: ${filePath}\n\n${finalMarkdown.substring(0, 1000)}...` }], isError: false }; jobManager.setJobResult(jobId, finalResult); } catch (error) { logger.error({ err: error, jobId, tool: 'generateTaskList' }, 'Error during background job execution.'); let appError; if (error instanceof AppError) { appError = error; } else if (error instanceof Error) { appError = new ToolExecutionError('Failed during task list generation background job.', { originalError: error.message }, error); } else { appError = new ToolExecutionError('An unknown error occurred during task list generation background job.', { thrownValue: String(error) }); } const mcpError = new McpError(ErrorCode.InternalError, appError.message, appError.context); const errorResult = { content: [{ type: 'text', text: `Error during background job ${jobId}: ${mcpError.message}` }], isError: true, errorDetails: mcpError }; jobManager.setJobResult(jobId, errorResult); sseNotifier.sendProgress(sessionId, jobId, JobStatus.FAILED, `Job failed: ${mcpError.message}`); } }); return initialResponse; }; const taskListToolDefinition = { name: "generate-task-list", description: "Creates structured development task lists, decomposing high-level tasks into detailed sub-tasks with implementation guidance.", inputSchema: taskListInputSchemaShape, executor: generateTaskList }; registerTool(taskListToolDefinition);