feat: Tool Compression MCP server for Phase 8
MCP server providing compressed versions of Read/Grep/Glob: - compressed_read: removes comments, blanks, collapses imports - compressed_grep: groups by file, dedupes adjacent matches - compressed_glob: collapses directories, shows type distribution Test results: 66.7% compression on sample file 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
121
dist/compressors/grep.js
vendored
Normal file
121
dist/compressors/grep.js
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Grep Compressor - Compress search results while preserving essential matches
|
||||
*
|
||||
* Strategies:
|
||||
* - Group by file
|
||||
* - Show first N matches per file + count
|
||||
* - Dedupe similar/adjacent matches
|
||||
* - Prioritize exact matches
|
||||
*/
|
||||
function parseGrepOutput(output) {
|
||||
const matches = [];
|
||||
const lines = output.split('\n').filter(l => l.trim());
|
||||
for (const line of lines) {
|
||||
// Parse format: file:line:content or file:line-content
|
||||
const match = line.match(/^(.+?):(\d+)[:-](.*)$/);
|
||||
if (match) {
|
||||
matches.push({
|
||||
file: match[1],
|
||||
line: parseInt(match[2]),
|
||||
content: match[3],
|
||||
});
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
function groupByFile(matches) {
|
||||
const grouped = new Map();
|
||||
for (const match of matches) {
|
||||
const existing = grouped.get(match.file) || [];
|
||||
existing.push(match);
|
||||
grouped.set(match.file, existing);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
function dedupeAdjacent(matches, threshold = 3) {
|
||||
if (matches.length <= 1)
|
||||
return matches;
|
||||
const result = [matches[0]];
|
||||
let skipped = 0;
|
||||
for (let i = 1; i < matches.length; i++) {
|
||||
const prev = result[result.length - 1];
|
||||
const curr = matches[i];
|
||||
// Skip if within threshold lines of previous match
|
||||
if (curr.line - prev.line <= threshold) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
result.push(curr);
|
||||
}
|
||||
// Add note about skipped adjacent matches
|
||||
if (skipped > 0 && result.length > 0) {
|
||||
const last = result[result.length - 1];
|
||||
result.push({
|
||||
file: last.file,
|
||||
line: -1,
|
||||
content: `[${skipped} adjacent matches omitted]`,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function compressGrep(output, options = {}) {
|
||||
const { maxMatchesPerFile = 3, maxTotalMatches = 20, dedupeAdjacent: shouldDedupe = true, showCounts = true, } = options;
|
||||
const matches = parseGrepOutput(output);
|
||||
const originalMatches = matches.length;
|
||||
if (originalMatches === 0) {
|
||||
return {
|
||||
content: 'No matches found.',
|
||||
originalMatches: 0,
|
||||
compressedMatches: 0,
|
||||
filesMatched: 0,
|
||||
savings: '0%',
|
||||
};
|
||||
}
|
||||
const grouped = groupByFile(matches);
|
||||
const filesMatched = grouped.size;
|
||||
const result = [];
|
||||
let totalShown = 0;
|
||||
// Sort files by match count (most matches first)
|
||||
const sortedFiles = Array.from(grouped.entries()).sort((a, b) => b[1].length - a[1].length);
|
||||
for (const [file, fileMatches] of sortedFiles) {
|
||||
if (totalShown >= maxTotalMatches) {
|
||||
const remaining = sortedFiles.length - result.filter(l => l.startsWith('## ')).length;
|
||||
if (remaining > 0) {
|
||||
result.push(`\n... [${remaining} more files with matches]`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Dedupe adjacent matches if configured
|
||||
let processed = shouldDedupe ? dedupeAdjacent(fileMatches) : fileMatches;
|
||||
// Limit matches per file
|
||||
const totalInFile = fileMatches.length;
|
||||
const shown = processed.slice(0, maxMatchesPerFile);
|
||||
const omitted = totalInFile - shown.length;
|
||||
result.push(`## ${file}`);
|
||||
if (showCounts && totalInFile > maxMatchesPerFile) {
|
||||
result.push(`(${totalInFile} matches, showing ${shown.length})`);
|
||||
}
|
||||
for (const match of shown) {
|
||||
if (match.line === -1) {
|
||||
result.push(` ${match.content}`);
|
||||
}
|
||||
else {
|
||||
result.push(` ${match.line}: ${match.content.trim()}`);
|
||||
totalShown++;
|
||||
}
|
||||
}
|
||||
if (omitted > 0) {
|
||||
result.push(` ... [${omitted} more matches in this file]`);
|
||||
}
|
||||
result.push('');
|
||||
}
|
||||
const compressedMatches = totalShown;
|
||||
const savings = ((1 - compressedMatches / originalMatches) * 100).toFixed(1);
|
||||
return {
|
||||
content: result.join('\n').trim(),
|
||||
originalMatches,
|
||||
compressedMatches,
|
||||
filesMatched,
|
||||
savings: `${savings}%`,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user