@vibe-kit/grok-cli
Version:
An open-source AI agent that brings the power of Grok directly into your terminal.
308 lines • 14.2 kB
JavaScript
import fs from "fs-extra";
import * as path from "path";
import axios from "axios";
import { ConfirmationService } from "../utils/confirmation-service.js";
export class MorphEditorTool {
confirmationService = ConfirmationService.getInstance();
morphApiKey;
morphBaseUrl = "https://api.morphllm.com/v1";
constructor(apiKey) {
this.morphApiKey = apiKey || process.env.MORPH_API_KEY || "";
if (!this.morphApiKey) {
console.warn("MORPH_API_KEY not found. Morph editor functionality will be limited.");
}
}
/**
* Use this tool to make an edit to an existing file.
*
* This will be read by a less intelligent model, which will quickly apply the edit. You should make it clear what the edit is, while also minimizing the unchanged code you write.
* When writing the edit, you should specify each edit in sequence, with the special comment // ... existing code ... to represent unchanged code in between edited lines.
*
* For example:
*
* // ... existing code ...
* FIRST_EDIT
* // ... existing code ...
* SECOND_EDIT
* // ... existing code ...
* THIRD_EDIT
* // ... existing code ...
*
* You should still bias towards repeating as few lines of the original file as possible to convey the change.
* But, each edit should contain sufficient context of unchanged lines around the code you're editing to resolve ambiguity.
* DO NOT omit spans of pre-existing code (or comments) without using the // ... existing code ... comment to indicate its absence. If you omit the existing code comment, the model may inadvertently delete these lines.
* If you plan on deleting a section, you must provide context before and after to delete it. If the initial code is ```code \n Block 1 \n Block 2 \n Block 3 \n code```, and you want to remove Block 2, you would output ```// ... existing code ... \n Block 1 \n Block 3 \n // ... existing code ...```.
* Make sure it is clear what the edit should be, and where it should be applied.
* Make edits to a file in a single edit_file call instead of multiple edit_file calls to the same file. The apply model can handle many distinct edits at once.
*/
async editFile(targetFile, instructions, codeEdit) {
try {
const resolvedPath = path.resolve(targetFile);
if (!(await fs.pathExists(resolvedPath))) {
return {
success: false,
error: `File not found: ${targetFile}`,
};
}
if (!this.morphApiKey) {
return {
success: false,
error: "MORPH_API_KEY not configured. Please set your Morph API key.",
};
}
// Read the initial code
const initialCode = await fs.readFile(resolvedPath, "utf-8");
// Check user confirmation before proceeding
const sessionFlags = this.confirmationService.getSessionFlags();
if (!sessionFlags.fileOperations && !sessionFlags.allOperations) {
const confirmationResult = await this.confirmationService.requestConfirmation({
operation: "Edit file with Morph Fast Apply",
filename: targetFile,
showVSCodeOpen: false,
content: `Instructions: ${instructions}\n\nEdit:\n${codeEdit}`,
}, "file");
if (!confirmationResult.confirmed) {
return {
success: false,
error: confirmationResult.feedback || "File edit cancelled by user",
};
}
}
// Call Morph Fast Apply API
const mergedCode = await this.callMorphApply(instructions, initialCode, codeEdit);
// Write the merged code back to file
await fs.writeFile(resolvedPath, mergedCode, "utf-8");
// Generate diff for display
const oldLines = initialCode.split("\n");
const newLines = mergedCode.split("\n");
const diff = this.generateDiff(oldLines, newLines, targetFile);
return {
success: true,
output: diff,
};
}
catch (error) {
return {
success: false,
error: `Error editing ${targetFile} with Morph: ${error.message}`,
};
}
}
async callMorphApply(instructions, initialCode, editSnippet) {
try {
const response = await axios.post(`${this.morphBaseUrl}/chat/completions`, {
model: "morph-v3-large",
messages: [
{
role: "user",
content: `<instruction>${instructions}</instruction>\n<code>${initialCode}</code>\n<update>${editSnippet}</update>`,
},
],
}, {
headers: {
"Authorization": `Bearer ${this.morphApiKey}`,
"Content-Type": "application/json",
},
});
if (!response.data.choices || !response.data.choices[0] || !response.data.choices[0].message) {
throw new Error("Invalid response format from Morph API");
}
return response.data.choices[0].message.content;
}
catch (error) {
if (error.response) {
throw new Error(`Morph API error (${error.response.status}): ${error.response.data}`);
}
throw error;
}
}
generateDiff(oldLines, newLines, filePath) {
const CONTEXT_LINES = 3;
const changes = [];
let i = 0, j = 0;
while (i < oldLines.length || j < newLines.length) {
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 };
let oldEnd = i;
let newEnd = j;
while (oldEnd < oldLines.length || newEnd < newLines.length) {
let matchFound = false;
let matchLength = 0;
for (let k = 0; k < Math.min(2, oldLines.length - oldEnd, newLines.length - newEnd); k++) {
if (oldEnd + k < oldLines.length &&
newEnd + k < newLines.length &&
oldLines[oldEnd + k] === newLines[newEnd + k]) {
matchLength++;
}
else {
break;
}
}
if (matchLength >= 2 || (oldEnd >= oldLines.length && newEnd >= newLines.length)) {
matchFound = true;
}
if (matchFound) {
break;
}
if (oldEnd < oldLines.length)
oldEnd++;
if (newEnd < newLines.length)
newEnd++;
}
changes.push({
oldStart: changeStart.old,
oldEnd: oldEnd,
newStart: changeStart.new,
newEnd: newEnd
});
i = oldEnd;
j = newEnd;
}
}
const hunks = [];
let accumulatedOffset = 0;
for (let changeIdx = 0; changeIdx < changes.length; changeIdx++) {
const change = changes[changeIdx];
let contextStart = Math.max(0, change.oldStart - CONTEXT_LINES);
let contextEnd = Math.min(oldLines.length, change.oldEnd + CONTEXT_LINES);
if (hunks.length > 0) {
const lastHunk = hunks[hunks.length - 1];
const lastHunkEnd = lastHunk.oldStart + lastHunk.oldCount;
if (lastHunkEnd >= contextStart) {
const oldHunkEnd = lastHunk.oldStart + lastHunk.oldCount;
const newContextEnd = Math.min(oldLines.length, change.oldEnd + CONTEXT_LINES);
for (let idx = oldHunkEnd; idx < change.oldStart; idx++) {
lastHunk.lines.push({ type: ' ', content: oldLines[idx] });
}
for (let idx = change.oldStart; idx < change.oldEnd; idx++) {
lastHunk.lines.push({ type: '-', content: oldLines[idx] });
}
for (let idx = change.newStart; idx < change.newEnd; idx++) {
lastHunk.lines.push({ type: '+', content: newLines[idx] });
}
for (let idx = change.oldEnd; idx < newContextEnd && idx < oldLines.length; idx++) {
lastHunk.lines.push({ type: ' ', content: oldLines[idx] });
}
lastHunk.oldCount = newContextEnd - lastHunk.oldStart;
lastHunk.newCount = lastHunk.oldCount + (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart);
continue;
}
}
const hunk = {
oldStart: contextStart + 1,
oldCount: contextEnd - contextStart,
newStart: contextStart + 1 + accumulatedOffset,
newCount: contextEnd - contextStart + (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart),
lines: []
};
for (let idx = contextStart; idx < change.oldStart; idx++) {
hunk.lines.push({ type: ' ', content: oldLines[idx] });
}
for (let idx = change.oldStart; idx < change.oldEnd; idx++) {
hunk.lines.push({ type: '-', content: oldLines[idx] });
}
for (let idx = change.newStart; idx < change.newEnd; idx++) {
hunk.lines.push({ type: '+', content: newLines[idx] });
}
for (let idx = change.oldEnd; idx < contextEnd && idx < oldLines.length; idx++) {
hunk.lines.push({ type: ' ', content: oldLines[idx] });
}
hunks.push(hunk);
accumulatedOffset += (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart);
}
let addedLines = 0;
let removedLines = 0;
for (const hunk of hunks) {
for (const line of hunk.lines) {
if (line.type === '+')
addedLines++;
if (line.type === '-')
removedLines++;
}
}
let summary = `Updated ${filePath} with Morph Fast Apply`;
if (addedLines > 0 && removedLines > 0) {
summary += ` - ${addedLines} addition${addedLines !== 1 ? "s" : ""} and ${removedLines} removal${removedLines !== 1 ? "s" : ""}`;
}
else if (addedLines > 0) {
summary += ` - ${addedLines} addition${addedLines !== 1 ? "s" : ""}`;
}
else if (removedLines > 0) {
summary += ` - ${removedLines} removal${removedLines !== 1 ? "s" : ""}`;
}
else if (changes.length === 0) {
return `No changes applied to ${filePath}`;
}
let diff = summary + "\n";
diff += `--- a/${filePath}\n`;
diff += `+++ b/${filePath}\n`;
for (const hunk of hunks) {
diff += `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@\n`;
for (const line of hunk.lines) {
diff += `${line.type}${line.content}\n`;
}
}
return diff.trim();
}
async view(filePath, viewRange) {
try {
const resolvedPath = path.resolve(filePath);
if (await fs.pathExists(resolvedPath)) {
const stats = await fs.stat(resolvedPath);
if (stats.isDirectory()) {
const files = await fs.readdir(resolvedPath);
return {
success: true,
output: `Directory contents of ${filePath}:\n${files.join("\n")}`,
};
}
const content = await fs.readFile(resolvedPath, "utf-8");
const lines = content.split("\n");
if (viewRange) {
const [start, end] = viewRange;
const selectedLines = lines.slice(start - 1, end);
const numberedLines = selectedLines
.map((line, idx) => `${start + idx}: ${line}`)
.join("\n");
return {
success: true,
output: `Lines ${start}-${end} of ${filePath}:\n${numberedLines}`,
};
}
const totalLines = lines.length;
const displayLines = totalLines > 10 ? lines.slice(0, 10) : lines;
const numberedLines = displayLines
.map((line, idx) => `${idx + 1}: ${line}`)
.join("\n");
const additionalLinesMessage = totalLines > 10 ? `\n... +${totalLines - 10} lines` : "";
return {
success: true,
output: `Contents of ${filePath}:\n${numberedLines}${additionalLinesMessage}`,
};
}
else {
return {
success: false,
error: `File or directory not found: ${filePath}`,
};
}
}
catch (error) {
return {
success: false,
error: `Error viewing ${filePath}: ${error.message}`,
};
}
}
setApiKey(apiKey) {
this.morphApiKey = apiKey;
}
getApiKey() {
return this.morphApiKey;
}
}
//# sourceMappingURL=morph-editor.js.map