Optional Fast Apply (#61)
This commit is contained in:
1 parent
3ed04b3228
commit
3c8a2863e2
5 files changed
+502
-3
No files matched your search
@@ -10,6 +10,7 @@ A conversational AI CLI tool powered by Grok with intelligent text editor capabi
|
||||
- **📝 Smart File Operations**: AI automatically uses tools to view, create, and edit files
|
||||
- **⚡ Bash Integration**: Execute shell commands through natural conversation
|
||||
- **🔧 Automatic Tool Selection**: AI intelligently chooses the right tools for your requests
|
||||
- **🚀 Morph Fast Apply**: Optional high-speed code editing at 4,500+ tokens/sec with 98% accuracy
|
||||
- **🔌 MCP Tools**: Extend capabilities with Model Context Protocol servers (Linear, GitHub, etc.)
|
||||
- **💬 Interactive UI**: Beautiful terminal interface built with Ink
|
||||
- **🌍 Global Installation**: Install and use anywhere with `npm i -g @vibe-kit/grok-cli`
|
||||
@@ -19,6 +20,7 @@ A conversational AI CLI tool powered by Grok with intelligent text editor capabi
|
||||
### Prerequisites
|
||||
- Node.js 16+
|
||||
- Grok API key from X.AI
|
||||
- (Optional, Recommended) Morph API key for Fast Apply editing
|
||||
|
||||
### Global Installation (Recommended)
|
||||
```bash
|
||||
@@ -64,6 +66,21 @@ Create `~/.grok/user-settings.json`:
|
||||
}
|
||||
```
|
||||
|
||||
3. (Optional, Recommended) Get your Morph API key from [Morph Dashboard](https://morphllm.com/dashboard/api-keys)
|
||||
|
||||
4. Set up your Morph API key for Fast Apply editing (choose one method):
|
||||
|
||||
**Method 1: Environment Variable**
|
||||
```bash
|
||||
export MORPH_API_KEY=your_morph_api_key_here
|
||||
```
|
||||
|
||||
**Method 2: .env File**
|
||||
```bash
|
||||
# Add to your .env file
|
||||
MORPH_API_KEY=your_morph_api_key_here
|
||||
```
|
||||
|
||||
### Custom Base URL (Optional)
|
||||
|
||||
By default, the CLI uses `https://api.x.ai/v1` as the Grok API endpoint. You can configure a custom endpoint if needed (choose one method):
|
||||
@@ -291,6 +308,35 @@ Follow the existing code style and patterns in this project.
|
||||
|
||||
Grok will automatically load and follow these instructions when working in your project directory. The custom instructions are added to Grok's system prompt and take priority over default behavior.
|
||||
|
||||
## Morph Fast Apply (Optional)
|
||||
|
||||
Grok CLI supports Morph's Fast Apply model for high-speed code editing at **4,500+ tokens/sec with 98% accuracy**. This is an optional feature that provides lightning-fast file editing capabilities.
|
||||
|
||||
**Setup**: Configure your Morph API key following the [setup instructions](#setup) above.
|
||||
|
||||
### How It Works
|
||||
|
||||
When `MORPH_API_KEY` is configured:
|
||||
- **`edit_file` tool becomes available** alongside the standard `str_replace_editor`
|
||||
- **Optimized for complex edits**: Use for multi-line changes, refactoring, and large modifications
|
||||
- **Intelligent editing**: Uses abbreviated edit format with `// ... existing code ...` comments
|
||||
- **Fallback support**: Standard tools remain available if Morph is unavailable
|
||||
|
||||
**When to use each tool:**
|
||||
- **`edit_file`** (Morph): Complex edits, refactoring, multi-line changes
|
||||
- **`str_replace_editor`**: Simple text replacements, single-line edits
|
||||
|
||||
### Example Usage
|
||||
|
||||
With Morph Fast Apply configured, you can request complex code changes:
|
||||
|
||||
```bash
|
||||
grok --prompt "refactor this function to use async/await and add error handling"
|
||||
grok -p "convert this class to TypeScript and add proper type annotations"
|
||||
```
|
||||
|
||||
The AI will automatically choose between `edit_file` (Morph) for complex changes or `str_replace_editor` for simple replacements.
|
||||
|
||||
## MCP Tools
|
||||
|
||||
Grok CLI supports MCP (Model Context Protocol) servers, allowing you to extend the AI assistant with additional tools and capabilities.
|
||||
|
||||
+17
-1
@@ -9,6 +9,7 @@ import {
|
||||
import { loadMCPConfig } from "../mcp/config";
|
||||
import {
|
||||
TextEditorTool,
|
||||
MorphEditorTool,
|
||||
BashTool,
|
||||
TodoTool,
|
||||
ConfirmationTool,
|
||||
@@ -42,6 +43,7 @@ export interface StreamingChunk {
|
||||
export class GrokAgent extends EventEmitter {
|
||||
private grokClient: GrokClient;
|
||||
private textEditor: TextEditorTool;
|
||||
private morphEditor: MorphEditorTool | null;
|
||||
private bash: BashTool;
|
||||
private todoTool: TodoTool;
|
||||
private confirmationTool: ConfirmationTool;
|
||||
@@ -61,6 +63,7 @@ export class GrokAgent extends EventEmitter {
|
||||
this.maxToolRounds = maxToolRounds || 400;
|
||||
this.grokClient = new GrokClient(apiKey, modelToUse, baseURL);
|
||||
this.textEditor = new TextEditorTool();
|
||||
this.morphEditor = process.env.MORPH_API_KEY ? new MorphEditorTool() : null;
|
||||
this.bash = new BashTool();
|
||||
this.todoTool = new TodoTool();
|
||||
this.confirmationTool = new ConfirmationTool();
|
||||
@@ -84,7 +87,7 @@ export class GrokAgent extends EventEmitter {
|
||||
You have access to these tools:
|
||||
- view_file: View file contents or directory listings
|
||||
- create_file: Create new files with content (ONLY use this for files that don't exist yet)
|
||||
- str_replace_editor: Replace text in existing files (ALWAYS use this to edit or update existing files)
|
||||
- str_replace_editor: Replace text in existing files (ALWAYS use this to edit or update existing files)${this.morphEditor ? '\n- edit_file: High-speed file editing with Morph Fast Apply (4,500+ tokens/sec with 98% accuracy)' : ''}
|
||||
- bash: Execute bash commands (use for searching, file discovery, navigation, and system operations)
|
||||
- search: Unified search tool for finding text content or files (similar to Cursor's search functionality)
|
||||
- create_todo_list: Create a visual todo list for planning and tracking tasks
|
||||
@@ -618,6 +621,19 @@ Current working directory: ${process.cwd()}`,
|
||||
args.replace_all
|
||||
);
|
||||
|
||||
case "edit_file":
|
||||
if (!this.morphEditor) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Morph Fast Apply not available. Please set MORPH_API_KEY environment variable to use this feature.",
|
||||
};
|
||||
}
|
||||
return await this.morphEditor.editFile(
|
||||
args.target_file,
|
||||
args.instructions,
|
||||
args.code_edit
|
||||
);
|
||||
|
||||
case "bash":
|
||||
return await this.bash.execute(args.command);
|
||||
|
||||
|
||||
+45
-2
@@ -2,7 +2,7 @@ import { GrokTool } from "./client";
|
||||
import { MCPManager, MCPTool } from "../mcp/client";
|
||||
import { loadMCPConfig } from "../mcp/config";
|
||||
|
||||
export const GROK_TOOLS: GrokTool[] = [
|
||||
const BASE_GROK_TOOLS: GrokTool[] = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -54,7 +54,7 @@ export const GROK_TOOLS: GrokTool[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "str_replace_editor",
|
||||
description: "Replace specific text in a file",
|
||||
description: "Replace specific text in a file. Use this for single line edits only",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -81,6 +81,7 @@ export const GROK_TOOLS: GrokTool[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -242,6 +243,48 @@ export const GROK_TOOLS: GrokTool[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// Morph Fast Apply tool (conditional)
|
||||
const MORPH_EDIT_TOOL: GrokTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "edit_file",
|
||||
description: "Use this tool to make an edit to an existing file.\n\nThis 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.\nWhen writing the edit, you should specify each edit in sequence, with the special comment // ... existing code ... to represent unchanged code in between edited lines.\n\nFor example:\n\n// ... existing code ...\nFIRST_EDIT\n// ... existing code ...\nSECOND_EDIT\n// ... existing code ...\nTHIRD_EDIT\n// ... existing code ...\n\nYou should still bias towards repeating as few lines of the original file as possible to convey the change.\nBut, each edit should contain sufficient context of unchanged lines around the code you're editing to resolve ambiguity.\nDO 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.\nIf 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 ...```.\nMake sure it is clear what the edit should be, and where it should be applied.\nMake 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.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
target_file: {
|
||||
type: "string",
|
||||
description: "The target file to modify."
|
||||
},
|
||||
instructions: {
|
||||
type: "string",
|
||||
description: "A single sentence instruction describing what you are going to do for the sketched edit. This is used to assist the less intelligent model in applying the edit. Use the first person to describe what you are going to do. Use it to disambiguate uncertainty in the edit."
|
||||
},
|
||||
code_edit: {
|
||||
type: "string",
|
||||
description: "Specify ONLY the precise lines of code that you wish to edit. NEVER specify or write out unchanged code. Instead, represent all unchanged code using the comment of the language you're editing in - example: // ... existing code ..."
|
||||
}
|
||||
},
|
||||
required: ["target_file", "instructions", "code_edit"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Function to build tools array conditionally
|
||||
function buildGrokTools(): GrokTool[] {
|
||||
const tools = [...BASE_GROK_TOOLS];
|
||||
|
||||
// Add Morph Fast Apply tool if API key is available
|
||||
if (process.env.MORPH_API_KEY) {
|
||||
tools.splice(3, 0, MORPH_EDIT_TOOL); // Insert after str_replace_editor
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
// Export dynamic tools array
|
||||
export const GROK_TOOLS: GrokTool[] = buildGrokTools();
|
||||
|
||||
// Global MCP manager instance
|
||||
let mcpManager: MCPManager | null = null;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { BashTool } from "./bash";
|
||||
export { TextEditorTool } from "./text-editor";
|
||||
export { MorphEditorTool } from "./morph-editor";
|
||||
export { TodoTool } from "./todo-tool";
|
||||
export { ConfirmationTool } from "./confirmation-tool";
|
||||
export { SearchTool } from "./search";
|
||||
@@ -0,0 +1,393 @@
|
||||
import * as fs from "fs-extra";
|
||||
import * as path from "path";
|
||||
import axios from "axios";
|
||||
import { ToolResult } from "../types";
|
||||
import { ConfirmationService } from "../utils/confirmation-service";
|
||||
|
||||
export class MorphEditorTool {
|
||||
private confirmationService = ConfirmationService.getInstance();
|
||||
private morphApiKey: string;
|
||||
private morphBaseUrl: string = "https://api.morphllm.com/v1";
|
||||
|
||||
constructor(apiKey?: string) {
|
||||
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: string,
|
||||
instructions: string,
|
||||
codeEdit: string
|
||||
): Promise<ToolResult> {
|
||||
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: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Error editing ${targetFile} with Morph: ${error.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async callMorphApply(
|
||||
instructions: string,
|
||||
initialCode: string,
|
||||
editSnippet: string
|
||||
): Promise<string> {
|
||||
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: any) {
|
||||
if (error.response) {
|
||||
throw new Error(`Morph API error (${error.response.status}): ${error.response.data}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private generateDiff(
|
||||
oldLines: string[],
|
||||
newLines: string[],
|
||||
filePath: string
|
||||
): string {
|
||||
const CONTEXT_LINES = 3;
|
||||
|
||||
const changes: Array<{
|
||||
oldStart: number;
|
||||
oldEnd: number;
|
||||
newStart: number;
|
||||
newEnd: number;
|
||||
}> = [];
|
||||
|
||||
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: Array<{
|
||||
oldStart: number;
|
||||
oldCount: number;
|
||||
newStart: number;
|
||||
newCount: number;
|
||||
lines: Array<{ type: '+' | '-' | ' '; content: string }>;
|
||||
}> = [];
|
||||
|
||||
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: typeof hunks[0] = {
|
||||
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: string,
|
||||
viewRange?: [number, number]
|
||||
): Promise<ToolResult> {
|
||||
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: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Error viewing ${filePath}: ${error.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setApiKey(apiKey: string): void {
|
||||
this.morphApiKey = apiKey;
|
||||
}
|
||||
|
||||
getApiKey(): string {
|
||||
return this.morphApiKey;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user