UNPKG

@vibe-kit/grok-cli

Version:

An open-source AI agent that brings the power of Grok directly into your terminal.

486 lines 20.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PatchEditor = void 0; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); class PatchEditor { /** * Apply a patch using context-based matching similar to OpenCode */ async applyPatch(filePath, operations) { try { const resolvedPath = path.resolve(filePath); if (!(await fs.pathExists(resolvedPath))) { return { success: false, error: `File not found: ${filePath}` }; } const content = await fs.readFile(resolvedPath, "utf-8"); const lines = content.split("\n"); const result = await this.processOperations(lines, operations); if (!result.success) { return { success: false, error: result.error || "Failed to apply patch" }; } const newContent = result.modifiedLines.join("\n"); await fs.writeFile(resolvedPath, newContent, "utf-8"); return { success: true, output: result.diff || `Successfully patched ${filePath}` }; } catch (error) { return { success: false, error: `Error applying patch to ${filePath}: ${error.message}` }; } } /** * Create a patch from search/replace with context */ async createContextualPatch(filePath, searchText, replaceText, contextLines = PatchEditor.CONTEXT_LINES) { try { const resolvedPath = path.resolve(filePath); if (!(await fs.pathExists(resolvedPath))) { return { success: false, error: `File not found: ${filePath}` }; } const content = await fs.readFile(resolvedPath, "utf-8"); const lines = content.split("\n"); const matchResult = this.findContextualMatch(lines, searchText, contextLines); if (!matchResult) { return { success: false, error: "Could not find search text with sufficient context" }; } const operations = [ { type: 'keep', lines: matchResult.contextBefore }, { type: 'remove', lines: matchResult.matchedLines, lineNumber: matchResult.startLine }, { type: 'add', lines: replaceText.split("\n"), lineNumber: matchResult.startLine }, { type: 'keep', lines: matchResult.contextAfter } ]; return this.applyPatch(filePath, operations); } catch (error) { return { success: false, error: `Error creating contextual patch: ${error.message}` }; } } /** * Smart string replacement using contextual matching */ async smartReplace(filePath, oldStr, newStr) { try { const resolvedPath = path.resolve(filePath); if (!(await fs.pathExists(resolvedPath))) { return { success: false, error: `File not found: ${filePath}` }; } const content = await fs.readFile(resolvedPath, "utf-8"); const lines = content.split("\n"); // First try exact match if (content.includes(oldStr)) { const newContent = content.replace(oldStr, newStr); await fs.writeFile(resolvedPath, newContent, "utf-8"); const oldLines = content.split("\n"); const newLines = newContent.split("\n"); const diff = this.generateUnifiedDiff(oldLines, newLines, filePath); return { success: true, output: diff }; } // Fall back to contextual matching return this.createContextualPatch(filePath, oldStr, newStr); } catch (error) { return { success: false, error: `Error in smart replace: ${error.message}` }; } } async processOperations(originalLines, operations) { const modifiedLines = []; const appliedChanges = []; let currentLineIndex = 0; for (const operation of operations) { switch (operation.type) { case 'keep': // Find and copy matching context lines const keepResult = this.findAndKeepLines(originalLines, currentLineIndex, operation.lines); if (!keepResult.found) { return { success: false, error: `Could not find expected context lines at position ${currentLineIndex}` }; } modifiedLines.push(...keepResult.matchedLines); currentLineIndex = keepResult.nextIndex; break; case 'remove': // Skip the lines that should be removed const removeResult = this.findAndRemoveLines(originalLines, currentLineIndex, operation.lines); if (!removeResult.found) { return { success: false, error: `Could not find lines to remove at position ${currentLineIndex}` }; } appliedChanges.push({ type: 'removal', oldStart: currentLineIndex, newStart: modifiedLines.length, oldLines: removeResult.removedLines, newLines: [] }); currentLineIndex = removeResult.nextIndex; break; case 'add': // Add new lines appliedChanges.push({ type: 'addition', oldStart: currentLineIndex, newStart: modifiedLines.length, oldLines: [], newLines: operation.lines }); modifiedLines.push(...operation.lines); break; } } // Add any remaining lines if (currentLineIndex < originalLines.length) { modifiedLines.push(...originalLines.slice(currentLineIndex)); } const diff = this.generateUnifiedDiff(originalLines, modifiedLines, "file"); return { success: true, modifiedLines, diff }; } findContextualMatch(lines, searchText, contextLines) { const searchLines = searchText.split("\n"); for (let i = 0; i <= lines.length - searchLines.length; i++) { const candidateLines = lines.slice(i, i + searchLines.length); if (this.linesMatch(candidateLines, searchLines)) { const contextStart = Math.max(0, i - contextLines); const contextEnd = Math.min(lines.length, i + searchLines.length + contextLines); return { startLine: i, endLine: i + searchLines.length - 1, matchedLines: candidateLines, contextBefore: lines.slice(contextStart, i), contextAfter: lines.slice(i + searchLines.length, contextEnd) }; } } return null; } findAndKeepLines(originalLines, startIndex, expectedLines) { if (expectedLines.length === 0) { return { found: true, matchedLines: [], nextIndex: startIndex }; } const endIndex = Math.min(startIndex + expectedLines.length, originalLines.length); const candidateLines = originalLines.slice(startIndex, endIndex); if (this.linesMatch(candidateLines, expectedLines)) { return { found: true, matchedLines: candidateLines, nextIndex: endIndex }; } // Try fuzzy matching const fuzzyResult = this.fuzzyMatchLines(originalLines, startIndex, expectedLines); if (fuzzyResult) { return fuzzyResult; } return { found: false, matchedLines: [], nextIndex: startIndex }; } findAndRemoveLines(originalLines, startIndex, linesToRemove) { if (linesToRemove.length === 0) { return { found: true, removedLines: [], nextIndex: startIndex }; } const endIndex = Math.min(startIndex + linesToRemove.length, originalLines.length); const candidateLines = originalLines.slice(startIndex, endIndex); if (this.linesMatch(candidateLines, linesToRemove)) { return { found: true, removedLines: candidateLines, nextIndex: endIndex }; } // Try fuzzy matching for removal const fuzzyResult = this.fuzzyMatchLines(originalLines, startIndex, linesToRemove); if (fuzzyResult) { return { found: true, removedLines: fuzzyResult.matchedLines, nextIndex: fuzzyResult.nextIndex }; } return { found: false, removedLines: [], nextIndex: startIndex }; } linesMatch(lines1, lines2) { if (lines1.length !== lines2.length) return false; return lines1.every((line, index) => this.normalizeForComparison(line) === this.normalizeForComparison(lines2[index])); } fuzzyMatchLines(originalLines, startIndex, expectedLines) { const maxSearchWindow = Math.min(expectedLines.length * 2, 10); // Reduced search window for (let offset = 0; offset <= maxSearchWindow; offset++) { const searchStart = startIndex + offset; const searchEnd = Math.min(searchStart + expectedLines.length, originalLines.length); if (searchEnd - searchStart !== expectedLines.length) continue; // Avoid creating new arrays unless necessary const similarity = this.calculateSimilarityInPlace(originalLines, searchStart, searchEnd, expectedLines); if (similarity >= PatchEditor.FUZZ_THRESHOLD) { return { found: true, matchedLines: originalLines.slice(searchStart, searchEnd), nextIndex: searchEnd }; } } return null; } calculateSimilarity(lines1, lines2) { if (lines1.length !== lines2.length) return 0; let matches = 0; const totalLines = lines1.length; for (let i = 0; i < totalLines; i++) { const similarity = this.stringSimilarity(this.normalizeForComparison(lines1[i]), this.normalizeForComparison(lines2[i])); if (similarity >= 0.8) matches++; } return matches / totalLines; } calculateSimilarityInPlace(originalLines, startIndex, endIndex, expectedLines) { const length = endIndex - startIndex; if (length !== expectedLines.length) return 0; let matches = 0; for (let i = 0; i < length; i++) { const originalLine = this.normalizeForComparison(originalLines[startIndex + i]); const expectedLine = this.normalizeForComparison(expectedLines[i]); // Quick exact match check first if (originalLine === expectedLine) { matches++; continue; } // Only do expensive similarity calculation if needed const similarity = this.stringSimilarity(originalLine, expectedLine); if (similarity >= 0.8) matches++; } return matches / length; } stringSimilarity(str1, str2) { if (str1 === str2) return 1; const longer = str1.length > str2.length ? str1 : str2; const shorter = str1.length > str2.length ? str2 : str1; if (longer.length === 0) return 1; const distance = this.levenshteinDistance(longer, shorter); return (longer.length - distance) / longer.length; } levenshteinDistance(str1, str2) { // Early exit for performance and memory optimization if (str1 === str2) return 0; if (str1.length === 0) return str2.length; if (str2.length === 0) return str1.length; // Limit comparison for very long strings to prevent memory issues const maxLength = 200; if (str1.length > maxLength || str2.length > maxLength) { const truncated1 = str1.substring(0, maxLength); const truncated2 = str2.substring(0, maxLength); return this.levenshteinDistanceOptimized(truncated1, truncated2); } return this.levenshteinDistanceOptimized(str1, str2); } levenshteinDistanceOptimized(str1, str2) { // Use only two rows instead of full matrix to save memory const shorter = str1.length <= str2.length ? str1 : str2; const longer = str1.length <= str2.length ? str2 : str1; let previousRow = Array(shorter.length + 1).fill(0).map((_, i) => i); let currentRow = Array(shorter.length + 1).fill(0); for (let i = 1; i <= longer.length; i++) { currentRow[0] = i; for (let j = 1; j <= shorter.length; j++) { const cost = longer[i - 1] === shorter[j - 1] ? 0 : 1; currentRow[j] = Math.min(currentRow[j - 1] + 1, // insertion previousRow[j] + 1, // deletion previousRow[j - 1] + cost // substitution ); } // Swap rows const temp = previousRow; previousRow = currentRow; currentRow = temp; } return previousRow[shorter.length]; } normalizeForComparison(str) { return str .replace(/\s+/g, ' ') .replace(/["'`]/g, '"') .trim(); } generateUnifiedDiff(oldLines, newLines, fileName) { const hunks = this.generateHunks(oldLines, newLines); if (hunks.length === 0) { return `No changes in ${fileName}`; } let addedLines = 0; let removedLines = 0; for (const hunk of hunks) { addedLines += hunk.addedLines.length; removedLines += hunk.removedLines.length; } let diff = `Updated ${fileName}`; if (addedLines > 0 && removedLines > 0) { diff += ` with ${addedLines} addition${addedLines !== 1 ? "s" : ""} and ${removedLines} removal${removedLines !== 1 ? "s" : ""}`; } else if (addedLines > 0) { diff += ` with ${addedLines} addition${addedLines !== 1 ? "s" : ""}`; } else if (removedLines > 0) { diff += ` with ${removedLines} removal${removedLines !== 1 ? "s" : ""}`; } diff += `\n--- a/${fileName}\n+++ b/${fileName}\n`; for (const hunk of hunks) { diff += `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@\n`; for (const line of hunk.contextBefore) { diff += ` ${line}\n`; } for (const line of hunk.removedLines) { diff += `-${line}\n`; } for (const line of hunk.addedLines) { diff += `+${line}\n`; } for (const line of hunk.contextAfter) { diff += ` ${line}\n`; } } return diff.trim(); } generateHunks(oldLines, newLines) { const hunks = []; const changes = this.findChanges(oldLines, newLines); for (const change of changes) { const contextStart = Math.max(0, change.oldStart - PatchEditor.CONTEXT_LINES); const contextEnd = Math.min(oldLines.length, change.oldEnd + PatchEditor.CONTEXT_LINES); const hunk = { oldStart: contextStart + 1, oldCount: contextEnd - contextStart, newStart: contextStart + 1, newCount: contextEnd - contextStart + (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart), contextBefore: oldLines.slice(contextStart, change.oldStart), contextAfter: oldLines.slice(change.oldEnd, contextEnd), removedLines: oldLines.slice(change.oldStart, change.oldEnd), addedLines: newLines.slice(change.newStart, change.newEnd) }; hunks.push(hunk); } return hunks; } findChanges(oldLines, newLines) { const changes = []; let i = 0, j = 0; while (i < oldLines.length || j < newLines.length) { // Skip matching lines while (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) { i++; j++; } if (i < oldLines.length || j < newLines.length) { const changeStart = { old: i, new: j }; // Find end of change while (i < oldLines.length || j < newLines.length) { // Look for matching sequence let matchFound = false; for (let k = 0; k < 3 && !matchFound; k++) { if (i + k < oldLines.length && j + k < newLines.length && oldLines[i + k] === newLines[j + k]) { matchFound = true; } } if (matchFound || (i >= oldLines.length && j >= newLines.length)) { break; } if (i < oldLines.length) i++; if (j < newLines.length) j++; } changes.push({ oldStart: changeStart.old, oldEnd: i, newStart: changeStart.new, newEnd: j }); } } return changes; } } exports.PatchEditor = PatchEditor; PatchEditor.CONTEXT_LINES = 3; PatchEditor.FUZZ_THRESHOLD = 0.8; //# sourceMappingURL=patch-editor.js.map