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.
Files changed (174) hide show
  1. package/dist/agent.d.ts +8 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +49 -240
  4. package/dist/constants/tools.d.ts +0 -2
  5. package/dist/constants/tools.d.ts.map +1 -1
  6. package/dist/constants/tools.js +0 -2
  7. package/dist/core/plugin.d.ts +86 -0
  8. package/dist/core/plugin.d.ts.map +1 -0
  9. package/dist/core/plugin.js +164 -0
  10. package/dist/index.d.ts +1 -4
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -5
  13. package/dist/managers/MemoryRuleManager.d.ts +3 -1
  14. package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
  15. package/dist/managers/MemoryRuleManager.js +2 -1
  16. package/dist/managers/aiManager.d.ts +13 -23
  17. package/dist/managers/aiManager.d.ts.map +1 -1
  18. package/dist/managers/aiManager.js +59 -32
  19. package/dist/managers/backgroundTaskManager.d.ts +3 -1
  20. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  21. package/dist/managers/backgroundTaskManager.js +2 -1
  22. package/dist/managers/bashManager.d.ts +4 -4
  23. package/dist/managers/bashManager.d.ts.map +1 -1
  24. package/dist/managers/bashManager.js +5 -2
  25. package/dist/managers/foregroundTaskManager.d.ts +3 -0
  26. package/dist/managers/foregroundTaskManager.d.ts.map +1 -1
  27. package/dist/managers/foregroundTaskManager.js +2 -1
  28. package/dist/managers/hookManager.d.ts +3 -3
  29. package/dist/managers/hookManager.d.ts.map +1 -1
  30. package/dist/managers/hookManager.js +20 -19
  31. package/dist/managers/liveConfigManager.d.ts +6 -13
  32. package/dist/managers/liveConfigManager.d.ts.map +1 -1
  33. package/dist/managers/liveConfigManager.js +50 -45
  34. package/dist/managers/lspManager.d.ts +4 -5
  35. package/dist/managers/lspManager.d.ts.map +1 -1
  36. package/dist/managers/lspManager.js +13 -12
  37. package/dist/managers/mcpManager.d.ts +3 -2
  38. package/dist/managers/mcpManager.d.ts.map +1 -1
  39. package/dist/managers/mcpManager.js +16 -15
  40. package/dist/managers/messageManager.d.ts +5 -7
  41. package/dist/managers/messageManager.d.ts.map +1 -1
  42. package/dist/managers/messageManager.js +12 -7
  43. package/dist/managers/permissionManager.d.ts +6 -4
  44. package/dist/managers/permissionManager.d.ts.map +1 -1
  45. package/dist/managers/permissionManager.js +39 -63
  46. package/dist/managers/planManager.d.ts +4 -6
  47. package/dist/managers/planManager.d.ts.map +1 -1
  48. package/dist/managers/planManager.js +18 -4
  49. package/dist/managers/pluginManager.d.ts +10 -22
  50. package/dist/managers/pluginManager.d.ts.map +1 -1
  51. package/dist/managers/pluginManager.js +27 -14
  52. package/dist/managers/reversionManager.d.ts +4 -3
  53. package/dist/managers/reversionManager.d.ts.map +1 -1
  54. package/dist/managers/reversionManager.js +5 -2
  55. package/dist/managers/skillManager.d.ts +3 -2
  56. package/dist/managers/skillManager.d.ts.map +1 -1
  57. package/dist/managers/skillManager.js +15 -14
  58. package/dist/managers/slashCommandManager.d.ts +9 -16
  59. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  60. package/dist/managers/slashCommandManager.js +21 -10
  61. package/dist/managers/subagentManager.d.ts +7 -17
  62. package/dist/managers/subagentManager.d.ts.map +1 -1
  63. package/dist/managers/subagentManager.js +41 -34
  64. package/dist/managers/toolManager.d.ts +15 -38
  65. package/dist/managers/toolManager.d.ts.map +1 -1
  66. package/dist/managers/toolManager.js +66 -56
  67. package/dist/prompts/index.d.ts +6 -3
  68. package/dist/prompts/index.d.ts.map +1 -1
  69. package/dist/prompts/index.js +8 -16
  70. package/dist/services/MarketplaceService.d.ts.map +1 -1
  71. package/dist/services/MarketplaceService.js +13 -0
  72. package/dist/services/aiService.d.ts +4 -0
  73. package/dist/services/aiService.d.ts.map +1 -1
  74. package/dist/services/aiService.js +47 -7
  75. package/dist/services/configurationService.d.ts.map +1 -1
  76. package/dist/services/configurationService.js +30 -11
  77. package/dist/services/taskManager.d.ts +3 -1
  78. package/dist/services/taskManager.d.ts.map +1 -1
  79. package/dist/services/taskManager.js +2 -1
  80. package/dist/tools/bashTool.js +2 -2
  81. package/dist/tools/editTool.d.ts.map +1 -1
  82. package/dist/tools/editTool.js +9 -1
  83. package/dist/tools/readTool.d.ts.map +1 -1
  84. package/dist/tools/readTool.js +2 -2
  85. package/dist/tools/skillTool.d.ts +2 -4
  86. package/dist/tools/skillTool.d.ts.map +1 -1
  87. package/dist/tools/skillTool.js +61 -61
  88. package/dist/tools/taskOutputTool.js +1 -1
  89. package/dist/tools/taskTool.d.ts +2 -4
  90. package/dist/tools/taskTool.d.ts.map +1 -1
  91. package/dist/tools/taskTool.js +192 -187
  92. package/dist/tools/types.d.ts +11 -1
  93. package/dist/tools/types.d.ts.map +1 -1
  94. package/dist/tools/writeTool.d.ts.map +1 -1
  95. package/dist/tools/writeTool.js +4 -2
  96. package/dist/types/marketplace.d.ts +8 -0
  97. package/dist/types/marketplace.d.ts.map +1 -1
  98. package/dist/types/permissions.d.ts +1 -1
  99. package/dist/types/permissions.d.ts.map +1 -1
  100. package/dist/types/permissions.js +1 -3
  101. package/dist/types/skills.d.ts +0 -2
  102. package/dist/types/skills.d.ts.map +1 -1
  103. package/dist/types/tools.d.ts +0 -15
  104. package/dist/types/tools.d.ts.map +1 -1
  105. package/dist/utils/container.d.ts +31 -0
  106. package/dist/utils/container.d.ts.map +1 -0
  107. package/dist/utils/container.js +79 -0
  108. package/dist/utils/containerSetup.d.ts +26 -0
  109. package/dist/utils/containerSetup.d.ts.map +1 -0
  110. package/dist/utils/containerSetup.js +165 -0
  111. package/dist/utils/editUtils.d.ts +0 -3
  112. package/dist/utils/editUtils.d.ts.map +1 -1
  113. package/dist/utils/editUtils.js +4 -3
  114. package/dist/utils/hookMatcher.d.ts +1 -1
  115. package/dist/utils/hookMatcher.d.ts.map +1 -1
  116. package/dist/utils/hookMatcher.js +2 -2
  117. package/dist/utils/openaiClient.js +2 -2
  118. package/dist/utils/stringUtils.d.ts +6 -0
  119. package/dist/utils/stringUtils.d.ts.map +1 -1
  120. package/dist/utils/stringUtils.js +8 -0
  121. package/package.json +1 -1
  122. package/src/agent.ts +60 -282
  123. package/src/constants/tools.ts +0 -2
  124. package/src/core/plugin.ts +224 -0
  125. package/src/index.ts +1 -6
  126. package/src/managers/MemoryRuleManager.ts +6 -1
  127. package/src/managers/aiManager.ts +83 -58
  128. package/src/managers/backgroundTaskManager.ts +5 -1
  129. package/src/managers/bashManager.ts +9 -4
  130. package/src/managers/foregroundTaskManager.ts +3 -0
  131. package/src/managers/hookManager.ts +21 -23
  132. package/src/managers/liveConfigManager.ts +57 -53
  133. package/src/managers/lspManager.ts +14 -19
  134. package/src/managers/mcpManager.ts +20 -20
  135. package/src/managers/messageManager.ts +19 -12
  136. package/src/managers/permissionManager.ts +45 -70
  137. package/src/managers/planManager.ts +26 -7
  138. package/src/managers/pluginManager.ts +37 -33
  139. package/src/managers/reversionManager.ts +5 -3
  140. package/src/managers/skillManager.ts +19 -20
  141. package/src/managers/slashCommandManager.ts +30 -25
  142. package/src/managers/subagentManager.ts +53 -53
  143. package/src/managers/toolManager.ts +91 -90
  144. package/src/prompts/index.ts +12 -24
  145. package/src/services/MarketplaceService.ts +13 -0
  146. package/src/services/aiService.ts +61 -15
  147. package/src/services/configurationService.ts +34 -13
  148. package/src/services/taskManager.ts +5 -1
  149. package/src/tools/bashTool.ts +2 -2
  150. package/src/tools/editTool.ts +9 -1
  151. package/src/tools/readTool.ts +2 -2
  152. package/src/tools/skillTool.ts +75 -71
  153. package/src/tools/taskOutputTool.ts +1 -1
  154. package/src/tools/taskTool.ts +224 -225
  155. package/src/tools/types.ts +12 -1
  156. package/src/tools/writeTool.ts +4 -2
  157. package/src/types/marketplace.ts +9 -0
  158. package/src/types/permissions.ts +0 -4
  159. package/src/types/skills.ts +0 -3
  160. package/src/types/tools.ts +0 -17
  161. package/src/utils/container.ts +92 -0
  162. package/src/utils/containerSetup.ts +256 -0
  163. package/src/utils/editUtils.ts +4 -3
  164. package/src/utils/hookMatcher.ts +2 -2
  165. package/src/utils/openaiClient.ts +2 -2
  166. package/src/utils/stringUtils.ts +9 -0
  167. package/dist/tools/deleteFileTool.d.ts +0 -6
  168. package/dist/tools/deleteFileTool.d.ts.map +0 -1
  169. package/dist/tools/deleteFileTool.js +0 -100
  170. package/dist/tools/multiEditTool.d.ts +0 -6
  171. package/dist/tools/multiEditTool.d.ts.map +0 -1
  172. package/dist/tools/multiEditTool.js +0 -246
  173. package/src/tools/deleteFileTool.ts +0 -127
  174. 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
- };