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
JavaScript
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()
};
}
}