wave-agent-sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/agent.d.ts +96 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +286 -0
- package/dist/hooks/executor.d.ts +56 -0
- package/dist/hooks/executor.d.ts.map +1 -0
- package/dist/hooks/executor.js +312 -0
- package/dist/hooks/index.d.ts +17 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +14 -0
- package/dist/hooks/manager.d.ts +90 -0
- package/dist/hooks/manager.d.ts.map +1 -0
- package/dist/hooks/manager.js +395 -0
- package/dist/hooks/matcher.d.ts +49 -0
- package/dist/hooks/matcher.d.ts.map +1 -0
- package/dist/hooks/matcher.js +147 -0
- package/dist/hooks/settings.d.ts +46 -0
- package/dist/hooks/settings.d.ts.map +1 -0
- package/dist/hooks/settings.js +100 -0
- package/dist/hooks/types.d.ts +80 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +59 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/managers/aiManager.d.ts +61 -0
- package/dist/managers/aiManager.d.ts.map +1 -0
- package/dist/managers/aiManager.js +415 -0
- package/dist/managers/backgroundBashManager.d.ts +27 -0
- package/dist/managers/backgroundBashManager.d.ts.map +1 -0
- package/dist/managers/backgroundBashManager.js +166 -0
- package/dist/managers/bashManager.d.ts +20 -0
- package/dist/managers/bashManager.d.ts.map +1 -0
- package/dist/managers/bashManager.js +66 -0
- package/dist/managers/mcpManager.d.ts +63 -0
- package/dist/managers/mcpManager.d.ts.map +1 -0
- package/dist/managers/mcpManager.js +378 -0
- package/dist/managers/messageManager.d.ts +85 -0
- package/dist/managers/messageManager.d.ts.map +1 -0
- package/dist/managers/messageManager.js +265 -0
- package/dist/managers/skillManager.d.ts +59 -0
- package/dist/managers/skillManager.d.ts.map +1 -0
- package/dist/managers/skillManager.js +317 -0
- package/dist/managers/slashCommandManager.d.ts +77 -0
- package/dist/managers/slashCommandManager.d.ts.map +1 -0
- package/dist/managers/slashCommandManager.js +208 -0
- package/dist/managers/toolManager.d.ts +23 -0
- package/dist/managers/toolManager.d.ts.map +1 -0
- package/dist/managers/toolManager.js +79 -0
- package/dist/services/aiService.d.ts +28 -0
- package/dist/services/aiService.d.ts.map +1 -0
- package/dist/services/aiService.js +180 -0
- package/dist/services/memory.d.ts +8 -0
- package/dist/services/memory.d.ts.map +1 -0
- package/dist/services/memory.js +128 -0
- package/dist/services/session.d.ts +54 -0
- package/dist/services/session.d.ts.map +1 -0
- package/dist/services/session.js +196 -0
- package/dist/tools/bashTool.d.ts +14 -0
- package/dist/tools/bashTool.d.ts.map +1 -0
- package/dist/tools/bashTool.js +351 -0
- package/dist/tools/deleteFileTool.d.ts +6 -0
- package/dist/tools/deleteFileTool.d.ts.map +1 -0
- package/dist/tools/deleteFileTool.js +67 -0
- package/dist/tools/editTool.d.ts +6 -0
- package/dist/tools/editTool.d.ts.map +1 -0
- package/dist/tools/editTool.js +168 -0
- package/dist/tools/globTool.d.ts +6 -0
- package/dist/tools/globTool.d.ts.map +1 -0
- package/dist/tools/globTool.js +113 -0
- package/dist/tools/grepTool.d.ts +6 -0
- package/dist/tools/grepTool.d.ts.map +1 -0
- package/dist/tools/grepTool.js +268 -0
- package/dist/tools/lsTool.d.ts +6 -0
- package/dist/tools/lsTool.d.ts.map +1 -0
- package/dist/tools/lsTool.js +160 -0
- package/dist/tools/multiEditTool.d.ts +6 -0
- package/dist/tools/multiEditTool.d.ts.map +1 -0
- package/dist/tools/multiEditTool.js +222 -0
- package/dist/tools/readTool.d.ts +6 -0
- package/dist/tools/readTool.d.ts.map +1 -0
- package/dist/tools/readTool.js +136 -0
- package/dist/tools/types.d.ts +35 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/writeTool.d.ts +6 -0
- package/dist/tools/writeTool.d.ts.map +1 -0
- package/dist/tools/writeTool.js +138 -0
- package/dist/types.d.ts +212 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/utils/bashHistory.d.ts +46 -0
- package/dist/utils/bashHistory.d.ts.map +1 -0
- package/dist/utils/bashHistory.js +236 -0
- package/dist/utils/commandArgumentParser.d.ts +34 -0
- package/dist/utils/commandArgumentParser.d.ts.map +1 -0
- package/dist/utils/commandArgumentParser.js +123 -0
- package/dist/utils/constants.d.ts +27 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +28 -0
- package/dist/utils/convertMessagesForAPI.d.ts +9 -0
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -0
- package/dist/utils/convertMessagesForAPI.js +189 -0
- package/dist/utils/customCommands.d.ts +14 -0
- package/dist/utils/customCommands.d.ts.map +1 -0
- package/dist/utils/customCommands.js +71 -0
- package/dist/utils/fileFilter.d.ts +26 -0
- package/dist/utils/fileFilter.d.ts.map +1 -0
- package/dist/utils/fileFilter.js +177 -0
- package/dist/utils/markdownParser.d.ts +27 -0
- package/dist/utils/markdownParser.d.ts.map +1 -0
- package/dist/utils/markdownParser.js +109 -0
- package/dist/utils/mcpUtils.d.ts +24 -0
- package/dist/utils/mcpUtils.d.ts.map +1 -0
- package/dist/utils/mcpUtils.js +51 -0
- package/dist/utils/messageOperations.d.ts +118 -0
- package/dist/utils/messageOperations.d.ts.map +1 -0
- package/dist/utils/messageOperations.js +334 -0
- package/dist/utils/path.d.ts +25 -0
- package/dist/utils/path.d.ts.map +1 -0
- package/dist/utils/path.js +109 -0
- package/dist/utils/skillParser.d.ts +18 -0
- package/dist/utils/skillParser.d.ts.map +1 -0
- package/dist/utils/skillParser.js +147 -0
- package/dist/utils/stringUtils.d.ts +13 -0
- package/dist/utils/stringUtils.d.ts.map +1 -0
- package/dist/utils/stringUtils.js +44 -0
- package/package.json +51 -0
- package/src/agent.ts +405 -0
- package/src/hooks/executor.ts +440 -0
- package/src/hooks/index.ts +52 -0
- package/src/hooks/manager.ts +618 -0
- package/src/hooks/matcher.ts +187 -0
- package/src/hooks/settings.ts +129 -0
- package/src/hooks/types.ts +169 -0
- package/src/index.ts +24 -0
- package/src/managers/aiManager.ts +573 -0
- package/src/managers/backgroundBashManager.ts +203 -0
- package/src/managers/bashManager.ts +97 -0
- package/src/managers/mcpManager.ts +493 -0
- package/src/managers/messageManager.ts +415 -0
- package/src/managers/skillManager.ts +404 -0
- package/src/managers/slashCommandManager.ts +293 -0
- package/src/managers/toolManager.ts +106 -0
- package/src/services/aiService.ts +252 -0
- package/src/services/memory.ts +149 -0
- package/src/services/session.ts +265 -0
- package/src/tools/bashTool.ts +402 -0
- package/src/tools/deleteFileTool.ts +81 -0
- package/src/tools/editTool.ts +192 -0
- package/src/tools/globTool.ts +135 -0
- package/src/tools/grepTool.ts +326 -0
- package/src/tools/lsTool.ts +187 -0
- package/src/tools/multiEditTool.ts +268 -0
- package/src/tools/readTool.ts +165 -0
- package/src/tools/types.ts +47 -0
- package/src/tools/writeTool.ts +163 -0
- package/src/types.ts +260 -0
- package/src/utils/bashHistory.ts +303 -0
- package/src/utils/commandArgumentParser.ts +153 -0
- package/src/utils/constants.ts +37 -0
- package/src/utils/convertMessagesForAPI.ts +236 -0
- package/src/utils/customCommands.ts +85 -0
- package/src/utils/fileFilter.ts +202 -0
- package/src/utils/markdownParser.ts +156 -0
- package/src/utils/mcpUtils.ts +81 -0
- package/src/utils/messageOperations.ts +506 -0
- package/src/utils/path.ts +118 -0
- package/src/utils/skillParser.ts +188 -0
- package/src/utils/stringUtils.ts +50 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { readFile, writeFile } from "fs/promises";
|
|
2
|
+
import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
3
|
+
import { resolvePath, getDisplayPath } from "../utils/path.js";
|
|
4
|
+
import { diffLines } from "diff";
|
|
5
|
+
|
|
6
|
+
interface EditOperation {
|
|
7
|
+
old_string: string;
|
|
8
|
+
new_string: string;
|
|
9
|
+
replace_all?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format compact parameter display
|
|
14
|
+
*/
|
|
15
|
+
function formatCompactParams(
|
|
16
|
+
args: Record<string, unknown>,
|
|
17
|
+
context: ToolContext,
|
|
18
|
+
): string {
|
|
19
|
+
const filePath = args.file_path as string;
|
|
20
|
+
const edits = args.edits as EditOperation[];
|
|
21
|
+
const editCount = edits ? edits.length : 0;
|
|
22
|
+
const displayPath = getDisplayPath(filePath || "", context.workdir);
|
|
23
|
+
return `${displayPath} ${editCount} edits`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Multi-edit tool plugin
|
|
28
|
+
*/
|
|
29
|
+
export const multiEditTool: ToolPlugin = {
|
|
30
|
+
name: "MultiEdit",
|
|
31
|
+
formatCompactParams,
|
|
32
|
+
config: {
|
|
33
|
+
type: "function",
|
|
34
|
+
function: {
|
|
35
|
+
name: "MultiEdit",
|
|
36
|
+
description:
|
|
37
|
+
"This is a tool for making multiple edits to a single file in one operation. It is built on top of the Edit tool and allows you to perform multiple find-and-replace operations efficiently. Prefer this tool over the Edit tool when you need to make multiple edits to the same file.\n\nBefore using this tool:\n\n1. Use the Read tool to understand the file's contents and context\n2. Verify the directory path is correct\n\nTo make multiple file edits, provide the following:\n1. file_path: The absolute path to the file to modify (must be absolute, not relative)\n2. edits: An array of edit operations to perform, where each edit contains:\n - old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation)\n - new_string: The edited text to replace the old_string\n - replace_all: Replace all occurences of old_string. This parameter is optional and defaults to false.\n\nIMPORTANT:\n- All edits are applied in sequence, in the order they are provided\n- Each edit operates on the result of the previous edit\n- All edits must be valid for the operation to succeed - if any edit fails, none will be applied\n- This tool is ideal when you need to make several changes to different parts of the same file\n- For Jupyter notebooks (.ipynb files), use the NotebookEdit instead\n\nCRITICAL REQUIREMENTS:\n1. All edits follow the same requirements as the single Edit tool\n2. The edits are atomic - either all succeed or none are applied\n3. Plan your edits carefully to avoid conflicts between sequential operations\n\nWARNING:\n- The tool will fail if edits.old_string doesn't match the file contents exactly (including whitespace)\n- The tool will fail if edits.old_string and edits.new_string are the same\n- Since edits are applied in sequence, ensure that earlier edits don't affect the text that later edits are trying to find\n\nWhen making edits:\n- Ensure all edits result in idiomatic, correct code\n- Do not leave the code in a broken state\n- Always use absolute file paths (starting with /)\n- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.\n- Use replace_all for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.\n\nIf you want to create a new file, use:\n- A new file path, including dir name if needed\n- First edit: empty old_string and the new file's contents as new_string\n- Subsequent edits: normal edit operations on the created content",
|
|
38
|
+
parameters: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
file_path: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "The absolute path to the file to modify",
|
|
44
|
+
},
|
|
45
|
+
edits: {
|
|
46
|
+
type: "array",
|
|
47
|
+
items: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
old_string: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "The text to replace",
|
|
53
|
+
},
|
|
54
|
+
new_string: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "The text to replace it with",
|
|
57
|
+
},
|
|
58
|
+
replace_all: {
|
|
59
|
+
type: "boolean",
|
|
60
|
+
default: false,
|
|
61
|
+
description:
|
|
62
|
+
"Replace all occurences of old_string (default false).",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
required: ["old_string", "new_string"],
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
},
|
|
68
|
+
minItems: 1,
|
|
69
|
+
description:
|
|
70
|
+
"Array of edit operations to perform sequentially on the file",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: ["file_path", "edits"],
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
execute: async (
|
|
79
|
+
args: Record<string, unknown>,
|
|
80
|
+
context: ToolContext,
|
|
81
|
+
): Promise<ToolResult> => {
|
|
82
|
+
const filePath = args.file_path as string;
|
|
83
|
+
const edits = args.edits as EditOperation[];
|
|
84
|
+
|
|
85
|
+
// Validate required parameters
|
|
86
|
+
if (!filePath || typeof filePath !== "string") {
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
content: "",
|
|
90
|
+
error: "file_path parameter is required and must be a string",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
content: "",
|
|
98
|
+
error: "edits parameter is required and must be a non-empty array",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Validate each edit operation
|
|
103
|
+
for (let i = 0; i < edits.length; i++) {
|
|
104
|
+
const edit = edits[i];
|
|
105
|
+
if (!edit || typeof edit !== "object") {
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
content: "",
|
|
109
|
+
error: `Edit operation ${i + 1} must be an object`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (typeof edit.old_string !== "string") {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
content: "",
|
|
117
|
+
error: `Edit operation ${i + 1}: old_string is required and must be a string`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof edit.new_string !== "string") {
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
content: "",
|
|
125
|
+
error: `Edit operation ${i + 1}: new_string is required and must be a string`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (edit.old_string === edit.new_string) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
content: "",
|
|
133
|
+
error: `Edit operation ${i + 1}: old_string and new_string must be different`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const resolvedPath = resolvePath(filePath, context.workdir);
|
|
140
|
+
|
|
141
|
+
// Read file content
|
|
142
|
+
let originalContent: string;
|
|
143
|
+
let isNewFile = false;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
originalContent = await readFile(resolvedPath, "utf-8");
|
|
147
|
+
} catch (readError) {
|
|
148
|
+
// Check if this is a new file creation case (first edit has empty old_string)
|
|
149
|
+
if (edits[0] && edits[0].old_string === "") {
|
|
150
|
+
originalContent = "";
|
|
151
|
+
isNewFile = true;
|
|
152
|
+
// logger.info(`Creating new file: ${resolvedPath}`);
|
|
153
|
+
} else {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
content: "",
|
|
157
|
+
error: `Failed to read file: ${readError instanceof Error ? readError.message : String(readError)}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let currentContent = originalContent;
|
|
163
|
+
const appliedEdits: string[] = [];
|
|
164
|
+
|
|
165
|
+
// Apply each edit operation in sequence
|
|
166
|
+
for (let i = 0; i < edits.length; i++) {
|
|
167
|
+
const edit = edits[i];
|
|
168
|
+
const replaceAll = edit.replace_all || false;
|
|
169
|
+
|
|
170
|
+
// Special handling for the first edit of new file creation
|
|
171
|
+
if (isNewFile && i === 0 && edit.old_string === "") {
|
|
172
|
+
currentContent = edit.new_string;
|
|
173
|
+
appliedEdits.push(
|
|
174
|
+
`Created file with content (${edit.new_string.length} characters)`,
|
|
175
|
+
);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check if old_string exists
|
|
180
|
+
if (!currentContent.includes(edit.old_string)) {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
content: "",
|
|
184
|
+
error: `Edit operation ${i + 1}: old_string not found in current content: "${edit.old_string}"`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let replacementCount: number;
|
|
189
|
+
|
|
190
|
+
if (replaceAll) {
|
|
191
|
+
// Replace all matches
|
|
192
|
+
const regex = new RegExp(escapeRegExp(edit.old_string), "g");
|
|
193
|
+
currentContent = currentContent.replace(regex, edit.new_string);
|
|
194
|
+
replacementCount = (currentContent.match(regex) || []).length;
|
|
195
|
+
appliedEdits.push(
|
|
196
|
+
`Replaced ${replacementCount} instances of "${edit.old_string.substring(0, 50)}${edit.old_string.length > 50 ? "..." : ""}"`,
|
|
197
|
+
);
|
|
198
|
+
} else {
|
|
199
|
+
// Replace only the first match, but first check if it's unique
|
|
200
|
+
const matches = currentContent.split(edit.old_string).length - 1;
|
|
201
|
+
if (matches > 1) {
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
content: "",
|
|
205
|
+
error: `Edit operation ${i + 1}: old_string appears ${matches} times in the current content. Either provide a larger string with more surrounding context to make it unique or use replace_all=true.`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
currentContent = currentContent.replace(
|
|
210
|
+
edit.old_string,
|
|
211
|
+
edit.new_string,
|
|
212
|
+
);
|
|
213
|
+
appliedEdits.push(
|
|
214
|
+
`Replaced "${edit.old_string.substring(0, 50)}${edit.old_string.length > 50 ? "..." : ""}"`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Write file
|
|
220
|
+
try {
|
|
221
|
+
await writeFile(resolvedPath, currentContent, "utf-8");
|
|
222
|
+
} catch (writeError) {
|
|
223
|
+
return {
|
|
224
|
+
success: false,
|
|
225
|
+
content: "",
|
|
226
|
+
error: `Failed to write file: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Generate diff information
|
|
231
|
+
const diffResult = diffLines(originalContent, currentContent);
|
|
232
|
+
|
|
233
|
+
const shortResult = isNewFile
|
|
234
|
+
? `Created file with ${edits.length} operations`
|
|
235
|
+
: `Applied ${edits.length} edits`;
|
|
236
|
+
|
|
237
|
+
const detailedContent = `${shortResult}\n\nOperations performed:\n${appliedEdits.map((edit, i) => `${i + 1}. ${edit}`).join("\n")}`;
|
|
238
|
+
|
|
239
|
+
// logger.info(`MultiEdit tool: ${shortResult}`);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
success: true,
|
|
243
|
+
content: detailedContent,
|
|
244
|
+
shortResult,
|
|
245
|
+
filePath: resolvedPath,
|
|
246
|
+
originalContent,
|
|
247
|
+
newContent: currentContent,
|
|
248
|
+
diffResult,
|
|
249
|
+
};
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const errorMessage =
|
|
252
|
+
error instanceof Error ? error.message : String(error);
|
|
253
|
+
// logger.error(`MultiEdit tool error: ${errorMessage}`);
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
content: "",
|
|
257
|
+
error: errorMessage,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Escape regular expression special characters
|
|
265
|
+
*/
|
|
266
|
+
function escapeRegExp(string: string): string {
|
|
267
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
268
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
3
|
+
import { resolvePath, getDisplayPath } from "../utils/path.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read Tool Plugin - Read file content
|
|
7
|
+
*/
|
|
8
|
+
export const readTool: ToolPlugin = {
|
|
9
|
+
name: "Read",
|
|
10
|
+
config: {
|
|
11
|
+
type: "function",
|
|
12
|
+
function: {
|
|
13
|
+
name: "Read",
|
|
14
|
+
description:
|
|
15
|
+
"Reads a file from the local filesystem. You can access any file directly by using this tool.\nAssume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to 2000 lines starting from the beginning of the file\n- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than 2000 characters will be truncated\n- Results are returned using cat -n format, with line numbers starting at 1\n- This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.\n- This tool can read PDF files (.pdf). PDFs are processed page by page, extracting both text and visual content for analysis.\n- This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs, combining code, text, and visualizations.\n- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.\n- You will regularly be asked to read screenshots. If the user provides a path to a screenshot ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths like /var/folders/123/abc/T/TemporaryItems/NSIRD_screencaptureui_ZfB1tD/Screenshot.png\n- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.",
|
|
16
|
+
parameters: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
file_path: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "The absolute path to the file to read",
|
|
22
|
+
},
|
|
23
|
+
offset: {
|
|
24
|
+
type: "number",
|
|
25
|
+
description:
|
|
26
|
+
"The line number to start reading from. Only provide if the file is too large to read at once",
|
|
27
|
+
},
|
|
28
|
+
limit: {
|
|
29
|
+
type: "number",
|
|
30
|
+
description:
|
|
31
|
+
"The number of lines to read. Only provide if the file is too large to read at once.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ["file_path"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
execute: async (
|
|
39
|
+
args: Record<string, unknown>,
|
|
40
|
+
context: ToolContext,
|
|
41
|
+
): Promise<ToolResult> => {
|
|
42
|
+
const filePath = args.file_path as string;
|
|
43
|
+
const offset = args.offset as number;
|
|
44
|
+
const limit = args.limit as number;
|
|
45
|
+
|
|
46
|
+
if (!filePath || typeof filePath !== "string") {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
content: "",
|
|
50
|
+
error: "file_path parameter is required and must be a string",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Note: New Read tool requires absolute paths, so we don't use resolvePath
|
|
56
|
+
// But for compatibility, if it's not an absolute path, we still try to resolve
|
|
57
|
+
const actualFilePath = filePath.startsWith("/")
|
|
58
|
+
? filePath
|
|
59
|
+
: resolvePath(filePath, context.workdir);
|
|
60
|
+
|
|
61
|
+
const fileContent = await readFile(actualFilePath, "utf-8");
|
|
62
|
+
|
|
63
|
+
// Check if file is empty
|
|
64
|
+
if (fileContent.length === 0) {
|
|
65
|
+
// logger.warn(`File ${filePath} exists but has empty contents`);
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
content:
|
|
69
|
+
"⚠️ System reminder: This file exists but has empty contents.",
|
|
70
|
+
shortResult: "Empty file",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const lines = fileContent.split("\n");
|
|
75
|
+
const totalLines = lines.length;
|
|
76
|
+
|
|
77
|
+
// Handle offset and limit
|
|
78
|
+
let startLine = 1;
|
|
79
|
+
let endLine = Math.min(totalLines, 2000); // Default maximum read 2000 lines
|
|
80
|
+
|
|
81
|
+
if (typeof offset === "number") {
|
|
82
|
+
startLine = Math.max(1, offset);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof limit === "number") {
|
|
86
|
+
endLine = Math.min(totalLines, startLine + limit - 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// If no offset and limit specified, read entire file (maximum 2000 lines)
|
|
90
|
+
if (typeof offset !== "number" && typeof limit !== "number") {
|
|
91
|
+
startLine = 1;
|
|
92
|
+
endLine = Math.min(totalLines, 2000);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Validate line number range
|
|
96
|
+
if (startLine > totalLines) {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
content: "",
|
|
100
|
+
error: `Start line ${startLine} exceeds total lines ${totalLines}`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Extract specified line range
|
|
105
|
+
const selectedLines = lines.slice(startLine - 1, endLine);
|
|
106
|
+
|
|
107
|
+
// Format output (cat -n format, with line numbers)
|
|
108
|
+
const formattedContent = selectedLines
|
|
109
|
+
.map((line, index) => {
|
|
110
|
+
const lineNumber = startLine + index;
|
|
111
|
+
// Truncate overly long lines
|
|
112
|
+
const truncatedLine =
|
|
113
|
+
line.length > 2000 ? line.substring(0, 2000) + "..." : line;
|
|
114
|
+
return `${lineNumber.toString().padStart(6)}\t${truncatedLine}`;
|
|
115
|
+
})
|
|
116
|
+
.join("\n");
|
|
117
|
+
|
|
118
|
+
// Add file information header
|
|
119
|
+
let content = `File: ${filePath}\n`;
|
|
120
|
+
if (startLine > 1 || endLine < totalLines) {
|
|
121
|
+
content += `Lines ${startLine}-${endLine} of ${totalLines}\n`;
|
|
122
|
+
} else {
|
|
123
|
+
content += `Total lines: ${totalLines}\n`;
|
|
124
|
+
}
|
|
125
|
+
content += "─".repeat(50) + "\n";
|
|
126
|
+
content += formattedContent;
|
|
127
|
+
|
|
128
|
+
// If only showing partial content, add prompt
|
|
129
|
+
if (endLine < totalLines) {
|
|
130
|
+
content += `\n${"─".repeat(50)}\n`;
|
|
131
|
+
content += `... ${totalLines - endLine} more lines not shown`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
content,
|
|
137
|
+
shortResult: `Read ${selectedLines.length} lines${totalLines > 2000 ? " (truncated)" : ""}`,
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
content: "",
|
|
143
|
+
error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
formatCompactParams: (
|
|
148
|
+
params: Record<string, unknown>,
|
|
149
|
+
context: ToolContext,
|
|
150
|
+
) => {
|
|
151
|
+
const filePath = params.file_path as string;
|
|
152
|
+
const offset = params.offset as number;
|
|
153
|
+
const limit = params.limit as number;
|
|
154
|
+
|
|
155
|
+
let displayPath = getDisplayPath(filePath || "", context.workdir);
|
|
156
|
+
|
|
157
|
+
if (typeof offset === "number" || typeof limit === "number") {
|
|
158
|
+
const offsetStr = typeof offset === "number" ? offset.toString() : "1";
|
|
159
|
+
const limitStr = typeof limit === "number" ? limit.toString() : "2000";
|
|
160
|
+
displayPath += ` ${offsetStr}:${limitStr}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return displayPath;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool plugin interface definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ChatCompletionFunctionTool } from "openai/resources.js";
|
|
6
|
+
|
|
7
|
+
export interface ToolPlugin {
|
|
8
|
+
name: string;
|
|
9
|
+
config: ChatCompletionFunctionTool;
|
|
10
|
+
execute: (
|
|
11
|
+
args: Record<string, unknown>,
|
|
12
|
+
context: ToolContext,
|
|
13
|
+
) => Promise<ToolResult>;
|
|
14
|
+
formatCompactParams?: (
|
|
15
|
+
params: Record<string, unknown>,
|
|
16
|
+
context: ToolContext,
|
|
17
|
+
) => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ToolResult {
|
|
21
|
+
success: boolean;
|
|
22
|
+
content: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
// Short output, used to display summary information in collapsed state
|
|
25
|
+
shortResult?: string;
|
|
26
|
+
// Additional properties for file editing tools
|
|
27
|
+
originalContent?: string;
|
|
28
|
+
newContent?: string;
|
|
29
|
+
diffResult?: Array<{
|
|
30
|
+
count?: number;
|
|
31
|
+
value: string;
|
|
32
|
+
added?: boolean;
|
|
33
|
+
removed?: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
filePath?: string;
|
|
36
|
+
// Image data, for supporting multimedia content
|
|
37
|
+
images?: Array<{
|
|
38
|
+
data: string; // base64 encoded image data
|
|
39
|
+
mediaType?: string; // Image media type, such as "image/png"
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ToolContext {
|
|
44
|
+
abortSignal?: AbortSignal;
|
|
45
|
+
backgroundBashManager?: import("../managers/backgroundBashManager.js").BackgroundBashManager;
|
|
46
|
+
workdir: string;
|
|
47
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
4
|
+
import { resolvePath, getDisplayPath } from "../utils/path.js";
|
|
5
|
+
import { diffLines } from "diff";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* File Write Tool Plugin
|
|
9
|
+
*/
|
|
10
|
+
export const writeTool: ToolPlugin = {
|
|
11
|
+
name: "Write",
|
|
12
|
+
config: {
|
|
13
|
+
type: "function",
|
|
14
|
+
function: {
|
|
15
|
+
name: "Write",
|
|
16
|
+
description:
|
|
17
|
+
"Writes a file to the local filesystem.\n\nUsage:\n- This tool will overwrite the existing file if there is one at the provided path.\n- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.",
|
|
18
|
+
parameters: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
file_path: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description:
|
|
24
|
+
"The absolute path to the file to write (must be absolute, not relative)",
|
|
25
|
+
},
|
|
26
|
+
content: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "The content to write to the file",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ["file_path", "content"],
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
execute: async (
|
|
37
|
+
args: Record<string, unknown>,
|
|
38
|
+
context: ToolContext,
|
|
39
|
+
): Promise<ToolResult> => {
|
|
40
|
+
const filePath = args.file_path as string;
|
|
41
|
+
const content = args.content as string;
|
|
42
|
+
|
|
43
|
+
// Validate required parameters
|
|
44
|
+
if (!filePath || typeof filePath !== "string") {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
content: "",
|
|
48
|
+
error: "file_path parameter is required and must be a string",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof content !== "string") {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
content: "",
|
|
56
|
+
error: "content parameter is required and must be a string",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const resolvedPath = resolvePath(filePath, context.workdir);
|
|
62
|
+
|
|
63
|
+
// Check if file already exists
|
|
64
|
+
let originalContent = "";
|
|
65
|
+
let isExistingFile = false;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
originalContent = await readFile(resolvedPath, "utf-8");
|
|
69
|
+
isExistingFile = true;
|
|
70
|
+
} catch {
|
|
71
|
+
// File doesn't exist, this is normal for new file creation
|
|
72
|
+
isExistingFile = false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if overwriting existing file but content is the same
|
|
76
|
+
if (isExistingFile && originalContent === content) {
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
content: `File ${filePath} already has the same content, no changes needed`,
|
|
80
|
+
shortResult: "No changes needed",
|
|
81
|
+
filePath: resolvedPath,
|
|
82
|
+
originalContent,
|
|
83
|
+
newContent: content,
|
|
84
|
+
diffResult: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Ensure directory exists
|
|
89
|
+
const fileDir = dirname(resolvedPath);
|
|
90
|
+
try {
|
|
91
|
+
await mkdir(fileDir, { recursive: true });
|
|
92
|
+
} catch (mkdirError) {
|
|
93
|
+
// Ignore directory already exists error
|
|
94
|
+
if (
|
|
95
|
+
mkdirError instanceof Error &&
|
|
96
|
+
!mkdirError.message.includes("EEXIST")
|
|
97
|
+
) {
|
|
98
|
+
// logger.warn(
|
|
99
|
+
// `Failed to create directory ${fileDir}: ${mkdirError.message}`,
|
|
100
|
+
// );
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Write file
|
|
105
|
+
try {
|
|
106
|
+
await writeFile(resolvedPath, content, "utf-8");
|
|
107
|
+
} catch (writeError) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
content: "",
|
|
111
|
+
error: `Failed to write file: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Generate diff information
|
|
116
|
+
const diffResult = diffLines(originalContent, content);
|
|
117
|
+
|
|
118
|
+
const shortResult = isExistingFile ? "File overwritten" : "File created";
|
|
119
|
+
|
|
120
|
+
const lines = content.split("\n").length;
|
|
121
|
+
const chars = content.length;
|
|
122
|
+
const detailedContent = `${shortResult} (${lines} lines, ${chars} characters)`;
|
|
123
|
+
|
|
124
|
+
// logger.info(`Write tool: ${shortResult}`);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
success: true,
|
|
128
|
+
content: detailedContent,
|
|
129
|
+
shortResult,
|
|
130
|
+
filePath: resolvedPath,
|
|
131
|
+
originalContent,
|
|
132
|
+
newContent: content,
|
|
133
|
+
diffResult,
|
|
134
|
+
};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const errorMessage =
|
|
137
|
+
error instanceof Error ? error.message : String(error);
|
|
138
|
+
// logger.error(`Write tool error: ${errorMessage}`);
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
content: "",
|
|
142
|
+
error: errorMessage,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
formatCompactParams: (
|
|
147
|
+
params: Record<string, unknown>,
|
|
148
|
+
context: ToolContext,
|
|
149
|
+
) => {
|
|
150
|
+
const filePath = params.file_path as string;
|
|
151
|
+
const content = params.content as string;
|
|
152
|
+
|
|
153
|
+
let displayPath = getDisplayPath(filePath || "", context.workdir);
|
|
154
|
+
|
|
155
|
+
if (content) {
|
|
156
|
+
const lines = content.split("\n").length;
|
|
157
|
+
const chars = content.length;
|
|
158
|
+
displayPath += ` ${lines} lines, ${chars} chars`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return displayPath;
|
|
162
|
+
},
|
|
163
|
+
};
|