UNPKG

@alvinveroy/codecompass

Version:

AI-powered MCP server for codebase navigation and LLM prompt optimization

416 lines (409 loc) 22.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.capability_searchCodeSnippets = capability_searchCodeSnippets; exports.capability_getRepositoryOverview = capability_getRepositoryOverview; exports.capability_getChangelog = capability_getChangelog; exports.capability_fetchMoreSearchResults = capability_fetchMoreSearchResults; exports.capability_getFullFileContent = capability_getFullFileContent; exports.capability_listDirectory = capability_listDirectory; exports.capability_getAdjacentFileChunks = capability_getAdjacentFileChunks; exports.capability_generateSuggestionWithContext = capability_generateSuggestionWithContext; exports.capability_analyzeCodeProblemWithContext = capability_analyzeCodeProblemWithContext; const config_service_1 = require("./config-service"); // Added configService const query_refinement_1 = require("./query-refinement"); // Assuming this is where it is const agent_1 = require("./agent"); // Import existing helpers from agent.ts const promises_1 = __importDefault(require("fs/promises")); // For getChangelog, getFullFileContent, listDirectory const path_1 = __importDefault(require("path")); // For getChangelog, getFullFileContent, listDirectory const llm_provider_1 = require("./llm-provider"); // Alias for LLM-dependent caps // capability_searchCodeSnippets async function capability_searchCodeSnippets(context, params) { const { qdrantClient, suggestionModelAvailable } = context; // repoPath not directly used if files list is empty const { query } = params; config_service_1.logger.info(`Executing capability_searchCodeSnippets with query: ${params.query}`); // Assuming validateGitRepository and git.listFiles are handled by the orchestrator // or are not strictly needed for the capability if files list is passed or search is global. // For now, let's assume searchWithRefinement can handle an empty files array if repo context isn't pre-filtered. const { results: qdrantResults } = await (0, query_refinement_1.searchWithRefinement)(qdrantClient, query, [] // Pass empty array for files, or orchestrator needs to provide this. ); const formattedResultsPromises = qdrantResults.map(async (r) => { const payload = r.payload; let filepathDisplay = "N/A"; let snippetContent = "Content not available"; let isChunked = false; let originalFilepath = undefined; let chunkIndex = undefined; let totalChunks = undefined; let lastModified = undefined; if (payload?.dataType === 'file_chunk') { filepathDisplay = payload.filepath; snippetContent = payload.file_content_chunk; isChunked = true; originalFilepath = payload.filepath; chunkIndex = payload.chunk_index; totalChunks = payload.total_chunks; lastModified = payload.last_modified; if (isChunked) { filepathDisplay = `${payload.filepath} (Chunk ${(chunkIndex ?? 0) + 1}/${totalChunks ?? 'N/A'})`; } } else if (payload) { // Handle other types or log a warning if only file_chunk is expected here config_service_1.logger.warn(`capability_searchCodeSnippets: Received non-file_chunk payload type: ${payload.dataType} for result ID ${r.id}`); // Provide default/fallback values for FormattedSearchResult filepathDisplay = payload.filepath || `Unknown path (ID: ${r.id})`; snippetContent = `Non-file content (type: ${payload.dataType})`; } const processedSnippetContent = await (0, agent_1.processSnippet)(snippetContent, query, filepathDisplay, suggestionModelAvailable); return { filepath: filepathDisplay, snippet: processedSnippetContent, last_modified: lastModified, relevance: r.score, is_chunked: isChunked, original_filepath: originalFilepath, chunk_index: chunkIndex, total_chunks: totalChunks, }; }); return Promise.all(formattedResultsPromises); } // capability_getRepositoryOverview async function capability_getRepositoryOverview(context, params) { const { qdrantClient, repoPath, suggestionModelAvailable } = context; const { query } = params; config_service_1.logger.info(`Executing capability_getRepositoryOverview with query: ${params.query}`); const processedDiff = await (0, agent_1.getProcessedDiff)(repoPath, suggestionModelAvailable); const { results: qdrantResults, refinedQuery } = await (0, query_refinement_1.searchWithRefinement)(qdrantClient, query, [] // Assuming files list is not passed or handled by orchestrator ); const searchResultsPromises = qdrantResults.map(async (r) => { const payload = r.payload; let filepathDisplay = "N/A"; let snippetContent = "Content not available"; let isChunked = false; let originalFilepath = undefined; let chunkIndex = undefined; let totalChunks = undefined; let lastModified = undefined; // Populate based on payload type if (payload?.dataType === 'file_chunk') { filepathDisplay = payload.filepath; snippetContent = payload.file_content_chunk; isChunked = true; originalFilepath = payload.filepath; chunkIndex = payload.chunk_index; totalChunks = payload.total_chunks; lastModified = payload.last_modified; if (isChunked) { filepathDisplay = `${payload.filepath} (Chunk ${(chunkIndex ?? 0) + 1}/${totalChunks ?? 'N/A'})`; } } else if (payload?.dataType === 'commit_info') { filepathDisplay = `Commit: ${payload.commit_oid.substring(0, 7)}`; snippetContent = `Message: ${payload.commit_message}`; lastModified = payload.commit_date; } else if (payload?.dataType === 'diff_chunk') { filepathDisplay = `Diff: ${payload.filepath} (Commit: ${payload.commit_oid.substring(0, 7)})`; snippetContent = payload.diff_content_chunk; isChunked = true; originalFilepath = payload.filepath; chunkIndex = payload.chunk_index; totalChunks = payload.total_chunks; } else if (payload && typeof payload === 'object' && 'dataType' in payload) { // Check if payload is an object and has dataType const unknownPayload = payload; // Use a broader type for safe access config_service_1.logger.warn(`capability_getRepositoryOverview: Received payload with unhandled dataType '${unknownPayload.dataType || 'undefined'}' or unexpected structure for result ID ${r.id}`); filepathDisplay = unknownPayload.filepath || `Unknown path (ID: ${r.id})`; snippetContent = `Non-standard content (type: ${unknownPayload.dataType || 'unknown_structure'})`; } else if (payload) { // Payload exists but is not an object or doesn't have dataType config_service_1.logger.warn(`capability_getRepositoryOverview: Received malformed or unexpected payload structure for result ID ${r.id}`, { payload }); filepathDisplay = payload.filepath || `Unknown path (ID: ${r.id})`; // Keep existing fallback for filepath snippetContent = `Malformed payload (ID: ${r.id})`; } const processedSnippetContent = await (0, agent_1.processSnippet)(snippetContent, query, filepathDisplay, suggestionModelAvailable); return { filepath: filepathDisplay, snippet: processedSnippetContent, last_modified: lastModified, relevance: r.score, is_chunked: isChunked, original_filepath: originalFilepath, chunk_index: chunkIndex, total_chunks: totalChunks, }; }); const searchResults = await Promise.all(searchResultsPromises); return { refinedQuery, diffSummary: processedDiff, searchResults, }; } // capability_getChangelog async function capability_getChangelog(context, _params // No parameters ) { const { repoPath } = context; config_service_1.logger.info("Executing capability_getChangelog"); try { const changelogPath = path_1.default.join(repoPath, 'CHANGELOG.md'); const changelogContent = await promises_1.default.readFile(changelogPath, 'utf8'); return { changelog: changelogContent.substring(0, config_service_1.configService.MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY || 2000) // Use a config value }; } catch (_error) { if (_error?.code === 'ENOENT') { return { changelog: "No changelog found" }; } return { changelog: "No changelog available", error: "Failed to read changelog" }; } } // capability_fetchMoreSearchResults (similar to capability_searchCodeSnippets) async function capability_fetchMoreSearchResults(context, params) { const { qdrantClient, suggestionModelAvailable } = context; // repoPath not directly used if files list is empty const { query } = params; config_service_1.logger.info(`Executing capability_fetchMoreSearchResults with query: ${params.query}`); const moreResultsLimit = config_service_1.configService.REQUEST_ADDITIONAL_CONTEXT_MAX_SEARCH_RESULTS; const { results: qdrantResults } = await (0, query_refinement_1.searchWithRefinement)(qdrantClient, query, [], // Assuming files list is not passed or handled by orchestrator moreResultsLimit); const formattedResultsPromises = qdrantResults.map(async (r) => { const payload = r.payload; let filepathDisplay = "N/A"; let snippetContent = "Content not available"; let isChunked = false; let originalFilepath = undefined; let chunkIndex = undefined; let totalChunks = undefined; let lastModified = undefined; if (payload?.dataType === 'file_chunk') { filepathDisplay = payload.filepath; snippetContent = payload.file_content_chunk; isChunked = true; originalFilepath = payload.filepath; chunkIndex = payload.chunk_index; totalChunks = payload.total_chunks; lastModified = payload.last_modified; if (isChunked) { filepathDisplay = `${payload.filepath} (Chunk ${(chunkIndex ?? 0) + 1}/${totalChunks ?? 'N/A'})`; } } else if (payload) { config_service_1.logger.warn(`capability_fetchMoreSearchResults: Received non-file_chunk payload type: ${payload.dataType} for result ID ${r.id}`); filepathDisplay = payload.filepath || `Unknown path (ID: ${r.id})`; snippetContent = `Non-file content (type: ${payload.dataType})`; } const processedSnippetContent = await (0, agent_1.processSnippet)(snippetContent, query, filepathDisplay, suggestionModelAvailable); return { filepath: filepathDisplay, snippet: processedSnippetContent, last_modified: lastModified, relevance: r.score, is_chunked: isChunked, original_filepath: originalFilepath, chunk_index: chunkIndex, total_chunks: totalChunks, }; }); return Promise.all(formattedResultsPromises); } // capability_getFullFileContent async function capability_getFullFileContent(context, params) { const { repoPath, suggestionModelAvailable } = context; const { filepath } = params; config_service_1.logger.info(`Executing capability_getFullFileContent for path: ${params.filepath}`); const targetFilePath = path_1.default.resolve(repoPath, filepath); if (!targetFilePath.startsWith(path_1.default.resolve(repoPath))) { throw new Error(`Access denied: Path "${filepath}" is outside the repository.`); } try { let fileContent = await promises_1.default.readFile(targetFilePath, 'utf8'); const MAX_CONTENT_LENGTH = config_service_1.configService.MAX_FILE_CONTENT_LENGTH_FOR_CAPABILITY || 10000; // Use a config value if (fileContent.length > MAX_CONTENT_LENGTH) { if (suggestionModelAvailable) { try { const llmProvider = await (0, llm_provider_1.getLLMProvider)(); const summaryPrompt = `The user requested the full content of "${filepath}". The content is too long (${fileContent.length} characters). Summarize it concisely, focusing on its main purpose, key functions/classes, and overall structure. Keep the summary informative yet brief.\n\nFile Content (partial):\n${fileContent.substring(0, MAX_CONTENT_LENGTH * 2)}`; // Provide more for summary context fileContent = `Summary of ${filepath}:\n${await llmProvider.generateText(summaryPrompt)}`; config_service_1.logger.info(`Summarized large file content for ${filepath}`); } catch (summaryError) { const sErr = summaryError instanceof Error ? summaryError : new Error(String(summaryError)); config_service_1.logger.warn(`Failed to summarize full file content for ${filepath}. Using truncated content. Error: ${sErr.message}`); fileContent = `Content of ${filepath} is too large. Summary attempt failed. Truncated content:\n${fileContent.substring(0, MAX_CONTENT_LENGTH)}...`; } } else { config_service_1.logger.warn(`Suggestion model not available to summarize large file ${filepath}. Using truncated content.`); fileContent = `Content of ${filepath} is too large. Full content omitted as suggestion model is offline. Truncated content:\n${fileContent.substring(0, MAX_CONTENT_LENGTH)}...`; } } return { filepath, content: fileContent }; } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); config_service_1.logger.error(`Failed to read file "${filepath}": ${err.message}`); throw new Error(`Failed to read file "${filepath}": ${err.message}`); } } // capability_listDirectory async function capability_listDirectory(context, params) { const { repoPath } = context; const { dirPath } = params; config_service_1.logger.info(`Executing capability_listDirectory for path: ${params.dirPath}`); const targetDirPath = path_1.default.resolve(repoPath, dirPath); if (!targetDirPath.startsWith(path_1.default.resolve(repoPath))) { throw new Error(`Access denied: Path "${dirPath}" is outside the repository.`); } try { const entries = await promises_1.default.readdir(targetDirPath, { withFileTypes: true }); const listing = entries.map(entry => ({ name: entry.name, type: entry.isDirectory() ? 'directory' : 'file' })); const MAX_DIR_ENTRIES = config_service_1.configService.MAX_DIR_LISTING_ENTRIES_FOR_CAPABILITY || 50; // Use a config value if (listing.length > MAX_DIR_ENTRIES) { return { path: dirPath, listing: listing.slice(0, MAX_DIR_ENTRIES), note: `Listing truncated. Showing first ${MAX_DIR_ENTRIES} of ${listing.length} entries.` }; } return { path: dirPath, listing }; } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); config_service_1.logger.error(`Failed to list directory "${dirPath}": ${err.message}`); throw new Error(`Failed to list directory "${dirPath}": ${err.message}`); } } async function capability_getAdjacentFileChunks(context, params) { const { qdrantClient } = context; const { filepath, currentChunkIndex } = params; config_service_1.logger.info(`Executing capability_getAdjacentFileChunks for file: "${filepath}", current chunk: ${currentChunkIndex}`); const adjacentChunksResult = []; const chunksToFetchIndices = [currentChunkIndex - 1, currentChunkIndex + 1].filter(idx => idx >= 0); for (const targetIndex of chunksToFetchIndices) { try { const scrollResponse = await qdrantClient.scroll(config_service_1.configService.COLLECTION_NAME, { filter: { must: [ { key: "payload.dataType", match: { value: "file_chunk" } }, // Ensure we only get file_chunks { key: "payload.filepath", match: { value: filepath } }, { key: "payload.chunk_index", match: { value: targetIndex } } ] }, limit: 1, with_payload: true, with_vector: false, }); if (scrollResponse.points.length > 0 && scrollResponse.points[0].payload) { const pointPayload = scrollResponse.points[0].payload; // Type is already narrowed by filter if Qdrant respects it fully // Or, we can cast if confident, but better to check dataType if it could be mixed. // Given the filter, it *should* be FileChunkPayload. if (pointPayload.dataType === 'file_chunk') { // Explicit check for safety const fileChunkPayload = pointPayload; // Cast via unknown adjacentChunksResult.push({ filepath: fileChunkPayload.filepath, chunk_index: fileChunkPayload.chunk_index, snippet: fileChunkPayload.file_content_chunk, }); } else { // This case should ideally not be hit due to the Qdrant filter const actualDataType = (pointPayload && typeof pointPayload === 'object' && 'dataType' in pointPayload) ? pointPayload.dataType : 'unknown'; config_service_1.logger.warn(`capability_getAdjacentFileChunks: Expected file_chunk, got ${actualDataType} for ${filepath} chunk ${targetIndex}`); adjacentChunksResult.push({ filepath: filepath, chunk_index: targetIndex, snippet: "", note: `Chunk ${targetIndex} for file ${filepath} had unexpected data type ${actualDataType}.` }); } } else { adjacentChunksResult.push({ filepath: filepath, chunk_index: targetIndex, snippet: "", // Correct: No payload to access here, so snippet is empty note: `Chunk ${targetIndex} not found for file ${filepath}.` }); } } catch (searchError) { const sErr = searchError instanceof Error ? searchError : new Error(String(searchError)); config_service_1.logger.warn(`Failed to fetch chunk ${targetIndex} for ${filepath}: ${sErr.message}`); adjacentChunksResult.push({ filepath: filepath, chunk_index: targetIndex, snippet: "", note: `Error fetching chunk ${targetIndex} for file ${filepath}: ${sErr.message}` }); } } return { filepath: filepath, requested_chunk_index: currentChunkIndex, retrieved_chunks: adjacentChunksResult }; } // capability_generateSuggestionWithContext async function capability_generateSuggestionWithContext(context, // _context was used, now context is used for suggestionModelAvailable params) { const { query, repoPathName, filesContextString, diffSummary, recentQueriesStrings, relevantSnippets } = params; config_service_1.logger.info(`Executing capability_generateSuggestionWithContext for query: ${params.query}`); if (!context.suggestionModelAvailable) { return { suggestion: "Suggestion generation capability requires an LLM, which is currently not available." }; } const prompt = ` **Context**: Repository: ${repoPathName} Files: ${filesContextString} Recent Changes: ${diffSummary ? diffSummary.substring(0, 1000) : "Not available"}${diffSummary && diffSummary.length > 1000 ? "..." : ""} ${recentQueriesStrings.length > 0 ? `Recent Queries: ${recentQueriesStrings.join(", ")}` : ''} **Relevant Snippets**: ${relevantSnippets.map(c => `File: ${c.filepath} (Last modified: ${c.last_modified || 'N/A'}, Relevance: ${(c.relevance || 0).toFixed(2)})${c.is_chunked ? ` [Chunk ${(c.chunk_index ?? 0) + 1}/${c.total_chunks ?? 'N/A'} of ${c.original_filepath}]` : ''}\n${(c.snippet || "").substring(0, 500)}${(c.snippet || "").length > 500 ? "..." : ""}`).join("\n\n")} **Instruction**: Based on the provided context and snippets, generate a detailed code suggestion for "${query}". Include: - A suggested code implementation or improvement. - An explanation of how it addresses the query. - References to the provided snippets or context where applicable. `; const llmProvider = await (0, llm_provider_1.getLLMProvider)(); const suggestion = await llmProvider.generateText(prompt); return { suggestion: suggestion || "No suggestion generated." }; } // capability_analyzeCodeProblemWithContext async function capability_analyzeCodeProblemWithContext(context, // _context was used, now context is used params) { const { problemQuery, relevantSnippets } = params; config_service_1.logger.info(`Executing capability_analyzeCodeProblemWithContext for problem: ${params.problemQuery}`); if (!context.suggestionModelAvailable) { return { analysis: "Code problem analysis capability requires an LLM, which is currently not available." }; } const analysisPrompt = ` **Code Problem Analysis** Problem: ${problemQuery} **Relevant Code**: ${relevantSnippets.map(c => `File: ${c.filepath}${c.is_chunked ? ` [Chunk ${(c.chunk_index ?? 0) + 1}/${c.total_chunks ?? 'N/A'} of ${c.original_filepath}]` : ''}\n\`\`\`\n${(c.snippet || "").substring(0, 500)}${(c.snippet || "").length > 500 ? "..." : ""}\n\`\`\``).join("\n\n")} **Instructions**: 1. Analyze the problem described above. 2. Identify potential causes based on the code snippets. 3. List possible solutions. 4. Recommend the best approach. Structure your analysis with these sections: - Problem Understanding - Root Cause Analysis - Potential Solutions - Recommended Approach `; const llmProvider = await (0, llm_provider_1.getLLMProvider)(); const analysis = await llmProvider.generateText(analysisPrompt); return { analysis }; }