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.

691 lines (690 loc) 29.6 kB
import logger from '../../../logger.js'; import { FileSearchService, FileReaderService } from '../../../services/file-search-service/index.js'; export class ContextEnrichmentService { static instance; fileSearchService; fileReaderService; config; constructor() { this.fileSearchService = FileSearchService.getInstance(); this.fileReaderService = FileReaderService.getInstance(); this.config = { defaultMaxFiles: 20, defaultMaxContentSize: 100000, minRelevanceThreshold: 0.3, fileTypePriorities: { '.ts': 1.0, '.js': 0.9, '.tsx': 0.95, '.jsx': 0.85, '.json': 0.7, '.md': 0.6, '.txt': 0.5, '.yml': 0.6, '.yaml': 0.6, '.config.js': 0.8, '.config.ts': 0.8 }, keywordBoostFactor: 0.2, recencyWeightDays: 30 }; logger.debug('Context enrichment service initialized'); } static getInstance() { if (!ContextEnrichmentService.instance) { ContextEnrichmentService.instance = new ContextEnrichmentService(); } return ContextEnrichmentService.instance; } async gatherContext(request) { const startTime = Date.now(); logger.info({ taskDescription: request.taskDescription.substring(0, 100), projectPath: request.projectPath }, 'Starting context gathering'); try { const searchStartTime = Date.now(); const candidateFiles = await this.discoverCandidateFiles(request); const searchTime = Date.now() - searchStartTime; const readStartTime = Date.now(); const readResult = await this.readAndScoreFiles(candidateFiles, request); const readTime = Date.now() - readStartTime; const scoringStartTime = Date.now(); const selectedFiles = await this.selectBestFiles(readResult, request); const scoringTime = Date.now() - scoringStartTime; const totalTime = Date.now() - startTime; const result = { contextFiles: selectedFiles, failedFiles: readResult?.errors?.map(e => e.filePath) || [], summary: { totalFiles: selectedFiles.length, totalSize: selectedFiles.reduce((sum, f) => sum + f.charCount, 0), averageRelevance: selectedFiles.length > 0 ? selectedFiles.reduce((sum, f) => sum + f.relevance.overallScore, 0) / selectedFiles.length : 0, topFileTypes: this.getTopFileTypes(selectedFiles), gatheringTime: totalTime }, metrics: { searchTime, readTime, scoringTime, totalTime, cacheHitRate: readResult?.metrics?.cacheHits ? readResult.metrics.cacheHits / Math.max(readResult.metrics.totalFiles, 1) : 0 } }; logger.info({ totalFiles: result.summary.totalFiles, totalSize: result.summary.totalSize, averageRelevance: result.summary.averageRelevance.toFixed(2), gatheringTime: result.summary.gatheringTime }, 'Context gathering completed'); return result; } catch (error) { logger.error({ err: error, request }, 'Context gathering failed'); throw error; } } async discoverCandidateFiles(request) { const candidateFiles = new Set(); if (request.includeFiles) { request.includeFiles.forEach(file => candidateFiles.add(file)); } if (request.searchPatterns) { for (const pattern of request.searchPatterns) { const searchOptions = { pattern, searchStrategy: 'fuzzy', maxResults: 50, fileTypes: request.priorityFileTypes, excludeDirs: request.excludeDirs, cacheResults: true }; const results = await this.fileSearchService.searchFiles(request.projectPath, searchOptions); if (results && Array.isArray(results)) { results.forEach(result => candidateFiles.add(result.filePath)); } } } if (request.globPatterns) { for (const globPattern of request.globPatterns) { const searchOptions = { glob: globPattern, searchStrategy: 'glob', maxResults: 100, excludeDirs: request.excludeDirs, cacheResults: true }; const results = await this.fileSearchService.searchFiles(request.projectPath, searchOptions); if (results && Array.isArray(results)) { results.forEach(result => candidateFiles.add(result.filePath)); } } } if (request.contentKeywords) { for (const keyword of request.contentKeywords) { const searchOptions = { content: keyword, searchStrategy: 'content', maxResults: 30, fileTypes: request.priorityFileTypes, excludeDirs: request.excludeDirs, cacheResults: true }; const results = await this.fileSearchService.searchFiles(request.projectPath, searchOptions); if (results && Array.isArray(results)) { results.forEach(result => candidateFiles.add(result.filePath)); } } } if (!request.searchPatterns && !request.globPatterns && !request.contentKeywords && !request.includeFiles) { const keywords = this.extractKeywordsFromTask(request.taskDescription); for (const keyword of keywords.slice(0, 3)) { const searchOptions = { pattern: keyword, searchStrategy: 'fuzzy', maxResults: 20, fileTypes: request.priorityFileTypes, excludeDirs: request.excludeDirs, cacheResults: true }; const results = await this.fileSearchService.searchFiles(request.projectPath, searchOptions); if (results && Array.isArray(results)) { results.forEach(result => candidateFiles.add(result.filePath)); } } } const candidateArray = Array.from(candidateFiles); logger.debug({ candidateCount: candidateArray.length }, 'Discovered candidate files'); return candidateArray; } async readAndScoreFiles(candidateFiles, _request) { const readOptions = { maxFileSize: 5 * 1024 * 1024, cacheContent: true, includeMetadata: true, maxLines: 1000 }; return this.fileReaderService.readFiles(candidateFiles, readOptions); } async selectBestFiles(readResult, request) { const maxFiles = request.maxFiles || this.config.defaultMaxFiles; const maxContentSize = request.maxContentSize || this.config.defaultMaxContentSize; if (!readResult || !readResult.files) { logger.warn('No files in readResult, returning empty array'); return []; } const scoredFiles = readResult.files.map(file => ({ ...file, relevance: this.calculateRelevance(file, request) })); const relevantFiles = scoredFiles.filter(file => file.relevance.overallScore >= this.config.minRelevanceThreshold); relevantFiles.sort((a, b) => b.relevance.overallScore - a.relevance.overallScore); const selectedFiles = []; let totalSize = 0; for (const file of relevantFiles) { if (selectedFiles.length >= maxFiles) break; if (totalSize + file.charCount > maxContentSize) break; selectedFiles.push(file); totalSize += file.charCount; } logger.debug({ totalCandidates: readResult.files.length, relevantFiles: relevantFiles.length, selectedFiles: selectedFiles.length, totalSize }, 'File selection completed'); return selectedFiles; } calculateRelevance(file, request) { const nameRelevance = this.calculateNameRelevance(file.filePath, request.taskDescription); const contentRelevance = this.calculateContentRelevance(file.content, request); const typePriority = this.config.fileTypePriorities[file.extension] || 0.5; const recencyFactor = this.calculateRecencyFactor(file.lastModified); const sizeFactor = this.calculateSizeFactor(file.charCount); const overallScore = (nameRelevance * 0.3 + contentRelevance * 0.4 + typePriority * 0.15 + recencyFactor * 0.1 + sizeFactor * 0.05); return { nameRelevance, contentRelevance, typePriority, recencyFactor, sizeFactor, overallScore: Math.min(overallScore, 1.0) }; } calculateNameRelevance(filePath, taskDescription) { const fileName = filePath.toLowerCase(); const taskWords = this.extractKeywordsFromTask(taskDescription); let relevanceScore = 0; for (const word of taskWords) { if (fileName.includes(word.toLowerCase())) { relevanceScore += 1; } } return taskWords.length > 0 ? Math.min(relevanceScore / taskWords.length, 1.0) : 0; } calculateContentRelevance(content, request) { const contentLower = content.toLowerCase(); let relevanceScore = 0; let totalKeywords = 0; const taskKeywords = this.extractKeywordsFromTask(request.taskDescription); for (const keyword of taskKeywords) { totalKeywords++; if (contentLower.includes(keyword.toLowerCase())) { relevanceScore += 1; } } if (request.contentKeywords) { for (const keyword of request.contentKeywords) { totalKeywords++; if (contentLower.includes(keyword.toLowerCase())) { relevanceScore += 1 + this.config.keywordBoostFactor; } } } return totalKeywords > 0 ? Math.min(relevanceScore / totalKeywords, 1.0) : 0; } calculateRecencyFactor(lastModified) { const now = new Date(); const daysDiff = (now.getTime() - lastModified.getTime()) / (1000 * 60 * 60 * 24); if (daysDiff <= 1) return 1.0; if (daysDiff <= 7) return 0.9; if (daysDiff <= this.config.recencyWeightDays) return 0.7; return 0.5; } calculateSizeFactor(charCount) { if (charCount <= 1000) return 1.0; if (charCount <= 5000) return 0.9; if (charCount <= 20000) return 0.7; if (charCount <= 50000) return 0.5; return 0.3; } extractKeywordsFromTask(taskDescription) { const stopWords = new Set([ 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'up', 'about', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'among', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'can', 'this', 'that', 'these', 'those' ]); const words = taskDescription .toLowerCase() .replace(/[^\w\s]/g, ' ') .split(/\s+/) .filter(word => word.length > 2 && !stopWords.has(word)) .filter(word => !/^\d+$/.test(word)); return Array.from(new Set(words)); } getTopFileTypes(files) { const typeCount = new Map(); files.forEach(file => { const ext = file.extension || 'unknown'; typeCount.set(ext, (typeCount.get(ext) || 0) + 1); }); return Array.from(typeCount.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([ext]) => ext); } async createContextSummary(contextResult) { const { contextFiles, summary } = contextResult; if (contextFiles.length === 0) { return 'No relevant context files found for this task.'; } let contextSummary = `## Context Summary\n\n`; contextSummary += `Found ${summary.totalFiles} relevant files (${Math.round(summary.totalSize / 1024)}KB total)\n`; contextSummary += `Average relevance: ${(summary.averageRelevance * 100).toFixed(1)}%\n`; contextSummary += `Top file types: ${summary.topFileTypes.join(', ')}\n\n`; contextSummary += `## File Contents\n\n`; for (const file of contextFiles) { const relativePath = file.filePath.split('/').slice(-3).join('/'); const relevancePercent = (file.relevance.overallScore * 100).toFixed(1); contextSummary += `### ${relativePath} (${relevancePercent}% relevant)\n\n`; contextSummary += `\`\`\`${file.extension.slice(1) || 'text'}\n`; const content = file.content.length > 2000 ? file.content.substring(0, 2000) + '\n... (truncated)' : file.content; contextSummary += content; contextSummary += `\n\`\`\`\n\n`; } return contextSummary; } async extractContextFromPRD(prdData) { try { logger.info({ projectName: prdData.metadata.projectName, featureCount: prdData.features.length }, 'Extracting context from PRD'); const languages = this.extractLanguagesFromTechStack(prdData.technical.techStack); const frameworks = this.extractFrameworksFromTechStack(prdData.technical.techStack); const tools = this.extractToolsFromTechStack(prdData.technical.techStack); const complexity = this.determineComplexityFromPRD(prdData); const teamSize = this.extractTeamSizeFromConstraints(prdData.constraints); const codebaseSize = this.estimateCodebaseSizeFromPRD(prdData); const projectContext = { projectId: `prd-${prdData.metadata.projectName.toLowerCase().replace(/\s+/g, '-')}`, projectPath: process.cwd(), projectName: prdData.metadata.projectName, description: prdData.overview.description, languages, frameworks, buildTools: [], tools, configFiles: [], entryPoints: [], architecturalPatterns: prdData.technical.architecturalPatterns, existingTasks: [], codebaseSize, teamSize, complexity, codebaseContext: { relevantFiles: [], contextSummary: prdData.overview.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: 'auto-detected' } }; logger.info({ projectId: projectContext.projectId, languages: languages.length, frameworks: frameworks.length, complexity, featureCount: prdData.features.length }, 'Successfully extracted context from PRD'); return projectContext; } catch (error) { logger.error({ err: error, prdPath: prdData.metadata.filePath }, 'Failed to extract context from PRD'); throw error; } } async extractContextFromTaskList(taskListData) { try { logger.info({ projectName: taskListData.metadata.projectName, taskCount: taskListData.metadata.totalTasks, phaseCount: taskListData.metadata.phaseCount }, 'Extracting context from task list'); const languages = this.extractLanguagesFromTechStack(taskListData.overview.techStack); const frameworks = this.extractFrameworksFromTechStack(taskListData.overview.techStack); const tools = this.extractToolsFromTechStack(taskListData.overview.techStack); const complexity = this.determineComplexityFromTaskList(taskListData); const teamSize = this.estimateTeamSizeFromTaskList(taskListData); const codebaseSize = this.estimateCodebaseSizeFromTaskList(taskListData); const existingTasks = []; const projectContext = { projectId: `task-list-${taskListData.metadata.projectName.toLowerCase().replace(/\s+/g, '-')}`, projectPath: process.cwd(), projectName: taskListData.metadata.projectName, description: taskListData.overview.description, languages, frameworks, buildTools: [], tools, configFiles: [], entryPoints: [], architecturalPatterns: [], existingTasks, codebaseSize, teamSize, complexity, codebaseContext: { relevantFiles: [], contextSummary: taskListData.overview.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: 'auto-detected' } }; logger.info({ projectId: projectContext.projectId, languages: languages.length, frameworks: frameworks.length, complexity, taskCount: taskListData.metadata.totalTasks, totalHours: taskListData.statistics.totalEstimatedHours }, 'Successfully extracted context from task list'); return projectContext; } catch (error) { logger.error({ err: error, taskListPath: taskListData.metadata.filePath }, 'Failed to extract context from task list'); throw error; } } extractLanguagesFromTechStack(techStack) { const languageKeywords = { 'javascript': ['javascript', 'js', 'node.js', 'nodejs'], 'typescript': ['typescript', 'ts'], 'python': ['python', 'py', 'django', 'flask', 'fastapi'], 'java': ['java', 'spring', 'maven', 'gradle'], 'csharp': ['c#', 'csharp', '.net', 'dotnet', 'asp.net'], 'php': ['php', 'laravel', 'symfony', 'composer'], 'ruby': ['ruby', 'rails', 'gem'], 'go': ['go', 'golang'], 'rust': ['rust', 'cargo'], 'swift': ['swift', 'ios'], 'kotlin': ['kotlin', 'android'], 'dart': ['dart', 'flutter'], 'scala': ['scala', 'sbt'], 'clojure': ['clojure', 'leiningen'] }; const detectedLanguages = new Set(); const techStackLower = techStack.map(item => item.toLowerCase()); for (const [language, keywords] of Object.entries(languageKeywords)) { if (keywords.some(keyword => techStackLower.some(item => item.includes(keyword)))) { detectedLanguages.add(language); } } return Array.from(detectedLanguages); } extractFrameworksFromTechStack(techStack) { const frameworkKeywords = { 'react': ['react', 'react.js', 'reactjs'], 'vue': ['vue', 'vue.js', 'vuejs'], 'angular': ['angular', 'angularjs'], 'svelte': ['svelte', 'sveltekit'], 'next.js': ['next.js', 'nextjs', 'next'], 'nuxt.js': ['nuxt.js', 'nuxtjs', 'nuxt'], 'express': ['express', 'express.js'], 'fastify': ['fastify'], 'nestjs': ['nestjs', 'nest.js'], 'django': ['django'], 'flask': ['flask'], 'fastapi': ['fastapi'], 'spring': ['spring', 'spring boot'], 'laravel': ['laravel'], 'rails': ['rails', 'ruby on rails'], 'gin': ['gin'], 'fiber': ['fiber'], 'actix': ['actix'], 'rocket': ['rocket'] }; const detectedFrameworks = new Set(); const techStackLower = techStack.map(item => item.toLowerCase()); for (const [framework, keywords] of Object.entries(frameworkKeywords)) { if (keywords.some(keyword => techStackLower.some(item => item.includes(keyword)))) { detectedFrameworks.add(framework); } } return Array.from(detectedFrameworks); } extractToolsFromTechStack(techStack) { const toolKeywords = { 'docker': ['docker', 'dockerfile', 'container'], 'kubernetes': ['kubernetes', 'k8s', 'kubectl'], 'redis': ['redis'], 'postgresql': ['postgresql', 'postgres', 'pg'], 'mysql': ['mysql'], 'mongodb': ['mongodb', 'mongo'], 'elasticsearch': ['elasticsearch', 'elastic'], 'nginx': ['nginx'], 'apache': ['apache'], 'webpack': ['webpack'], 'vite': ['vite'], 'babel': ['babel'], 'eslint': ['eslint'], 'prettier': ['prettier'], 'jest': ['jest'], 'cypress': ['cypress'], 'playwright': ['playwright'], 'git': ['git', 'github', 'gitlab'], 'aws': ['aws', 'amazon web services'], 'gcp': ['gcp', 'google cloud'], 'azure': ['azure', 'microsoft azure'] }; const detectedTools = new Set(); const techStackLower = techStack.map(item => item.toLowerCase()); for (const [tool, keywords] of Object.entries(toolKeywords)) { if (keywords.some(keyword => techStackLower.some(item => item.includes(keyword)))) { detectedTools.add(tool); } } return Array.from(detectedTools); } determineComplexityFromPRD(prdData) { let complexityScore = 0; if (prdData.features.length > 10) complexityScore += 2; else if (prdData.features.length > 5) complexityScore += 1; if (prdData.technical.techStack.length > 8) complexityScore += 2; else if (prdData.technical.techStack.length > 4) complexityScore += 1; if (prdData.technical.architecturalPatterns.length > 3) complexityScore += 1; if (prdData.technical.performanceRequirements.length > 3) complexityScore += 1; if (prdData.technical.securityRequirements.length > 3) complexityScore += 1; const totalConstraints = prdData.constraints.timeline.length + prdData.constraints.budget.length + prdData.constraints.resources.length + prdData.constraints.technical.length; if (totalConstraints > 6) complexityScore += 1; if (complexityScore >= 5) return 'high'; if (complexityScore >= 3) return 'medium'; return 'low'; } determineComplexityFromTaskList(taskListData) { let complexityScore = 0; if (taskListData.metadata.totalTasks > 20) complexityScore += 2; else if (taskListData.metadata.totalTasks > 10) complexityScore += 1; if (taskListData.metadata.phaseCount > 5) complexityScore += 1; if (taskListData.statistics.totalEstimatedHours > 100) complexityScore += 2; else if (taskListData.statistics.totalEstimatedHours > 50) complexityScore += 1; const highPriorityTasks = (taskListData.statistics.tasksByPriority.high || 0) + (taskListData.statistics.tasksByPriority.critical || 0); if (highPriorityTasks > 5) complexityScore += 1; if (taskListData.overview.techStack.length > 5) complexityScore += 1; if (complexityScore >= 5) return 'high'; if (complexityScore >= 3) return 'medium'; return 'low'; } extractTeamSizeFromConstraints(constraints) { for (const resource of constraints.resources) { const teamMatch = resource.match(/(\d+)\s*(?:developers?|engineers?|people|team members?)/i); if (teamMatch) { return parseInt(teamMatch[1], 10); } } return 3; } estimateTeamSizeFromTaskList(taskListData) { const totalHours = taskListData.statistics.totalEstimatedHours; const totalTasks = taskListData.metadata.totalTasks; if (totalHours > 200) return Math.min(Math.ceil(totalHours / 160), 8); if (totalHours > 80) return Math.min(Math.ceil(totalHours / 80), 5); if (totalTasks > 15) return Math.min(Math.ceil(totalTasks / 8), 4); return Math.max(1, Math.ceil(totalTasks / 10)); } estimateCodebaseSizeFromPRD(prdData) { let sizeScore = 0; if (prdData.features.length > 15) sizeScore += 2; else if (prdData.features.length > 8) sizeScore += 1; if (prdData.technical.techStack.length > 10) sizeScore += 2; else if (prdData.technical.techStack.length > 5) sizeScore += 1; if (prdData.technical.architecturalPatterns.some(pattern => pattern.toLowerCase().includes('microservice') || pattern.toLowerCase().includes('distributed'))) { sizeScore += 2; } if (sizeScore >= 4) return 'large'; if (sizeScore >= 2) return 'medium'; return 'small'; } estimateCodebaseSizeFromTaskList(taskListData) { const totalHours = taskListData.statistics.totalEstimatedHours; const totalTasks = taskListData.metadata.totalTasks; if (totalHours > 150 || totalTasks > 25) return 'large'; if (totalHours > 75 || totalTasks > 15) return 'medium'; return 'small'; } extractHoursFromEffort(effort) { const match = effort.match(/(\d+(?:\.\d+)?)\s*(?:hours?|hrs?|h)/i); return match ? parseFloat(match[1]) : 0; } clearCache() { this.fileSearchService.clearCache(); this.fileReaderService.clearCache(); logger.info('Context enrichment cache cleared'); } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; logger.debug({ config: this.config }, 'Context enrichment configuration updated'); } getConfig() { return { ...this.config }; } getPerformanceMetrics() { return { searchMetrics: this.fileSearchService.getPerformanceMetrics(), readerCacheStats: this.fileReaderService.getCacheStats(), searchCacheStats: this.fileSearchService.getCacheStats() }; } }