UNPKG

repomix

Version:

A tool to pack repository contents to single file for AI consumption

187 lines (186 loc) 7.89 kB
import fs from 'node:fs/promises'; import { z } from 'zod'; import { logger } from '../../shared/logger.js'; import { buildMcpToolErrorResponse, buildMcpToolSuccessResponse, convertErrorToJson, getOutputFilePath, } from './mcpToolRuntime.js'; const grepRepomixOutputInputSchema = z.object({ outputId: z.string().describe('ID of the Repomix output file to search'), pattern: z.string().describe('Search pattern (JavaScript RegExp regular expression syntax)'), contextLines: z.coerce .number() .default(0) .describe('Number of context lines to show before and after each match (default: 0). Overridden by beforeLines/afterLines if specified.'), beforeLines: z.coerce .number() .optional() .describe('Number of context lines to show before each match (like grep -B). Takes precedence over contextLines.'), afterLines: z.coerce .number() .optional() .describe('Number of context lines to show after each match (like grep -A). Takes precedence over contextLines.'), ignoreCase: z.boolean().default(false).describe('Perform case-insensitive matching (default: false)'), }); const grepRepomixOutputOutputSchema = z.object({ description: z.string().describe('Human-readable description of the search results'), matches: z .array(z.object({ lineNumber: z.number().describe('Line number where the match was found'), line: z.string().describe('The full line content'), matchedText: z.string().describe('The actual text that matched the pattern'), })) .describe('Array of search matches found'), formattedOutput: z.array(z.string()).describe('Formatted grep-style output with context lines'), totalMatches: z.number().describe('Total number of matches found'), pattern: z.string().describe('The search pattern that was used'), }); export const registerGrepRepomixOutputTool = (mcpServer) => { mcpServer.registerTool('grep_repomix_output', { title: 'Grep Repomix Output', description: 'Search for patterns in a Repomix output file using grep-like functionality with JavaScript RegExp syntax. Returns matching lines with optional context lines around matches.', inputSchema: grepRepomixOutputInputSchema, outputSchema: grepRepomixOutputOutputSchema, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ outputId, pattern, contextLines = 0, beforeLines, afterLines, ignoreCase = false, }) => { try { logger.trace(`Searching Repomix output with ID: ${outputId}, pattern: ${pattern}`); const filePath = getOutputFilePath(outputId); if (!filePath) { return buildMcpToolErrorResponse({ errorMessage: `Error: Output file with ID ${outputId} not found. The output file may have been deleted or the ID is invalid.`, details: { outputId, reason: 'FILE_NOT_FOUND', }, }); } try { await fs.access(filePath); } catch { return buildMcpToolErrorResponse({ errorMessage: `Error: Output file does not exist at path: ${filePath}. The temporary file may have been cleaned up.`, details: { outputId, reason: 'FILE_ACCESS_ERROR', }, }); } const content = await fs.readFile(filePath, 'utf8'); const finalBeforeLines = beforeLines !== undefined ? beforeLines : contextLines; const finalAfterLines = afterLines !== undefined ? afterLines : contextLines; let searchResult; try { searchResult = performGrepSearch(content, { pattern, contextLines, beforeLines: finalBeforeLines, afterLines: finalAfterLines, ignoreCase, }); } catch (error) { return buildMcpToolErrorResponse({ errorMessage: `Error: ${error instanceof Error ? error.message : String(error)}`, details: { outputId, pattern, reason: 'SEARCH_ERROR', }, }); } if (searchResult.matches.length === 0) { return buildMcpToolSuccessResponse({ description: `No matches found for pattern "${pattern}" in Repomix output file (ID: ${outputId}).`, matches: [], formattedOutput: [], totalMatches: 0, pattern, }); } return buildMcpToolSuccessResponse({ description: `Found ${searchResult.matches.length} match(es) for pattern "${pattern}" in Repomix output file (ID: ${outputId}):`, matches: searchResult.matches, formattedOutput: searchResult.formattedOutput, totalMatches: searchResult.matches.length, pattern, }); } catch (error) { logger.error(`Error in grep_repomix_output: ${error}`); return buildMcpToolErrorResponse(convertErrorToJson(error)); } }); }; export const createRegexPattern = (pattern, ignoreCase, deps = { RegExp, }) => { const regexFlags = ignoreCase ? 'gi' : 'g'; try { return new deps.RegExp(pattern, regexFlags); } catch (error) { throw new Error(`Invalid regular expression pattern: ${pattern}. ${error instanceof Error ? error.message : String(error)}`); } }; export const searchInContent = (content, options, deps = { createRegexPattern, }) => { return searchInLines(content.split('\n'), options, deps); }; export const searchInLines = (lines, options, deps = { createRegexPattern, }) => { const regex = deps.createRegexPattern(options.pattern, options.ignoreCase); const matches = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const match = line.match(regex); if (match) { matches.push({ lineNumber: i + 1, line, matchedText: match[0], }); } } return matches; }; export const formatSearchResults = (lines, matches, beforeLines, afterLines) => { if (matches.length === 0) { return []; } const resultLines = []; const addedLines = new Set(); for (const match of matches) { const start = Math.max(0, match.lineNumber - 1 - beforeLines); const end = Math.min(lines.length - 1, match.lineNumber - 1 + afterLines); if (resultLines.length > 0 && start > Math.min(...addedLines) + 1) { resultLines.push('--'); } for (let i = start; i <= end; i++) { if (!addedLines.has(i)) { const lineNum = i + 1; const prefix = i === match.lineNumber - 1 ? `${lineNum}:` : `${lineNum}-`; resultLines.push(`${prefix}${lines[i]}`); addedLines.add(i); } } } return resultLines; }; export const performGrepSearch = (content, options, deps = { searchInLines, formatSearchResults, }) => { const lines = content.split('\n'); const matches = deps.searchInLines(lines, options); const formattedOutput = deps.formatSearchResults(lines, matches, options.beforeLines, options.afterLines); return { matches, formattedOutput, }; };