@alvinveroy/codecompass
Version:
AI-powered MCP server for codebase navigation and LLM prompt optimization
416 lines (409 loc) • 22.9 kB
JavaScript
;
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 };
}