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.

319 lines (318 loc) • 13.8 kB
import { FileSearchService } from '../../../../services/file-search-service/index.js'; import logger from '../../../../logger.js'; import path from 'path'; export class SearchFilesHandler { intent = 'search_files'; async handle(recognizedIntent, toolParams, context) { try { logger.info({ intent: recognizedIntent.intent, sessionId: context.sessionId }, 'Processing file search request'); const searchPattern = this.extractSearchPattern(recognizedIntent, toolParams); const searchOptions = this.extractSearchOptions(recognizedIntent, toolParams); const projectPath = this.extractProjectPath(recognizedIntent, toolParams, context); if (!searchPattern) { return { success: false, result: { content: [{ type: "text", text: "āŒ Please specify what files to search for. For example: 'find auth files' or 'search for component files'" }], isError: true } }; } const fileSearchService = FileSearchService.getInstance(); const results = await fileSearchService.searchFiles(projectPath, { pattern: searchPattern, searchStrategy: searchOptions.strategy || 'fuzzy', fileTypes: searchOptions.extensions, excludeDirs: searchOptions.excludePatterns, maxResults: searchOptions.maxResults || 20, cacheResults: true }); if (results.length === 0) { return { success: true, result: { content: [{ type: "text", text: `šŸ” No files found matching "${searchPattern}". Try a different search term or check the project path.` }] } }; } let responseText = `šŸ” Found ${results.length} files matching "${searchPattern}":\n\n`; results.slice(0, 10).forEach((result, index) => { const relativePath = path.relative(projectPath, result.filePath); const score = result.score ? ` (${(result.score * 100).toFixed(0)}% match)` : ''; const size = result.metadata?.size ? ` - ${this.formatBytes(result.metadata.size)}` : ''; responseText += `${index + 1}. **${relativePath}**${score}${size}\n`; }); if (results.length > 10) { responseText += `\n... and ${results.length - 10} more files\n`; } const metrics = fileSearchService.getPerformanceMetrics(); responseText += `\nšŸ“Š Search completed in ${metrics.searchTime}ms (${metrics.filesScanned} files scanned)`; return { success: true, result: { content: [{ type: "text", text: responseText }] }, followUpSuggestions: [ `Search for content in "${searchPattern}" files`, 'Search for a different file pattern', 'List all files in the project' ] }; } catch (error) { logger.error({ err: error, intent: recognizedIntent.intent, sessionId: context.sessionId }, 'File search failed'); return { success: false, result: { content: [{ type: "text", text: `āŒ Failed to search files: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true } }; } } extractSearchPattern(recognizedIntent, toolParams) { if (toolParams.pattern || toolParams.query) { return (toolParams.pattern || toolParams.query); } const patternEntity = recognizedIntent.entities.find(e => e.type === 'searchPattern' || e.type === 'fileName'); if (patternEntity) { return patternEntity.value; } const patterns = [ /(?:find|search\s+for|locate)\s+(.+?)\s+files?/i, /(?:find|search\s+for|locate)\s+(.+)/i, /files?\s+(?:named|called)\s+(.+)/i, /(.+?)\s+files?/i ]; for (const pattern of patterns) { const match = recognizedIntent.originalInput.match(pattern); if (match) { return match[1].trim(); } } return null; } extractSearchOptions(recognizedIntent, _toolParams) { const options = {}; const input = recognizedIntent.originalInput.toLowerCase(); const extMatch = input.match(/\.(\w+)\s+files?|(\w+)\s+files?/); if (extMatch) { const ext = extMatch[1] || extMatch[2]; if (['js', 'ts', 'tsx', 'jsx', 'py', 'java', 'cpp', 'css', 'html'].includes(ext)) { options.extensions = [`.${ext}`]; } } if (input.includes('exact') || input.includes('exactly')) { options.strategy = 'exact'; } else if (input.includes('regex') || input.includes('pattern')) { options.strategy = 'regex'; } else { options.strategy = 'fuzzy'; } if (input.includes('exclude') || input.includes('ignore')) { options.excludePatterns = ['node_modules', '.git', 'dist', 'build']; } const limitMatch = input.match(/(?:first|top|limit)\s+(\d+)/); if (limitMatch) { options.maxResults = parseInt(limitMatch[1], 10); } return options; } extractProjectPath(recognizedIntent, toolParams, context) { if (toolParams.path || toolParams.projectPath) { return path.resolve(toolParams.path || toolParams.projectPath); } if (context.currentProject) { return process.cwd(); } return process.cwd(); } formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } } export class SearchContentHandler { intent = 'search_content'; async handle(recognizedIntent, toolParams, context) { try { logger.info({ intent: recognizedIntent.intent, sessionId: context.sessionId }, 'Processing content search request'); const searchQuery = this.extractSearchQuery(recognizedIntent, toolParams); const searchOptions = this.extractSearchOptions(recognizedIntent, toolParams); const projectPath = this.extractProjectPath(recognizedIntent, toolParams, context); if (!searchQuery) { return { success: false, result: { content: [{ type: "text", text: "āŒ Please specify what content to search for. For example: 'find useState in files' or 'search for authentication code'" }], isError: true } }; } const fileSearchService = FileSearchService.getInstance(); const results = await fileSearchService.searchFiles(projectPath, { content: searchQuery, searchStrategy: 'content', fileTypes: searchOptions.extensions, excludeDirs: searchOptions.excludePatterns || ['node_modules', '.git', 'dist', 'build'], maxResults: searchOptions.maxResults || 15, caseSensitive: searchOptions.caseSensitive || false, cacheResults: true }); if (results.length === 0) { return { success: true, result: { content: [{ type: "text", text: `šŸ” No content found matching "${searchQuery}". Try a different search term or check the file types.` }] } }; } let responseText = `šŸ” Found "${searchQuery}" in ${results.length} files:\n\n`; results.slice(0, 8).forEach((result, index) => { const relativePath = path.relative(projectPath, result.filePath); const matchCount = result.lineNumbers ? result.lineNumbers.length : 0; responseText += `${index + 1}. **${relativePath}** (${matchCount} matches)\n`; if (result.preview) { const lines = result.preview.split('\n').slice(0, 2); lines.forEach((line, _lineIndex) => { const lineText = line.trim(); responseText += ` \`${lineText.substring(0, 80)}${lineText.length > 80 ? '...' : ''}\`\n`; }); if (result.lineNumbers && result.lineNumbers.length > 2) { responseText += ` ... and ${result.lineNumbers.length - 2} more matches\n`; } } responseText += '\n'; }); if (results.length > 8) { responseText += `... and ${results.length - 8} more files\n\n`; } const metrics = fileSearchService.getPerformanceMetrics(); responseText += `šŸ“Š Content search completed in ${metrics.searchTime}ms (${metrics.filesScanned} files scanned)`; return { success: true, result: { content: [{ type: "text", text: responseText }] }, followUpSuggestions: [ `Search for files containing "${searchQuery}"`, 'Search for a different content pattern', 'Show more details for specific files' ] }; } catch (error) { logger.error({ err: error, intent: recognizedIntent.intent, sessionId: context.sessionId }, 'Content search failed'); return { success: false, result: { content: [{ type: "text", text: `āŒ Failed to search content: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true } }; } } extractSearchQuery(recognizedIntent, toolParams) { if (toolParams.query || toolParams.content) { return (toolParams.query || toolParams.content); } const queryEntity = recognizedIntent.entities.find(e => e.type === 'searchQuery' || e.type === 'content'); if (queryEntity) { return queryEntity.value; } const input = recognizedIntent.originalInput; const patterns = [ /(?:find|search\s+for|locate)\s+(.+?)\s+(?:in\s+files?|in\s+code)/i, /(?:find|search\s+for|locate)\s+"(.+?)"/i, /(?:find|search\s+for|locate)\s+'(.+?)'/i, /(?:find|search\s+for|locate)\s+(.+)/i, /content\s+(?:containing|with)\s+(.+)/i ]; for (const pattern of patterns) { const match = input.match(pattern); if (match) { return match[1].trim(); } } return null; } extractSearchOptions(recognizedIntent, _toolParams) { const options = {}; const input = recognizedIntent.originalInput.toLowerCase(); const extMatch = input.match(/in\s+\.(\w+)\s+files?|in\s+(\w+)\s+files?/); if (extMatch) { const ext = extMatch[1] || extMatch[2]; if (['js', 'ts', 'tsx', 'jsx', 'py', 'java', 'cpp', 'css', 'html'].includes(ext)) { options.extensions = [`.${ext}`]; } } if (input.includes('case sensitive') || input.includes('exact case')) { options.caseSensitive = true; } if (input.includes('regex') || input.includes('regular expression')) { options.useRegex = true; } const contextMatch = input.match(/(?:with|show)\s+(\d+)\s+lines?\s+(?:of\s+)?context/); if (contextMatch) { options.contextLines = parseInt(contextMatch[1], 10); } const limitMatch = input.match(/(?:first|top|limit)\s+(\d+)/); if (limitMatch) { options.maxResults = parseInt(limitMatch[1], 10); } return options; } extractProjectPath(recognizedIntent, toolParams, context) { if (toolParams.path || toolParams.projectPath) { return path.resolve(toolParams.path || toolParams.projectPath); } if (context.currentProject) { return process.cwd(); } return process.cwd(); } }