Skip to main content
Glama
editBlock.ts8.64 kB
// edit_block tool implementation // Enhanced str_replace with fuzzy fallback and diff visualization import fs from 'fs'; import path from 'path'; import { logAudit } from '../../audit.js'; import { detectLineEnding, normalizeLineEndings, describeLineEndingDifference } from './lineEndings.js'; import { recursiveFuzzyIndexOf, getSimilarityRatio, countOccurrences, DEFAULT_FUZZY_THRESHOLD } from './fuzzySearch.js'; import { generateDiff, formatInlineDiff, summarizeDiff } from './diffVisualizer.js'; export interface EditBlockArgs { path: string; search: string; replace: string; expectedReplacements?: number; fuzzyThreshold?: number; dryRun?: boolean; } export interface EditBlockResult { success: boolean; applied: boolean; message: string; diff?: string; fuzzyMatch?: { similarity: number; foundText: string; inlineDiff: string; }; occurrencesFound?: number; occurrencesExpected?: number; } /** * Handle edit_block tool call * Performs a search/replace operation with fuzzy fallback and diff preview */ export async function handleEditBlock(args: EditBlockArgs): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean; }> { const { path: filePath, search, replace, expectedReplacements = 1, fuzzyThreshold = DEFAULT_FUZZY_THRESHOLD, dryRun = false } = args; try { const result = await performEditBlock( filePath, search, replace, expectedReplacements, fuzzyThreshold, dryRun ); await logAudit('edit_block', { path: filePath, searchLength: search.length, replaceLength: replace.length, expectedReplacements, dryRun }, result); return { content: [{ type: 'text', text: formatEditBlockResponse(result) }], isError: !result.success }; } catch (error: any) { await logAudit('edit_block', args, null, error.message); return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true }; } } /** * Core edit_block logic */ async function performEditBlock( filePath: string, search: string, replace: string, expectedReplacements: number, fuzzyThreshold: number, dryRun: boolean ): Promise<EditBlockResult> { // Validate inputs if (search === '') { return { success: false, applied: false, message: 'Empty search strings are not allowed. Please provide a non-empty string to search for.' }; } // Check file exists if (!fs.existsSync(filePath)) { return { success: false, applied: false, message: `File not found: ${filePath}` }; } // Read file content const content = fs.readFileSync(filePath, 'utf-8'); const fileLineEnding = detectLineEnding(content); // Normalize search string to match file's line endings const normalizedSearch = normalizeLineEndings(search, fileLineEnding); const normalizedReplace = normalizeLineEndings(replace, fileLineEnding); // Count exact occurrences const exactCount = countOccurrences(content, normalizedSearch); // Case 1: Exact match found with correct count if (exactCount > 0 && exactCount === expectedReplacements) { // Perform the replacement let newContent: string; if (expectedReplacements === 1) { // Replace only the first occurrence const index = content.indexOf(normalizedSearch); newContent = content.substring(0, index) + normalizedReplace + content.substring(index + normalizedSearch.length); } else { // Replace all occurrences newContent = content.split(normalizedSearch).join(normalizedReplace); } // Generate diff for preview const diffResult = generateDiff(content, newContent, path.basename(filePath)); if (dryRun) { return { success: true, applied: false, message: `DRY RUN: Would apply ${expectedReplacements} replacement(s) to ${filePath}`, diff: diffResult.unified, occurrencesFound: exactCount, occurrencesExpected: expectedReplacements }; } // Apply the change fs.writeFileSync(filePath, newContent, 'utf-8'); return { success: true, applied: true, message: `Successfully applied ${expectedReplacements} replacement(s) to ${filePath}`, diff: diffResult.unified, occurrencesFound: exactCount, occurrencesExpected: expectedReplacements }; } // Case 2: Exact match found but wrong count if (exactCount > 0 && exactCount !== expectedReplacements) { return { success: false, applied: false, message: `Expected ${expectedReplacements} occurrence(s) but found ${exactCount} in ${filePath}.\n\n` + `Options:\n` + `1. Set expectedReplacements to ${exactCount} to replace all occurrences\n` + `2. Add more context to your search string to match only the specific occurrence(s) you want`, occurrencesFound: exactCount, occurrencesExpected: expectedReplacements }; } // Case 3: No exact match - try fuzzy search const fuzzyMatch = recursiveFuzzyIndexOf(content, normalizedSearch); const similarity = fuzzyMatch.similarity; // Check for line ending differences const lineEndingDiff = describeLineEndingDifference(search, content); // Generate inline diff to show what's different const inlineDiff = formatInlineDiff(normalizedSearch, fuzzyMatch.value); if (similarity >= fuzzyThreshold) { // Good fuzzy match found - show preview and request confirmation const previewContent = content.substring(0, fuzzyMatch.start) + normalizedReplace + content.substring(fuzzyMatch.end); const diffResult = generateDiff(content, previewContent, path.basename(filePath)); return { success: false, // Not applied automatically - requires confirmation applied: false, message: `Exact match not found, but found similar text with ${Math.round(similarity * 100)}% similarity.\n\n` + `Character differences:\n${inlineDiff}\n\n` + (lineEndingDiff ? `Note: ${lineEndingDiff}\n\n` : '') + `To apply this edit, use the exact text from the file:\n\`\`\`\n${fuzzyMatch.value}\n\`\`\`\n\n` + `Preview of changes:\n${diffResult.unified}`, diff: diffResult.unified, fuzzyMatch: { similarity, foundText: fuzzyMatch.value, inlineDiff }, occurrencesFound: 0, occurrencesExpected: expectedReplacements }; } // Case 4: No good match found return { success: false, applied: false, message: `Search text not found in ${filePath}.\n\n` + `The closest match was ${Math.round(similarity * 100)}% similar (threshold: ${Math.round(fuzzyThreshold * 100)}%).\n\n` + `Character differences:\n${inlineDiff}\n\n` + (lineEndingDiff ? `Note: ${lineEndingDiff}\n\n` : '') + `Suggestions:\n` + `1. Copy the exact text from the file\n` + `2. Use read_file_lines to view the current content\n` + `3. Check for whitespace or line ending differences`, fuzzyMatch: { similarity, foundText: fuzzyMatch.value, inlineDiff }, occurrencesFound: 0, occurrencesExpected: expectedReplacements }; } /** * Format the result for display */ function formatEditBlockResponse(result: EditBlockResult): string { let output = result.message; // Add diff if available and not already in message if (result.diff && !result.message.includes('Preview of changes')) { output += `\n\n**Changes:**\n\`\`\`diff\n${result.diff}\n\`\`\``; } return output; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/mnehmos.ooda.mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server