wave-agent-sdk 0.6.4 → 0.7.0
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/dist/agent.d.ts +8 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +49 -240
- package/dist/constants/tools.d.ts +0 -2
- package/dist/constants/tools.d.ts.map +1 -1
- package/dist/constants/tools.js +0 -2
- package/dist/core/plugin.d.ts +86 -0
- package/dist/core/plugin.d.ts.map +1 -0
- package/dist/core/plugin.js +164 -0
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -5
- package/dist/managers/MemoryRuleManager.d.ts +3 -1
- package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
- package/dist/managers/MemoryRuleManager.js +2 -1
- package/dist/managers/aiManager.d.ts +13 -23
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +59 -32
- package/dist/managers/backgroundTaskManager.d.ts +3 -1
- package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
- package/dist/managers/backgroundTaskManager.js +2 -1
- package/dist/managers/bashManager.d.ts +4 -4
- package/dist/managers/bashManager.d.ts.map +1 -1
- package/dist/managers/bashManager.js +5 -2
- package/dist/managers/foregroundTaskManager.d.ts +3 -0
- package/dist/managers/foregroundTaskManager.d.ts.map +1 -1
- package/dist/managers/foregroundTaskManager.js +2 -1
- package/dist/managers/hookManager.d.ts +3 -3
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +20 -19
- package/dist/managers/liveConfigManager.d.ts +6 -13
- package/dist/managers/liveConfigManager.d.ts.map +1 -1
- package/dist/managers/liveConfigManager.js +50 -45
- package/dist/managers/lspManager.d.ts +4 -5
- package/dist/managers/lspManager.d.ts.map +1 -1
- package/dist/managers/lspManager.js +13 -12
- package/dist/managers/mcpManager.d.ts +3 -2
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +16 -15
- package/dist/managers/messageManager.d.ts +5 -7
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +12 -7
- package/dist/managers/permissionManager.d.ts +6 -4
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +39 -63
- package/dist/managers/planManager.d.ts +4 -6
- package/dist/managers/planManager.d.ts.map +1 -1
- package/dist/managers/planManager.js +18 -4
- package/dist/managers/pluginManager.d.ts +10 -22
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +27 -14
- package/dist/managers/reversionManager.d.ts +4 -3
- package/dist/managers/reversionManager.d.ts.map +1 -1
- package/dist/managers/reversionManager.js +5 -2
- package/dist/managers/skillManager.d.ts +3 -2
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +15 -14
- package/dist/managers/slashCommandManager.d.ts +9 -16
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +21 -10
- package/dist/managers/subagentManager.d.ts +7 -17
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +41 -34
- package/dist/managers/toolManager.d.ts +15 -38
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +66 -56
- package/dist/prompts/index.d.ts +6 -3
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +8 -16
- package/dist/services/MarketplaceService.d.ts.map +1 -1
- package/dist/services/MarketplaceService.js +13 -0
- package/dist/services/aiService.d.ts +4 -0
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +47 -7
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +30 -11
- package/dist/services/taskManager.d.ts +3 -1
- package/dist/services/taskManager.d.ts.map +1 -1
- package/dist/services/taskManager.js +2 -1
- package/dist/tools/bashTool.js +2 -2
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +9 -1
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +2 -2
- package/dist/tools/skillTool.d.ts +2 -4
- package/dist/tools/skillTool.d.ts.map +1 -1
- package/dist/tools/skillTool.js +61 -61
- package/dist/tools/taskOutputTool.js +1 -1
- package/dist/tools/taskTool.d.ts +2 -4
- package/dist/tools/taskTool.d.ts.map +1 -1
- package/dist/tools/taskTool.js +192 -187
- package/dist/tools/types.d.ts +11 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +4 -2
- package/dist/types/marketplace.d.ts +8 -0
- package/dist/types/marketplace.d.ts.map +1 -1
- package/dist/types/permissions.d.ts +1 -1
- package/dist/types/permissions.d.ts.map +1 -1
- package/dist/types/permissions.js +1 -3
- package/dist/types/skills.d.ts +0 -2
- package/dist/types/skills.d.ts.map +1 -1
- package/dist/types/tools.d.ts +0 -15
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/container.d.ts +31 -0
- package/dist/utils/container.d.ts.map +1 -0
- package/dist/utils/container.js +79 -0
- package/dist/utils/containerSetup.d.ts +26 -0
- package/dist/utils/containerSetup.d.ts.map +1 -0
- package/dist/utils/containerSetup.js +165 -0
- package/dist/utils/editUtils.d.ts +0 -3
- package/dist/utils/editUtils.d.ts.map +1 -1
- package/dist/utils/editUtils.js +4 -3
- package/dist/utils/hookMatcher.d.ts +1 -1
- package/dist/utils/hookMatcher.d.ts.map +1 -1
- package/dist/utils/hookMatcher.js +2 -2
- package/dist/utils/openaiClient.js +2 -2
- package/dist/utils/stringUtils.d.ts +6 -0
- package/dist/utils/stringUtils.d.ts.map +1 -1
- package/dist/utils/stringUtils.js +8 -0
- package/package.json +1 -1
- package/src/agent.ts +60 -282
- package/src/constants/tools.ts +0 -2
- package/src/core/plugin.ts +224 -0
- package/src/index.ts +1 -6
- package/src/managers/MemoryRuleManager.ts +6 -1
- package/src/managers/aiManager.ts +83 -58
- package/src/managers/backgroundTaskManager.ts +5 -1
- package/src/managers/bashManager.ts +9 -4
- package/src/managers/foregroundTaskManager.ts +3 -0
- package/src/managers/hookManager.ts +21 -23
- package/src/managers/liveConfigManager.ts +57 -53
- package/src/managers/lspManager.ts +14 -19
- package/src/managers/mcpManager.ts +20 -20
- package/src/managers/messageManager.ts +19 -12
- package/src/managers/permissionManager.ts +45 -70
- package/src/managers/planManager.ts +26 -7
- package/src/managers/pluginManager.ts +37 -33
- package/src/managers/reversionManager.ts +5 -3
- package/src/managers/skillManager.ts +19 -20
- package/src/managers/slashCommandManager.ts +30 -25
- package/src/managers/subagentManager.ts +53 -53
- package/src/managers/toolManager.ts +91 -90
- package/src/prompts/index.ts +12 -24
- package/src/services/MarketplaceService.ts +13 -0
- package/src/services/aiService.ts +61 -15
- package/src/services/configurationService.ts +34 -13
- package/src/services/taskManager.ts +5 -1
- package/src/tools/bashTool.ts +2 -2
- package/src/tools/editTool.ts +9 -1
- package/src/tools/readTool.ts +2 -2
- package/src/tools/skillTool.ts +75 -71
- package/src/tools/taskOutputTool.ts +1 -1
- package/src/tools/taskTool.ts +224 -225
- package/src/tools/types.ts +12 -1
- package/src/tools/writeTool.ts +4 -2
- package/src/types/marketplace.ts +9 -0
- package/src/types/permissions.ts +0 -4
- package/src/types/skills.ts +0 -3
- package/src/types/tools.ts +0 -17
- package/src/utils/container.ts +92 -0
- package/src/utils/containerSetup.ts +256 -0
- package/src/utils/editUtils.ts +4 -3
- package/src/utils/hookMatcher.ts +2 -2
- package/src/utils/openaiClient.ts +2 -2
- package/src/utils/stringUtils.ts +9 -0
- package/dist/tools/deleteFileTool.d.ts +0 -6
- package/dist/tools/deleteFileTool.d.ts.map +0 -1
- package/dist/tools/deleteFileTool.js +0 -100
- package/dist/tools/multiEditTool.d.ts +0 -6
- package/dist/tools/multiEditTool.d.ts.map +0 -1
- package/dist/tools/multiEditTool.js +0 -246
- package/src/tools/deleteFileTool.ts +0 -127
- package/src/tools/multiEditTool.ts +0 -306
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from "fs/promises";
|
|
2
|
-
import { logger } from "../utils/globalLogger.js";
|
|
3
|
-
import { resolvePath, getDisplayPath } from "../utils/path.js";
|
|
4
|
-
import { escapeRegExp, analyzeEditMismatch } from "../utils/editUtils.js";
|
|
5
|
-
import { MULTI_EDIT_TOOL_NAME, EDIT_TOOL_NAME, READ_TOOL_NAME, } from "../constants/tools.js";
|
|
6
|
-
/**
|
|
7
|
-
* Format compact parameter display
|
|
8
|
-
*/
|
|
9
|
-
function formatCompactParams(args, context) {
|
|
10
|
-
const filePath = args.file_path;
|
|
11
|
-
const edits = args.edits;
|
|
12
|
-
const editCount = edits ? edits.length : 0;
|
|
13
|
-
const displayPath = getDisplayPath(filePath || "", context.workdir);
|
|
14
|
-
return `${displayPath} ${editCount} edits`;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Multi-edit tool plugin
|
|
18
|
-
*/
|
|
19
|
-
export const multiEditTool = {
|
|
20
|
-
name: MULTI_EDIT_TOOL_NAME,
|
|
21
|
-
formatCompactParams,
|
|
22
|
-
config: {
|
|
23
|
-
type: "function",
|
|
24
|
-
function: {
|
|
25
|
-
name: MULTI_EDIT_TOOL_NAME,
|
|
26
|
-
description: `This is a tool for making multiple edits to a single file in one operation. It is built on top of the ${EDIT_TOOL_NAME} tool and allows you to perform multiple find-and-replace operations efficiently. Prefer this tool over the ${EDIT_TOOL_NAME} tool when you need to make multiple edits to the same file.\n\nBefore using this tool:\n\n1. Use the ${READ_TOOL_NAME} 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_NAME} 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.`,
|
|
27
|
-
parameters: {
|
|
28
|
-
type: "object",
|
|
29
|
-
properties: {
|
|
30
|
-
file_path: {
|
|
31
|
-
type: "string",
|
|
32
|
-
description: "The absolute path to the file to modify",
|
|
33
|
-
},
|
|
34
|
-
edits: {
|
|
35
|
-
type: "array",
|
|
36
|
-
items: {
|
|
37
|
-
type: "object",
|
|
38
|
-
properties: {
|
|
39
|
-
old_string: {
|
|
40
|
-
type: "string",
|
|
41
|
-
description: "The text to replace",
|
|
42
|
-
},
|
|
43
|
-
new_string: {
|
|
44
|
-
type: "string",
|
|
45
|
-
description: "The text to replace it with",
|
|
46
|
-
},
|
|
47
|
-
replace_all: {
|
|
48
|
-
type: "boolean",
|
|
49
|
-
default: false,
|
|
50
|
-
description: "Replace all occurences of old_string (default false).",
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
required: ["old_string", "new_string"],
|
|
54
|
-
additionalProperties: false,
|
|
55
|
-
},
|
|
56
|
-
minItems: 1,
|
|
57
|
-
description: "Array of edit operations to perform sequentially on the file",
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
required: ["file_path", "edits"],
|
|
61
|
-
additionalProperties: false,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
execute: async (args, context) => {
|
|
66
|
-
const filePath = args.file_path;
|
|
67
|
-
const edits = args.edits;
|
|
68
|
-
// Validate required parameters
|
|
69
|
-
if (!filePath || typeof filePath !== "string") {
|
|
70
|
-
return {
|
|
71
|
-
success: false,
|
|
72
|
-
content: "",
|
|
73
|
-
error: "file_path parameter is required and must be a string",
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
if (!Array.isArray(edits) || edits.length === 0) {
|
|
77
|
-
return {
|
|
78
|
-
success: false,
|
|
79
|
-
content: "",
|
|
80
|
-
error: "edits parameter is required and must be a non-empty array",
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
// Validate each edit operation
|
|
84
|
-
for (let i = 0; i < edits.length; i++) {
|
|
85
|
-
const edit = edits[i];
|
|
86
|
-
if (!edit || typeof edit !== "object") {
|
|
87
|
-
return {
|
|
88
|
-
success: false,
|
|
89
|
-
content: "",
|
|
90
|
-
error: `Edit operation ${i + 1} must be an object`,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
if (typeof edit.old_string !== "string") {
|
|
94
|
-
return {
|
|
95
|
-
success: false,
|
|
96
|
-
content: "",
|
|
97
|
-
error: `Edit operation ${i + 1}: old_string is required and must be a string`,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
if (typeof edit.new_string !== "string") {
|
|
101
|
-
return {
|
|
102
|
-
success: false,
|
|
103
|
-
content: "",
|
|
104
|
-
error: `Edit operation ${i + 1}: new_string is required and must be a string`,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
if (edit.old_string === edit.new_string) {
|
|
108
|
-
return {
|
|
109
|
-
success: false,
|
|
110
|
-
content: "",
|
|
111
|
-
error: `Edit operation ${i + 1}: old_string and new_string must be different`,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
const resolvedPath = resolvePath(filePath, context.workdir);
|
|
117
|
-
// Read file content
|
|
118
|
-
let originalContent;
|
|
119
|
-
let isNewFile = false;
|
|
120
|
-
try {
|
|
121
|
-
originalContent = await readFile(resolvedPath, "utf-8");
|
|
122
|
-
}
|
|
123
|
-
catch (readError) {
|
|
124
|
-
// Check if this is a new file creation case (first edit has empty old_string)
|
|
125
|
-
if (edits[0] && edits[0].old_string === "") {
|
|
126
|
-
originalContent = "";
|
|
127
|
-
isNewFile = true;
|
|
128
|
-
logger.debug(`Creating new file: ${resolvedPath}`);
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
return {
|
|
132
|
-
success: false,
|
|
133
|
-
content: "",
|
|
134
|
-
error: `Failed to read file: ${readError instanceof Error ? readError.message : String(readError)}`,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
let currentContent = originalContent;
|
|
139
|
-
const appliedEdits = [];
|
|
140
|
-
// Apply each edit operation in sequence
|
|
141
|
-
for (let i = 0; i < edits.length; i++) {
|
|
142
|
-
const edit = edits[i];
|
|
143
|
-
const replaceAll = edit.replace_all || false;
|
|
144
|
-
// Special handling for the first edit of new file creation
|
|
145
|
-
if (isNewFile && i === 0 && edit.old_string === "") {
|
|
146
|
-
currentContent = edit.new_string;
|
|
147
|
-
appliedEdits.push(`Created file with content (${edit.new_string.length} characters)`);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
// Check if old_string exists
|
|
151
|
-
const matchedOldString = currentContent.includes(edit.old_string)
|
|
152
|
-
? edit.old_string
|
|
153
|
-
: null;
|
|
154
|
-
if (!matchedOldString) {
|
|
155
|
-
return {
|
|
156
|
-
success: false,
|
|
157
|
-
content: "",
|
|
158
|
-
error: `Edit operation ${i + 1}: ${analyzeEditMismatch(currentContent, edit.old_string)}`,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
let replacementCount;
|
|
162
|
-
if (replaceAll) {
|
|
163
|
-
// Replace all matches
|
|
164
|
-
const regex = new RegExp(escapeRegExp(edit.old_string), "g");
|
|
165
|
-
currentContent = currentContent.replace(regex, edit.new_string);
|
|
166
|
-
replacementCount = (currentContent.match(regex) || []).length;
|
|
167
|
-
appliedEdits.push(`Replaced ${replacementCount} instances of "${edit.old_string.substring(0, 50)}${edit.old_string.length > 50 ? "..." : ""}"`);
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
// Replace only the first match, but first check if it's unique
|
|
171
|
-
const matches = currentContent.split(matchedOldString).length - 1;
|
|
172
|
-
if (matches > 1) {
|
|
173
|
-
return {
|
|
174
|
-
success: false,
|
|
175
|
-
content: "",
|
|
176
|
-
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.`,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
currentContent = currentContent.replace(matchedOldString, edit.new_string);
|
|
180
|
-
appliedEdits.push(`Replaced "${edit.old_string.substring(0, 50)}${edit.old_string.length > 50 ? "..." : ""}"`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
// Permission check after validation but before real operation
|
|
184
|
-
if (context.permissionManager) {
|
|
185
|
-
try {
|
|
186
|
-
const permissionContext = context.permissionManager.createContext(MULTI_EDIT_TOOL_NAME, context.permissionMode || "default", context.canUseToolCallback, { file_path: filePath, edits });
|
|
187
|
-
const permissionResult = await context.permissionManager.checkPermission(permissionContext);
|
|
188
|
-
if (permissionResult.behavior === "deny") {
|
|
189
|
-
return {
|
|
190
|
-
success: false,
|
|
191
|
-
content: "",
|
|
192
|
-
error: `${MULTI_EDIT_TOOL_NAME} operation denied, reason: ${permissionResult.message || "No reason provided"}`,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
return {
|
|
198
|
-
success: false,
|
|
199
|
-
content: "",
|
|
200
|
-
error: "Permission check failed",
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
// Record snapshot for reversion
|
|
205
|
-
let snapshotId;
|
|
206
|
-
if (context.reversionManager && context.messageId) {
|
|
207
|
-
snapshotId = await context.reversionManager.recordSnapshot(context.messageId, resolvedPath, isNewFile ? "create" : "modify");
|
|
208
|
-
}
|
|
209
|
-
// Write file
|
|
210
|
-
try {
|
|
211
|
-
await writeFile(resolvedPath, currentContent, "utf-8");
|
|
212
|
-
// Commit snapshot on success
|
|
213
|
-
if (context.reversionManager && snapshotId) {
|
|
214
|
-
await context.reversionManager.commitSnapshot(snapshotId);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
catch (writeError) {
|
|
218
|
-
return {
|
|
219
|
-
success: false,
|
|
220
|
-
content: "",
|
|
221
|
-
error: `Failed to write file: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
const shortResult = isNewFile
|
|
225
|
-
? `Created file with ${edits.length} operations`
|
|
226
|
-
: `Applied ${edits.length} edits`;
|
|
227
|
-
const detailedContent = `${shortResult}\n\nOperations performed:\n${appliedEdits.map((edit, i) => `${i + 1}. ${edit}`).join("\n")}`;
|
|
228
|
-
logger.debug(`MultiEdit tool: ${shortResult}`);
|
|
229
|
-
return {
|
|
230
|
-
success: true,
|
|
231
|
-
content: detailedContent,
|
|
232
|
-
shortResult,
|
|
233
|
-
filePath: resolvedPath,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
catch (error) {
|
|
237
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
238
|
-
logger.error(`MultiEdit tool error: ${errorMessage}`);
|
|
239
|
-
return {
|
|
240
|
-
success: false,
|
|
241
|
-
content: "",
|
|
242
|
-
error: errorMessage,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
};
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { unlink } from "fs/promises";
|
|
2
|
-
import { logger } from "../utils/globalLogger.js";
|
|
3
|
-
import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
4
|
-
import { resolvePath, getDisplayPath } from "../utils/path.js";
|
|
5
|
-
import { DELETE_FILE_TOOL_NAME } from "../constants/tools.js";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Delete file tool plugin
|
|
9
|
-
*/
|
|
10
|
-
export const deleteFileTool: ToolPlugin = {
|
|
11
|
-
name: DELETE_FILE_TOOL_NAME,
|
|
12
|
-
config: {
|
|
13
|
-
type: "function",
|
|
14
|
-
function: {
|
|
15
|
-
name: DELETE_FILE_TOOL_NAME,
|
|
16
|
-
description: `Deletes a file at the specified path. The operation will fail gracefully if:
|
|
17
|
-
- The file doesn't exist
|
|
18
|
-
- The operation is rejected for security reasons
|
|
19
|
-
- The file cannot be deleted`,
|
|
20
|
-
parameters: {
|
|
21
|
-
type: "object",
|
|
22
|
-
properties: {
|
|
23
|
-
target_file: {
|
|
24
|
-
type: "string",
|
|
25
|
-
description:
|
|
26
|
-
"The path of the file to delete, relative to the workspace root.",
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
required: ["target_file"],
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
execute: async (
|
|
34
|
-
args: Record<string, unknown>,
|
|
35
|
-
context: ToolContext,
|
|
36
|
-
): Promise<ToolResult> => {
|
|
37
|
-
const targetFile = args.target_file as string;
|
|
38
|
-
|
|
39
|
-
if (!targetFile || typeof targetFile !== "string") {
|
|
40
|
-
return {
|
|
41
|
-
success: false,
|
|
42
|
-
content: "",
|
|
43
|
-
error: "target_file parameter is required and must be a string",
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const filePath = resolvePath(targetFile, context.workdir);
|
|
49
|
-
|
|
50
|
-
// Permission check after validation but before real operation
|
|
51
|
-
// Permission check after validation but before real operation
|
|
52
|
-
if (context.permissionManager) {
|
|
53
|
-
try {
|
|
54
|
-
const permissionContext = context.permissionManager.createContext(
|
|
55
|
-
DELETE_FILE_TOOL_NAME,
|
|
56
|
-
context.permissionMode || "default",
|
|
57
|
-
context.canUseToolCallback,
|
|
58
|
-
{ target_file: targetFile },
|
|
59
|
-
);
|
|
60
|
-
const permissionResult =
|
|
61
|
-
await context.permissionManager.checkPermission(permissionContext);
|
|
62
|
-
|
|
63
|
-
if (permissionResult.behavior === "deny") {
|
|
64
|
-
return {
|
|
65
|
-
success: false,
|
|
66
|
-
content: "",
|
|
67
|
-
error: `${DELETE_FILE_TOOL_NAME} operation denied, reason: ${permissionResult.message || "No reason provided"}`,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
} catch {
|
|
71
|
-
return {
|
|
72
|
-
success: false,
|
|
73
|
-
content: "",
|
|
74
|
-
error: "Permission check failed",
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Record snapshot for reversion
|
|
80
|
-
let snapshotId: string | undefined;
|
|
81
|
-
if (context.reversionManager && context.messageId) {
|
|
82
|
-
snapshotId = await context.reversionManager.recordSnapshot(
|
|
83
|
-
context.messageId,
|
|
84
|
-
filePath,
|
|
85
|
-
"delete",
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Delete file
|
|
90
|
-
await unlink(filePath);
|
|
91
|
-
|
|
92
|
-
// Commit snapshot on success
|
|
93
|
-
if (context.reversionManager && snapshotId) {
|
|
94
|
-
await context.reversionManager.commitSnapshot(snapshotId);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
logger.debug(`Successfully deleted file: ${filePath}`);
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
success: true,
|
|
101
|
-
content: `Successfully deleted file: ${targetFile}`,
|
|
102
|
-
shortResult: "File deleted",
|
|
103
|
-
};
|
|
104
|
-
} catch (error) {
|
|
105
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
106
|
-
return {
|
|
107
|
-
success: false,
|
|
108
|
-
content: "",
|
|
109
|
-
error: `File does not exist: ${targetFile}`,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
success: false,
|
|
115
|
-
content: "",
|
|
116
|
-
error: error instanceof Error ? error.message : String(error),
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
formatCompactParams: (
|
|
121
|
-
params: Record<string, unknown>,
|
|
122
|
-
context: ToolContext,
|
|
123
|
-
) => {
|
|
124
|
-
const targetFile = params.target_file as string;
|
|
125
|
-
return getDisplayPath(targetFile || "", context.workdir);
|
|
126
|
-
},
|
|
127
|
-
};
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from "fs/promises";
|
|
2
|
-
import { logger } from "../utils/globalLogger.js";
|
|
3
|
-
import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
4
|
-
import { resolvePath, getDisplayPath } from "../utils/path.js";
|
|
5
|
-
import { escapeRegExp, analyzeEditMismatch } from "../utils/editUtils.js";
|
|
6
|
-
import {
|
|
7
|
-
MULTI_EDIT_TOOL_NAME,
|
|
8
|
-
EDIT_TOOL_NAME,
|
|
9
|
-
READ_TOOL_NAME,
|
|
10
|
-
} from "../constants/tools.js";
|
|
11
|
-
|
|
12
|
-
interface EditOperation {
|
|
13
|
-
old_string: string;
|
|
14
|
-
new_string: string;
|
|
15
|
-
replace_all?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Format compact parameter display
|
|
20
|
-
*/
|
|
21
|
-
function formatCompactParams(
|
|
22
|
-
args: Record<string, unknown>,
|
|
23
|
-
context: ToolContext,
|
|
24
|
-
): string {
|
|
25
|
-
const filePath = args.file_path as string;
|
|
26
|
-
const edits = args.edits as EditOperation[];
|
|
27
|
-
const editCount = edits ? edits.length : 0;
|
|
28
|
-
const displayPath = getDisplayPath(filePath || "", context.workdir);
|
|
29
|
-
return `${displayPath} ${editCount} edits`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Multi-edit tool plugin
|
|
34
|
-
*/
|
|
35
|
-
export const multiEditTool: ToolPlugin = {
|
|
36
|
-
name: MULTI_EDIT_TOOL_NAME,
|
|
37
|
-
formatCompactParams,
|
|
38
|
-
config: {
|
|
39
|
-
type: "function",
|
|
40
|
-
function: {
|
|
41
|
-
name: MULTI_EDIT_TOOL_NAME,
|
|
42
|
-
description: `This is a tool for making multiple edits to a single file in one operation. It is built on top of the ${EDIT_TOOL_NAME} tool and allows you to perform multiple find-and-replace operations efficiently. Prefer this tool over the ${EDIT_TOOL_NAME} tool when you need to make multiple edits to the same file.\n\nBefore using this tool:\n\n1. Use the ${READ_TOOL_NAME} 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_NAME} 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.`,
|
|
43
|
-
parameters: {
|
|
44
|
-
type: "object",
|
|
45
|
-
properties: {
|
|
46
|
-
file_path: {
|
|
47
|
-
type: "string",
|
|
48
|
-
description: "The absolute path to the file to modify",
|
|
49
|
-
},
|
|
50
|
-
edits: {
|
|
51
|
-
type: "array",
|
|
52
|
-
items: {
|
|
53
|
-
type: "object",
|
|
54
|
-
properties: {
|
|
55
|
-
old_string: {
|
|
56
|
-
type: "string",
|
|
57
|
-
description: "The text to replace",
|
|
58
|
-
},
|
|
59
|
-
new_string: {
|
|
60
|
-
type: "string",
|
|
61
|
-
description: "The text to replace it with",
|
|
62
|
-
},
|
|
63
|
-
replace_all: {
|
|
64
|
-
type: "boolean",
|
|
65
|
-
default: false,
|
|
66
|
-
description:
|
|
67
|
-
"Replace all occurences of old_string (default false).",
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
required: ["old_string", "new_string"],
|
|
71
|
-
additionalProperties: false,
|
|
72
|
-
},
|
|
73
|
-
minItems: 1,
|
|
74
|
-
description:
|
|
75
|
-
"Array of edit operations to perform sequentially on the file",
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
required: ["file_path", "edits"],
|
|
79
|
-
additionalProperties: false,
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
execute: async (
|
|
84
|
-
args: Record<string, unknown>,
|
|
85
|
-
context: ToolContext,
|
|
86
|
-
): Promise<ToolResult> => {
|
|
87
|
-
const filePath = args.file_path as string;
|
|
88
|
-
const edits = args.edits as EditOperation[];
|
|
89
|
-
|
|
90
|
-
// Validate required parameters
|
|
91
|
-
if (!filePath || typeof filePath !== "string") {
|
|
92
|
-
return {
|
|
93
|
-
success: false,
|
|
94
|
-
content: "",
|
|
95
|
-
error: "file_path parameter is required and must be a string",
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!Array.isArray(edits) || edits.length === 0) {
|
|
100
|
-
return {
|
|
101
|
-
success: false,
|
|
102
|
-
content: "",
|
|
103
|
-
error: "edits parameter is required and must be a non-empty array",
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Validate each edit operation
|
|
108
|
-
for (let i = 0; i < edits.length; i++) {
|
|
109
|
-
const edit = edits[i];
|
|
110
|
-
if (!edit || typeof edit !== "object") {
|
|
111
|
-
return {
|
|
112
|
-
success: false,
|
|
113
|
-
content: "",
|
|
114
|
-
error: `Edit operation ${i + 1} must be an object`,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (typeof edit.old_string !== "string") {
|
|
119
|
-
return {
|
|
120
|
-
success: false,
|
|
121
|
-
content: "",
|
|
122
|
-
error: `Edit operation ${i + 1}: old_string is required and must be a string`,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (typeof edit.new_string !== "string") {
|
|
127
|
-
return {
|
|
128
|
-
success: false,
|
|
129
|
-
content: "",
|
|
130
|
-
error: `Edit operation ${i + 1}: new_string is required and must be a string`,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (edit.old_string === edit.new_string) {
|
|
135
|
-
return {
|
|
136
|
-
success: false,
|
|
137
|
-
content: "",
|
|
138
|
-
error: `Edit operation ${i + 1}: old_string and new_string must be different`,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
const resolvedPath = resolvePath(filePath, context.workdir);
|
|
145
|
-
|
|
146
|
-
// Read file content
|
|
147
|
-
let originalContent: string;
|
|
148
|
-
let isNewFile = false;
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
originalContent = await readFile(resolvedPath, "utf-8");
|
|
152
|
-
} catch (readError) {
|
|
153
|
-
// Check if this is a new file creation case (first edit has empty old_string)
|
|
154
|
-
if (edits[0] && edits[0].old_string === "") {
|
|
155
|
-
originalContent = "";
|
|
156
|
-
isNewFile = true;
|
|
157
|
-
logger.debug(`Creating new file: ${resolvedPath}`);
|
|
158
|
-
} else {
|
|
159
|
-
return {
|
|
160
|
-
success: false,
|
|
161
|
-
content: "",
|
|
162
|
-
error: `Failed to read file: ${readError instanceof Error ? readError.message : String(readError)}`,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let currentContent = originalContent;
|
|
168
|
-
const appliedEdits: string[] = [];
|
|
169
|
-
|
|
170
|
-
// Apply each edit operation in sequence
|
|
171
|
-
for (let i = 0; i < edits.length; i++) {
|
|
172
|
-
const edit = edits[i];
|
|
173
|
-
const replaceAll = edit.replace_all || false;
|
|
174
|
-
|
|
175
|
-
// Special handling for the first edit of new file creation
|
|
176
|
-
if (isNewFile && i === 0 && edit.old_string === "") {
|
|
177
|
-
currentContent = edit.new_string;
|
|
178
|
-
appliedEdits.push(
|
|
179
|
-
`Created file with content (${edit.new_string.length} characters)`,
|
|
180
|
-
);
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Check if old_string exists
|
|
185
|
-
const matchedOldString = currentContent.includes(edit.old_string)
|
|
186
|
-
? edit.old_string
|
|
187
|
-
: null;
|
|
188
|
-
|
|
189
|
-
if (!matchedOldString) {
|
|
190
|
-
return {
|
|
191
|
-
success: false,
|
|
192
|
-
content: "",
|
|
193
|
-
error: `Edit operation ${i + 1}: ${analyzeEditMismatch(currentContent, edit.old_string)}`,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let replacementCount: number;
|
|
198
|
-
|
|
199
|
-
if (replaceAll) {
|
|
200
|
-
// Replace all matches
|
|
201
|
-
const regex = new RegExp(escapeRegExp(edit.old_string), "g");
|
|
202
|
-
currentContent = currentContent.replace(regex, edit.new_string);
|
|
203
|
-
replacementCount = (currentContent.match(regex) || []).length;
|
|
204
|
-
appliedEdits.push(
|
|
205
|
-
`Replaced ${replacementCount} instances of "${edit.old_string.substring(0, 50)}${edit.old_string.length > 50 ? "..." : ""}"`,
|
|
206
|
-
);
|
|
207
|
-
} else {
|
|
208
|
-
// Replace only the first match, but first check if it's unique
|
|
209
|
-
const matches = currentContent.split(matchedOldString).length - 1;
|
|
210
|
-
if (matches > 1) {
|
|
211
|
-
return {
|
|
212
|
-
success: false,
|
|
213
|
-
content: "",
|
|
214
|
-
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.`,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
currentContent = currentContent.replace(
|
|
219
|
-
matchedOldString,
|
|
220
|
-
edit.new_string,
|
|
221
|
-
);
|
|
222
|
-
appliedEdits.push(
|
|
223
|
-
`Replaced "${edit.old_string.substring(0, 50)}${edit.old_string.length > 50 ? "..." : ""}"`,
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Permission check after validation but before real operation
|
|
229
|
-
if (context.permissionManager) {
|
|
230
|
-
try {
|
|
231
|
-
const permissionContext = context.permissionManager.createContext(
|
|
232
|
-
MULTI_EDIT_TOOL_NAME,
|
|
233
|
-
context.permissionMode || "default",
|
|
234
|
-
context.canUseToolCallback,
|
|
235
|
-
{ file_path: filePath, edits },
|
|
236
|
-
);
|
|
237
|
-
const permissionResult =
|
|
238
|
-
await context.permissionManager.checkPermission(permissionContext);
|
|
239
|
-
|
|
240
|
-
if (permissionResult.behavior === "deny") {
|
|
241
|
-
return {
|
|
242
|
-
success: false,
|
|
243
|
-
content: "",
|
|
244
|
-
error: `${MULTI_EDIT_TOOL_NAME} operation denied, reason: ${permissionResult.message || "No reason provided"}`,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
} catch {
|
|
248
|
-
return {
|
|
249
|
-
success: false,
|
|
250
|
-
content: "",
|
|
251
|
-
error: "Permission check failed",
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Record snapshot for reversion
|
|
257
|
-
let snapshotId: string | undefined;
|
|
258
|
-
if (context.reversionManager && context.messageId) {
|
|
259
|
-
snapshotId = await context.reversionManager.recordSnapshot(
|
|
260
|
-
context.messageId,
|
|
261
|
-
resolvedPath,
|
|
262
|
-
isNewFile ? "create" : "modify",
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Write file
|
|
267
|
-
try {
|
|
268
|
-
await writeFile(resolvedPath, currentContent, "utf-8");
|
|
269
|
-
// Commit snapshot on success
|
|
270
|
-
if (context.reversionManager && snapshotId) {
|
|
271
|
-
await context.reversionManager.commitSnapshot(snapshotId);
|
|
272
|
-
}
|
|
273
|
-
} catch (writeError) {
|
|
274
|
-
return {
|
|
275
|
-
success: false,
|
|
276
|
-
content: "",
|
|
277
|
-
error: `Failed to write file: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const shortResult = isNewFile
|
|
282
|
-
? `Created file with ${edits.length} operations`
|
|
283
|
-
: `Applied ${edits.length} edits`;
|
|
284
|
-
|
|
285
|
-
const detailedContent = `${shortResult}\n\nOperations performed:\n${appliedEdits.map((edit, i) => `${i + 1}. ${edit}`).join("\n")}`;
|
|
286
|
-
|
|
287
|
-
logger.debug(`MultiEdit tool: ${shortResult}`);
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
success: true,
|
|
291
|
-
content: detailedContent,
|
|
292
|
-
shortResult,
|
|
293
|
-
filePath: resolvedPath,
|
|
294
|
-
};
|
|
295
|
-
} catch (error) {
|
|
296
|
-
const errorMessage =
|
|
297
|
-
error instanceof Error ? error.message : String(error);
|
|
298
|
-
logger.error(`MultiEdit tool error: ${errorMessage}`);
|
|
299
|
-
return {
|
|
300
|
-
success: false,
|
|
301
|
-
content: "",
|
|
302
|
-
error: errorMessage,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
},
|
|
306
|
-
};
|