UNPKG

sourcegraph-mcp-server

Version:

A Model Context Protocol (MCP) server that connects AI assistants to Sourcegraph code search with natural language capabilities

400 lines (398 loc) 15.6 kB
"use strict"; /** * Enhanced result formatter with syntax highlighting and better display options */ Object.defineProperty(exports, "__esModule", { value: true }); exports.analyzeQueryEnhanced = exports.formatSearchResultsEnhanced = void 0; /** * Enhanced formatter for search results with better display formats * including markdown code blocks with syntax highlighting */ function formatSearchResultsEnhanced(results, queryInfo) { const { query, type } = queryInfo; const matchCount = results.matchCount; const items = results.results; // Early return for no results if (!items || items.length === 0) { return `No matches found for "${query}" with type:${type}.`; } // Group results by repository const repositories = {}; // Process each result item items.forEach((item) => { if (item.__typename === 'FileMatch') { const repoName = item.repository.name; const filePath = item.file.path; // Initialize repository if not exists if (!repositories[repoName]) { repositories[repoName] = { name: repoName, files: {} }; } // Initialize file array if not exists if (!repositories[repoName].files[filePath]) { repositories[repoName].files[filePath] = []; } // Add line matches item.lineMatches.forEach((match) => { repositories[repoName].files[filePath].push({ path: filePath, lineNumber: match.lineNumber, preview: match.preview }); }); } else if (item.__typename === 'CommitSearchResult') { const commit = item.commit; const repoName = commit.repository.name; // Initialize repository if not exists if (!repositories[repoName]) { repositories[repoName] = { name: repoName, files: {} }; } // Initialize commits array if not exists if (!repositories[repoName].commits) { repositories[repoName].commits = []; } // Add commit repositories[repoName].commits.push({ oid: commit.oid, message: commit.message, author: commit.author.person.name, email: commit.author.person.email, date: commit.author.date }); // If it's a diff search result, process the diffs if (type === 'diff' && item.diff) { // Initialize diffs array if not exists if (!repositories[repoName].diffs) { repositories[repoName].diffs = []; } const fileDiffs = item.diff.fileDiffs.map((fileDiff) => { return { oldPath: fileDiff.oldPath, newPath: fileDiff.newPath, hunks: fileDiff.hunks ? fileDiff.hunks.map((hunk) => ({ oldStart: hunk.oldRange.start, oldLines: hunk.oldRange.lines, newStart: hunk.newRange.start, newLines: hunk.newRange.lines, body: hunk.body })) : [] }; }); repositories[repoName].diffs.push({ commitInfo: { oid: commit.oid, message: commit.message, author: commit.author.person.name, date: commit.author.date }, fileDiffs }); } } }); // Generate enhanced formatted response return generateEnhancedResponse(repositories, { query, type, matchCount }); } exports.formatSearchResultsEnhanced = formatSearchResultsEnhanced; /** * Generate an enhanced response with better formatting and code blocks */ function generateEnhancedResponse(repositories, queryInfo) { const { query, type, matchCount } = queryInfo; const repoNames = Object.keys(repositories); // Create summary line with Markdown formatting let response = `## Search Results Found **${matchCount} matches** across ${repoNames.length} repositories for query: \`${query}\`\n\n`; // Generate structured response by repository repoNames.forEach((repoName, index) => { const repo = repositories[repoName]; response += `### ${index + 1}. Repository: ${repoName}\n\n`; // Handle file matches with syntax highlighting if (type === 'file') { const fileNames = Object.keys(repo.files); if (fileNames.length > 0) { fileNames.forEach(fileName => { // Determine the language for syntax highlighting const fileExtension = fileName.split('.').pop() || ''; const language = determineLanguage(fileExtension); response += `#### ${fileName}\n\n`; // Group matches by line number proximity for better context const matches = repo.files[fileName]; const sortedMatches = matches.sort((a, b) => a.lineNumber - b.lineNumber); // Create code blocks with line numbers let currentBlock = []; let lastLineNumber = -1; for (const match of sortedMatches) { // If there's a gap of more than 3 lines, start a new block if (lastLineNumber !== -1 && match.lineNumber > lastLineNumber + 3) { // Output the current block if (currentBlock.length > 0) { response += formatCodeBlock(currentBlock, language); currentBlock = []; } } currentBlock.push({ lineNumber: match.lineNumber, text: match.preview }); lastLineNumber = match.lineNumber; } // Output any remaining block if (currentBlock.length > 0) { response += formatCodeBlock(currentBlock, language); } response += '\n'; }); } } // Handle commit matches with better formatting else if (type === 'commit' && repo.commits && repo.commits.length > 0) { repo.commits.forEach(commit => { const shortId = commit.oid.substring(0, 7); response += `#### Commit ${shortId}\n\n`; response += `**Author:** ${commit.author}${commit.email ? ` <${commit.email}>` : ''}\n\n`; response += `**Date:** ${formatDate(commit.date)}\n\n`; // Format commit message as a quote block const messageLines = commit.message.trim().split('\n'); response += `**Message:**\n\n> ${messageLines.join('\n> ')}\n\n`; response += `---\n\n`; }); } // Handle diff matches with syntax highlighting else if (type === 'diff' && repo.diffs && repo.diffs.length > 0) { repo.diffs.forEach(diff => { const shortId = diff.commitInfo.oid.substring(0, 7); response += `#### Changes in commit ${shortId}\n\n`; response += `**Author:** ${diff.commitInfo.author}\n\n`; response += `**Date:** ${formatDate(diff.commitInfo.date)}\n\n`; // Format commit message as a quote block const messageLines = diff.commitInfo.message.trim().split('\n'); response += `**Message:**\n\n> ${messageLines.join('\n> ')}\n\n`; diff.fileDiffs.forEach(fileDiff => { const filePath = fileDiff.newPath || fileDiff.oldPath || 'Unknown file'; const fileExtension = filePath.split('.').pop() || ''; const language = determineLanguage(fileExtension); response += `##### ${filePath}\n\n`; // Show diffs with syntax highlighting fileDiff.hunks.forEach(hunk => { response += `\`\`\`diff\n@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@\n${hunk.body}\n\`\`\`\n\n`; }); }); response += `---\n\n`; }); } }); // Add a marker at the top indicating this is markdown content return `<!-- markdown content --> ${response}`; } /** * Format a block of code lines with line numbers */ function formatCodeBlock(lines, language) { let codeBlock = `\`\`\`${language}\n`; lines.forEach(line => { codeBlock += `${line.lineNumber}: ${line.text}\n`; }); codeBlock += '\`\`\`\n\n'; return codeBlock; } /** * Determine the language for syntax highlighting based on file extension */ function determineLanguage(extension) { const languageMap = { 'js': 'javascript', 'ts': 'typescript', 'jsx': 'jsx', 'tsx': 'tsx', 'py': 'python', 'java': 'java', 'c': 'c', 'cpp': 'cpp', 'h': 'c', 'hpp': 'cpp', 'cs': 'csharp', 'go': 'go', 'rb': 'ruby', 'php': 'php', 'html': 'html', 'css': 'css', 'scss': 'scss', 'less': 'less', 'json': 'json', 'md': 'markdown', 'sh': 'bash', 'bash': 'bash', 'yaml': 'yaml', 'yml': 'yaml', 'xml': 'xml', 'sql': 'sql', 'rs': 'rust', 'swift': 'swift', 'kt': 'kotlin', 'scala': 'scala', 'pl': 'perl', 'ex': 'elixir', 'exs': 'elixir', 'erl': 'erlang', 'hs': 'haskell', 'fs': 'fsharp', 'r': 'r' }; return languageMap[extension.toLowerCase()] || 'text'; } /** * Format a date string to a more readable format */ function formatDate(dateString) { try { const date = new Date(dateString); return date.toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } catch (e) { return dateString; // Return original if parsing fails } } /** * Enhanced version of analysis functions for natural language queries */ const llm_1 = require("../services/llm"); /** * Analyze a natural language query to determine search type and parameters * with enhanced formatting of the results */ async function analyzeQueryEnhanced(query) { try { // Use LLM to convert the natural query to Sourcegraph syntax const sourcegraphQuery = await (0, llm_1.convertQueryToSourcegraphSyntax)(query); console.log(`Converted query: ${sourcegraphQuery}`); // Parse the Sourcegraph query to extract components return parseSourcegraphQuery(sourcegraphQuery, query); } catch (error) { console.error('Error during query analysis with LLM:', error); // Fall back to the rule-based parsing if LLM fails return parseWithRules(query); } } exports.analyzeQueryEnhanced = analyzeQueryEnhanced; /** * Parse a Sourcegraph syntax query to extract components */ function parseSourcegraphQuery(sourcegraphQuery, originalQuery) { // Default parameters let searchType = 'file'; let searchQuery = sourcegraphQuery; let author = undefined; let after = undefined; const repos = []; // Extract search type const typeMatch = sourcegraphQuery.match(/\btype:(\w+)\b/); if (typeMatch) { searchType = typeMatch[1]; // Remove the type: parameter from the search query searchQuery = searchQuery.replace(/\btype:\w+\b/, '').trim(); } // Extract author const authorMatch = sourcegraphQuery.match(/\bauthor:([\w.-]+)\b/); if (authorMatch) { author = authorMatch[1]; // Remove the author: parameter from the search query searchQuery = searchQuery.replace(/\bauthor:[\w.-]+\b/, '').trim(); } // Extract date/after const afterMatch = sourcegraphQuery.match(/\bafter:(["'][^"']+["']|\S+)\b/); if (afterMatch) { after = afterMatch[1].replace(/["']/g, ''); // Remove the after: parameter from the search query searchQuery = searchQuery.replace(/\bafter:["'][^"']+["']|\bafter:\S+\b/, '').trim(); } // Extract repositories const repoRegex = /\brepo:(["'][^"']+["']|\S+)\b/g; let repoMatch; while ((repoMatch = repoRegex.exec(sourcegraphQuery)) !== null) { const repoName = repoMatch[1].replace(/["']/g, ''); repos.push(repoName); // We don't remove repo: from searchQuery as it's often needed in the final query } // Clean up any extra spaces searchQuery = searchQuery.replace(/\s+/g, ' ').trim(); return { type: searchType, query: searchQuery, author, after, repos, originalQuery // Keep the original query for reference }; } /** * Legacy rule-based parsing as fallback */ function parseWithRules(query) { // Default parameters let searchType = 'file'; let searchQuery = query; let author = undefined; let after = undefined; const repos = []; // Extract search type if (/\b(commit|commits)\b/i.test(query)) { searchType = 'commit'; } else if (/\b(diff|diffs|change|changes|pr|prs|pull request|pull requests)\b/i.test(query)) { searchType = 'diff'; } // Extract author const authorMatch = query.match(/\b(?:by|from|author)\s+([\w.-]+)\b/i); if (authorMatch) { author = authorMatch[1]; } // Extract date const dateMatch = query.match(/\b(?:after|since)\s+(\d{4}-\d{2}-\d{2})\b/i); if (dateMatch) { after = dateMatch[1]; } // Extract repositories const repoMatches = query.match(/\bin\s+repo(?:sitory)?\s+([\w\/.-]+)\b/ig); if (repoMatches) { repoMatches.forEach(match => { const repoName = match.replace(/\bin\s+repo(?:sitory)?\s+/i, ''); repos.push(repoName); }); } // Clean up the search query (remove extracted metadata) if (author) { searchQuery = searchQuery.replace(new RegExp(`\\b(?:by|from|author)\\s+${author}\\b`, 'i'), ''); } if (after) { searchQuery = searchQuery.replace(new RegExp(`\\b(?:after|since)\\s+${after}\\b`, 'i'), ''); } if (repos.length > 0) { repos.forEach(repo => { searchQuery = searchQuery.replace(new RegExp(`\\bin\\s+repo(?:sitory)?\\s+${repo}\\b`, 'i'), ''); }); } // Remove search type indicators from the query searchQuery = searchQuery .replace(/\b(search for|find|look for)\b/i, '') .replace(/\b(commit|commits|diff|diffs|change|changes|pr|prs|pull request|pull requests)\b/i, '') .replace(/\s+/g, ' ') .trim(); return { type: searchType, query: searchQuery, author, after, repos, originalQuery: query // Keep the original query for reference }; }