@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
JavaScript
;
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