@probelabs/probe 0.6.0-rc258 → 0.6.0-rc259

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/cjs/index.cjs CHANGED
@@ -35867,7 +35867,7 @@ async function handleLineEdit({ resolvedPath: resolvedPath2, file_path, start_li
35867
35867
  return buildLineEditResponse(file_path, startLine, endLine, newLines.length, fileLines, startLine - 1, action, modifications);
35868
35868
  }
35869
35869
  }
35870
- var import_ai, import_fs4, import_path5, import_fs5, editTool, createTool, editSchema, createSchema, editDescription, createDescription, editToolDefinition, createToolDefinition;
35870
+ var import_ai, import_fs4, import_path5, import_fs5, editTool, createTool, multiEditTool, editSchema, createSchema, multiEditSchema, editDescription, createDescription, multiEditDescription, editToolDefinition, createToolDefinition, multiEditToolDefinition;
35871
35871
  var init_edit = __esm({
35872
35872
  "src/tools/edit.js"() {
35873
35873
  "use strict";
@@ -35941,7 +35941,7 @@ Parameters:
35941
35941
  },
35942
35942
  required: ["file_path", "new_string"]
35943
35943
  },
35944
- execute: async ({ file_path, old_string, new_string, replace_all = false, symbol: symbol15, position, start_line, end_line }) => {
35944
+ execute: async ({ file_path, old_string, new_string, replace_all = false, symbol: symbol15, position, start_line, end_line, workingDirectory }) => {
35945
35945
  try {
35946
35946
  if (!file_path || typeof file_path !== "string" || file_path.trim() === "") {
35947
35947
  return `Error editing file: Invalid file_path - must be a non-empty string. Provide an absolute path or a path relative to the working directory (e.g. "src/main.js").`;
@@ -35949,7 +35949,8 @@ Parameters:
35949
35949
  if (new_string === void 0 || new_string === null || typeof new_string !== "string") {
35950
35950
  return `Error editing file: Invalid new_string - must be a string. Provide the replacement content as a string value (empty string "" is valid for deletions).`;
35951
35951
  }
35952
- const resolvedPath2 = (0, import_path5.isAbsolute)(file_path) ? file_path : (0, import_path5.resolve)(cwd || process.cwd(), file_path);
35952
+ const effectiveCwd = workingDirectory || cwd || process.cwd();
35953
+ const resolvedPath2 = (0, import_path5.isAbsolute)(file_path) ? file_path : (0, import_path5.resolve)(effectiveCwd, file_path);
35953
35954
  if (debug) {
35954
35955
  console.error(`[Edit] Attempting to edit file: ${resolvedPath2}`);
35955
35956
  }
@@ -36056,7 +36057,7 @@ Important:
36056
36057
  },
36057
36058
  required: ["file_path", "content"]
36058
36059
  },
36059
- execute: async ({ file_path, content, overwrite = false }) => {
36060
+ execute: async ({ file_path, content, overwrite = false, workingDirectory }) => {
36060
36061
  try {
36061
36062
  if (!file_path || typeof file_path !== "string" || file_path.trim() === "") {
36062
36063
  return `Error creating file: Invalid file_path - must be a non-empty string. Provide an absolute path or a path relative to the working directory (e.g. "src/newFile.js").`;
@@ -36064,7 +36065,8 @@ Important:
36064
36065
  if (content === void 0 || content === null || typeof content !== "string") {
36065
36066
  return `Error creating file: Invalid content - must be a string. Provide the file content as a string value (empty string "" is valid for an empty file).`;
36066
36067
  }
36067
- const resolvedPath2 = (0, import_path5.isAbsolute)(file_path) ? file_path : (0, import_path5.resolve)(cwd || process.cwd(), file_path);
36068
+ const effectiveCwd = workingDirectory || cwd || process.cwd();
36069
+ const resolvedPath2 = (0, import_path5.isAbsolute)(file_path) ? file_path : (0, import_path5.resolve)(effectiveCwd, file_path);
36068
36070
  if (debug) {
36069
36071
  console.error(`[Create] Attempting to create file: ${resolvedPath2}`);
36070
36072
  }
@@ -36093,6 +36095,70 @@ Important:
36093
36095
  }
36094
36096
  });
36095
36097
  };
36098
+ multiEditTool = (options = {}) => {
36099
+ const editInstance = editTool(options);
36100
+ return (0, import_ai.tool)({
36101
+ name: "multi_edit",
36102
+ description: "Apply multiple file edits in a single tool call. Accepts a JSON array of edit operations.",
36103
+ inputSchema: {
36104
+ type: "object",
36105
+ properties: {
36106
+ edits: {
36107
+ type: "string",
36108
+ description: "JSON array of edit operations. Each object supports: file_path, old_string, new_string, replace_all, symbol, position, start_line, end_line."
36109
+ }
36110
+ },
36111
+ required: ["edits"]
36112
+ },
36113
+ execute: async ({ edits: rawEdits }) => {
36114
+ let edits;
36115
+ if (typeof rawEdits === "string") {
36116
+ try {
36117
+ edits = JSON.parse(rawEdits);
36118
+ } catch (e5) {
36119
+ return `Error: Invalid JSON in edits parameter - ${e5.message}. Provide a raw JSON array between <edits> tags.`;
36120
+ }
36121
+ } else if (Array.isArray(rawEdits)) {
36122
+ edits = rawEdits;
36123
+ } else {
36124
+ return "Error: edits must be a JSON array of edit operations.";
36125
+ }
36126
+ if (!Array.isArray(edits) || edits.length === 0) {
36127
+ return "Error: edits must be a non-empty JSON array.";
36128
+ }
36129
+ if (edits.length > 50) {
36130
+ return `Error: Too many edits (${edits.length}). Maximum 50 per batch.`;
36131
+ }
36132
+ const results = [];
36133
+ let successCount = 0;
36134
+ let failCount = 0;
36135
+ for (let i5 = 0; i5 < edits.length; i5++) {
36136
+ const editOp = edits[i5];
36137
+ if (!editOp || typeof editOp !== "object" || Array.isArray(editOp)) {
36138
+ results.push(`[${i5 + 1}] FAIL: Invalid edit operation - must be an object`);
36139
+ failCount++;
36140
+ continue;
36141
+ }
36142
+ try {
36143
+ const result = await editInstance.execute(editOp);
36144
+ const isError = typeof result === "string" && result.startsWith("Error");
36145
+ if (isError) {
36146
+ results.push(`[${i5 + 1}] FAIL: ${result}`);
36147
+ failCount++;
36148
+ } else {
36149
+ results.push(`[${i5 + 1}] OK: ${result}`);
36150
+ successCount++;
36151
+ }
36152
+ } catch (error2) {
36153
+ results.push(`[${i5 + 1}] FAIL: ${error2.message}`);
36154
+ failCount++;
36155
+ }
36156
+ }
36157
+ const summary = `Multi-edit: ${successCount}/${edits.length} succeeded` + (failCount > 0 ? `, ${failCount} failed` : "");
36158
+ return summary + "\n\n" + results.join("\n");
36159
+ }
36160
+ });
36161
+ };
36096
36162
  editSchema = {
36097
36163
  type: "object",
36098
36164
  properties: {
@@ -36150,8 +36216,19 @@ Important:
36150
36216
  },
36151
36217
  required: ["file_path", "content"]
36152
36218
  };
36219
+ multiEditSchema = {
36220
+ type: "object",
36221
+ properties: {
36222
+ edits: {
36223
+ type: "string",
36224
+ description: "JSON array of edit operations"
36225
+ }
36226
+ },
36227
+ required: ["edits"]
36228
+ };
36153
36229
  editDescription = "Edit files using text replacement, AST-aware symbol operations, or line-targeted editing. Supports fuzzy matching for text edits and optional hash-based integrity verification for line edits.";
36154
36230
  createDescription = "Create new files with specified content. Will create parent directories if needed.";
36231
+ multiEditDescription = "Apply multiple file edits in a single tool call. Accepts a JSON array of edit operations, each supporting the same modes as the edit tool.";
36155
36232
  editToolDefinition = `
36156
36233
  ## edit
36157
36234
  Description: ${editDescription}
@@ -36307,6 +36384,44 @@ Examples:
36307
36384
  This is a new project.</content>
36308
36385
  <overwrite>true</overwrite>
36309
36386
  </create>`;
36387
+ multiEditToolDefinition = `
36388
+ ## multi_edit
36389
+ Description: ${multiEditDescription}
36390
+
36391
+ Apply multiple edits in one call. Each operation in the array uses the same parameters as the edit tool:
36392
+ - file_path, old_string, new_string (text mode)
36393
+ - file_path, symbol, new_string (symbol replace)
36394
+ - file_path, symbol, new_string, position (symbol insert)
36395
+ - file_path, start_line, new_string (line-targeted)
36396
+
36397
+ Edits are applied sequentially. Failures do not stop remaining edits. Maximum 50 edits per call.
36398
+
36399
+ Parameters:
36400
+ - edits: (required) JSON array of edit objects. Place raw JSON between tags.
36401
+
36402
+ When to use multi_edit vs edit:
36403
+ - Use edit for a single change to one file
36404
+ - Use multi_edit when making 2+ related changes across files (e.g., rename a function and update all call sites)
36405
+ - Use multi_edit for coordinated multi-file refactoring where order matters
36406
+
36407
+ Examples:
36408
+
36409
+ Multiple text replacements across files:
36410
+ <multi_edit>
36411
+ <edits>[
36412
+ {"file_path": "src/main.js", "old_string": "return false;", "new_string": "return true;"},
36413
+ {"file_path": "src/config.js", "old_string": "debug: false", "new_string": "debug: true"}
36414
+ ]</edits>
36415
+ </multi_edit>
36416
+
36417
+ Mixed edit modes in one batch:
36418
+ <multi_edit>
36419
+ <edits>[
36420
+ {"file_path": "src/utils.js", "symbol": "oldHelper", "new_string": "function newHelper() { return 42; }"},
36421
+ {"file_path": "src/main.js", "old_string": "oldHelper()", "new_string": "newHelper()", "replace_all": true},
36422
+ {"file_path": "src/index.js", "start_line": "10", "end_line": "12", "new_string": "export { newHelper };"}
36423
+ ]</edits>
36424
+ </multi_edit>`;
36310
36425
  }
36311
36426
  });
36312
36427
 
@@ -36799,7 +36914,8 @@ function getValidParamsForTool(toolName) {
36799
36914
  task: taskSchema,
36800
36915
  attempt_completion: attemptCompletionSchema,
36801
36916
  edit: editSchema,
36802
- create: createSchema
36917
+ create: createSchema,
36918
+ multi_edit: multiEditSchema
36803
36919
  };
36804
36920
  const schema = schemaMap[toolName];
36805
36921
  if (!schema) {
@@ -36839,7 +36955,7 @@ function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
36839
36955
  const closeTag = `</${toolName}>`;
36840
36956
  const openIndex = earliestOpenIndex;
36841
36957
  let closeIndex;
36842
- if (toolName === "attempt_completion") {
36958
+ if (LAST_INDEX_TOOLS.has(toolName)) {
36843
36959
  closeIndex = xmlString.lastIndexOf(closeTag);
36844
36960
  if (closeIndex !== -1 && closeIndex <= openIndex + openTag.length) {
36845
36961
  closeIndex = -1;
@@ -36864,7 +36980,15 @@ function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
36864
36980
  if (paramOpenIndex === -1) {
36865
36981
  continue;
36866
36982
  }
36867
- let paramCloseIndex = innerContent.indexOf(paramCloseTag, paramOpenIndex + paramOpenTag.length);
36983
+ let paramCloseIndex;
36984
+ if (RAW_CONTENT_PARAMS.has(paramName)) {
36985
+ paramCloseIndex = innerContent.lastIndexOf(paramCloseTag);
36986
+ if (paramCloseIndex !== -1 && paramCloseIndex <= paramOpenIndex + paramOpenTag.length) {
36987
+ paramCloseIndex = -1;
36988
+ }
36989
+ } else {
36990
+ paramCloseIndex = innerContent.indexOf(paramCloseTag, paramOpenIndex + paramOpenTag.length);
36991
+ }
36868
36992
  if (paramCloseIndex === -1) {
36869
36993
  let nextTagIndex = innerContent.length;
36870
36994
  for (const nextParam of validParams) {
@@ -36876,18 +37000,26 @@ function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
36876
37000
  }
36877
37001
  paramCloseIndex = nextTagIndex;
36878
37002
  }
36879
- let paramValue = unescapeXmlEntities(innerContent.substring(
37003
+ const rawValue = innerContent.substring(
36880
37004
  paramOpenIndex + paramOpenTag.length,
36881
37005
  paramCloseIndex
36882
- ).trim());
36883
- if (paramValue.toLowerCase() === "true") {
36884
- paramValue = true;
36885
- } else if (paramValue.toLowerCase() === "false") {
36886
- paramValue = false;
36887
- } else if (!isNaN(paramValue) && paramValue.trim() !== "") {
36888
- const num = Number(paramValue);
36889
- if (Number.isFinite(num)) {
36890
- paramValue = num;
37006
+ );
37007
+ let paramValue;
37008
+ if (RAW_CONTENT_PARAMS.has(paramName)) {
37009
+ paramValue = unescapeXmlEntities(rawValue.replace(/^\n/, "").replace(/\n$/, ""));
37010
+ } else {
37011
+ paramValue = unescapeXmlEntities(rawValue.trim());
37012
+ }
37013
+ if (!RAW_CONTENT_PARAMS.has(paramName)) {
37014
+ if (paramValue.toLowerCase() === "true") {
37015
+ paramValue = true;
37016
+ } else if (paramValue.toLowerCase() === "false") {
37017
+ paramValue = false;
37018
+ } else if (!isNaN(paramValue) && paramValue.trim() !== "") {
37019
+ const num = Number(paramValue);
37020
+ if (Number.isFinite(num)) {
37021
+ paramValue = num;
37022
+ }
36891
37023
  }
36892
37024
  }
36893
37025
  params[paramName] = paramValue;
@@ -36930,6 +37062,7 @@ function detectUnrecognizedToolCall(xmlString, validTools) {
36930
37062
  "readImage",
36931
37063
  "edit",
36932
37064
  "create",
37065
+ "multi_edit",
36933
37066
  "delegate",
36934
37067
  "bash",
36935
37068
  "task",
@@ -37059,7 +37192,7 @@ function resolveTargetPath(target, cwd) {
37059
37192
  }
37060
37193
  return filePart + suffix;
37061
37194
  }
37062
- var import_path6, searchSchema, searchAllSchema, querySchema, extractSchema, delegateSchema, listSkillsSchema, useSkillSchema, bashSchema, analyzeAllSchema, executePlanSchema, cleanupExecutePlanSchema, attemptCompletionSchema, searchToolDefinition, queryToolDefinition, extractToolDefinition, delegateToolDefinition, attemptCompletionToolDefinition, analyzeAllToolDefinition, bashToolDefinition, googleSearchToolDefinition, urlContextToolDefinition, searchDescription, queryDescription, extractDescription, delegateDescription, bashDescription, analyzeAllDescription, DEFAULT_VALID_TOOLS;
37195
+ var import_path6, searchSchema, searchAllSchema, querySchema, extractSchema, delegateSchema, listSkillsSchema, useSkillSchema, bashSchema, analyzeAllSchema, executePlanSchema, cleanupExecutePlanSchema, attemptCompletionSchema, searchToolDefinition, queryToolDefinition, extractToolDefinition, delegateToolDefinition, attemptCompletionToolDefinition, analyzeAllToolDefinition, bashToolDefinition, googleSearchToolDefinition, urlContextToolDefinition, searchDescription, queryDescription, extractDescription, delegateDescription, bashDescription, analyzeAllDescription, DEFAULT_VALID_TOOLS, RAW_CONTENT_PARAMS, LAST_INDEX_TOOLS;
37063
37196
  var init_common2 = __esm({
37064
37197
  "src/tools/common.js"() {
37065
37198
  "use strict";
@@ -37479,6 +37612,8 @@ Capabilities:
37479
37612
  "task",
37480
37613
  "attempt_completion"
37481
37614
  ];
37615
+ RAW_CONTENT_PARAMS = /* @__PURE__ */ new Set(["content", "new_string", "old_string"]);
37616
+ LAST_INDEX_TOOLS = /* @__PURE__ */ new Set(["attempt_completion", "create", "edit"]);
37482
37617
  }
37483
37618
  });
37484
37619
 
@@ -38144,6 +38279,9 @@ function createTools(configOptions) {
38144
38279
  if (configOptions.allowEdit && isToolAllowed("create")) {
38145
38280
  tools2.createTool = createTool(configOptions);
38146
38281
  }
38282
+ if (configOptions.allowEdit && isToolAllowed("multi_edit")) {
38283
+ tools2.multiEditTool = multiEditTool(configOptions);
38284
+ }
38147
38285
  return tools2;
38148
38286
  }
38149
38287
  function parseXmlToolCallWithThinking(xmlString, validTools) {
@@ -45529,6 +45667,13 @@ function createWrappedTools(baseTools) {
45529
45667
  baseTools.createTool.execute
45530
45668
  );
45531
45669
  }
45670
+ if (baseTools.multiEditTool) {
45671
+ wrappedTools.multiEditToolInstance = wrapToolWithEmitter(
45672
+ baseTools.multiEditTool,
45673
+ "multi_edit",
45674
+ baseTools.multiEditTool.execute
45675
+ );
45676
+ }
45532
45677
  return wrappedTools;
45533
45678
  }
45534
45679
  var import_child_process6, import_util11, import_crypto4, import_events, import_fs7, import_fs8, import_path8, toolCallEmitter, activeToolExecutions, wrapToolWithEmitter, listFilesTool, searchFilesTool, listFilesToolInstance, searchFilesToolInstance;
@@ -108617,6 +108762,9 @@ var init_ProbeAgent = __esm({
108617
108762
  if (wrappedTools.createToolInstance && isToolAllowed("create")) {
108618
108763
  this.toolImplementations.create = wrappedTools.createToolInstance;
108619
108764
  }
108765
+ if (wrappedTools.multiEditToolInstance && isToolAllowed("multi_edit")) {
108766
+ this.toolImplementations.multi_edit = wrappedTools.multiEditToolInstance;
108767
+ }
108620
108768
  }
108621
108769
  this.wrappedTools = wrappedTools;
108622
108770
  if (this.debug) {
@@ -109906,6 +110054,10 @@ Workspace: ${this.allowedFolders.join(", ")}`;
109906
110054
  }
109907
110055
  if (this.allowEdit && isToolAllowed("create")) {
109908
110056
  toolDefinitions += `${createToolDefinition}
110057
+ `;
110058
+ }
110059
+ if (this.allowEdit && isToolAllowed("multi_edit")) {
110060
+ toolDefinitions += `${multiEditToolDefinition}
109909
110061
  `;
109910
110062
  }
109911
110063
  if (this.enableBash && isToolAllowed("bash")) {
@@ -110002,6 +110154,9 @@ The configuration is loaded from src/config.js lines 15-25 which contains the da
110002
110154
  if (this.allowEdit && isToolAllowed("create")) {
110003
110155
  availableToolsList += "- create: Create new files with specified content.\n";
110004
110156
  }
110157
+ if (this.allowEdit && isToolAllowed("multi_edit")) {
110158
+ availableToolsList += "- multi_edit: Apply multiple file edits in one call using a JSON array of operations.\n";
110159
+ }
110005
110160
  if (this.enableDelegate && isToolAllowed("delegate")) {
110006
110161
  availableToolsList += "- delegate: Delegate big distinct tasks to specialized probe subagents.\n";
110007
110162
  }
@@ -110596,6 +110751,9 @@ You are working with a workspace. Available paths: ${workspaceDesc}
110596
110751
  if (this.allowEdit && this.allowedTools.isEnabled("create")) {
110597
110752
  validTools.push("create");
110598
110753
  }
110754
+ if (this.allowEdit && this.allowedTools.isEnabled("multi_edit")) {
110755
+ validTools.push("multi_edit");
110756
+ }
110599
110757
  if (this.enableBash && this.allowedTools.isEnabled("bash")) {
110600
110758
  validTools.push("bash");
110601
110759
  }
@@ -113702,6 +113860,10 @@ __export(tools_exports, {
113702
113860
  extractTool: () => extractTool,
113703
113861
  getCleanupExecutePlanToolDefinition: () => getCleanupExecutePlanToolDefinition,
113704
113862
  getExecutePlanToolDefinition: () => getExecutePlanToolDefinition,
113863
+ multiEditDescription: () => multiEditDescription,
113864
+ multiEditSchema: () => multiEditSchema,
113865
+ multiEditTool: () => multiEditTool,
113866
+ multiEditToolDefinition: () => multiEditToolDefinition,
113705
113867
  parseAndResolvePaths: () => parseAndResolvePaths,
113706
113868
  querySchema: () => querySchema,
113707
113869
  queryTool: () => queryTool,
@@ -114353,6 +114515,9 @@ __export(index_exports, {
114353
114515
  initializeSimpleTelemetryFromOptions: () => initializeSimpleTelemetryFromOptions,
114354
114516
  listFilesByLevel: () => listFilesByLevel,
114355
114517
  listFilesToolInstance: () => listFilesToolInstance,
114518
+ multiEditSchema: () => multiEditSchema,
114519
+ multiEditTool: () => multiEditTool,
114520
+ multiEditToolDefinition: () => multiEditToolDefinition,
114356
114521
  parseXmlToolCall: () => parseXmlToolCall,
114357
114522
  query: () => query,
114358
114523
  querySchema: () => querySchema,
@@ -114448,6 +114613,9 @@ init_index();
114448
114613
  initializeSimpleTelemetryFromOptions,
114449
114614
  listFilesByLevel,
114450
114615
  listFilesToolInstance,
114616
+ multiEditSchema,
114617
+ multiEditTool,
114618
+ multiEditToolDefinition,
114451
114619
  parseXmlToolCall,
114452
114620
  query,
114453
114621
  querySchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc258",
3
+ "version": "0.6.0-rc259",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -59,6 +59,7 @@ import {
59
59
  attemptCompletionToolDefinition,
60
60
  editToolDefinition,
61
61
  createToolDefinition,
62
+ multiEditToolDefinition,
62
63
  googleSearchToolDefinition,
63
64
  urlContextToolDefinition,
64
65
  attemptCompletionSchema,
@@ -964,6 +965,9 @@ export class ProbeAgent {
964
965
  if (wrappedTools.createToolInstance && isToolAllowed('create')) {
965
966
  this.toolImplementations.create = wrappedTools.createToolInstance;
966
967
  }
968
+ if (wrappedTools.multiEditToolInstance && isToolAllowed('multi_edit')) {
969
+ this.toolImplementations.multi_edit = wrappedTools.multiEditToolInstance;
970
+ }
967
971
  }
968
972
 
969
973
  // Store wrapped tools for ACP system
@@ -2565,6 +2569,9 @@ ${extractGuidance}
2565
2569
  if (this.allowEdit && isToolAllowed('create')) {
2566
2570
  toolDefinitions += `${createToolDefinition}\n`;
2567
2571
  }
2572
+ if (this.allowEdit && isToolAllowed('multi_edit')) {
2573
+ toolDefinitions += `${multiEditToolDefinition}\n`;
2574
+ }
2568
2575
  // Bash tool (require both enableBash flag AND allowedTools permission)
2569
2576
  if (this.enableBash && isToolAllowed('bash')) {
2570
2577
  toolDefinitions += `${bashToolDefinition}\n`;
@@ -2670,6 +2677,9 @@ The configuration is loaded from src/config.js lines 15-25 which contains the da
2670
2677
  if (this.allowEdit && isToolAllowed('create')) {
2671
2678
  availableToolsList += '- create: Create new files with specified content.\n';
2672
2679
  }
2680
+ if (this.allowEdit && isToolAllowed('multi_edit')) {
2681
+ availableToolsList += '- multi_edit: Apply multiple file edits in one call using a JSON array of operations.\n';
2682
+ }
2673
2683
  if (this.enableDelegate && isToolAllowed('delegate')) {
2674
2684
  availableToolsList += '- delegate: Delegate big distinct tasks to specialized probe subagents.\n';
2675
2685
  }
@@ -3430,6 +3440,9 @@ Follow these instructions carefully:
3430
3440
  if (this.allowEdit && this.allowedTools.isEnabled('create')) {
3431
3441
  validTools.push('create');
3432
3442
  }
3443
+ if (this.allowEdit && this.allowedTools.isEnabled('multi_edit')) {
3444
+ validTools.push('multi_edit');
3445
+ }
3433
3446
  // Bash tool (require both enableBash flag AND allowedTools permission)
3434
3447
  if (this.enableBash && this.allowedTools.isEnabled('bash')) {
3435
3448
  validTools.push('bash');
@@ -256,6 +256,15 @@ export function createWrappedTools(baseTools) {
256
256
  );
257
257
  }
258
258
 
259
+ // Wrap multi_edit tool
260
+ if (baseTools.multiEditTool) {
261
+ wrappedTools.multiEditToolInstance = wrapToolWithEmitter(
262
+ baseTools.multiEditTool,
263
+ 'multi_edit',
264
+ baseTools.multiEditTool.execute
265
+ );
266
+ }
267
+
259
268
  return wrappedTools;
260
269
  }
261
270
 
@@ -10,6 +10,7 @@ import {
10
10
  bashTool,
11
11
  editTool,
12
12
  createTool,
13
+ multiEditTool,
13
14
  DEFAULT_SYSTEM_MESSAGE,
14
15
  attemptCompletionSchema,
15
16
  attemptCompletionToolDefinition,
@@ -23,6 +24,7 @@ import {
23
24
  bashSchema,
24
25
  editSchema,
25
26
  createSchema,
27
+ multiEditSchema,
26
28
  searchToolDefinition,
27
29
  queryToolDefinition,
28
30
  extractToolDefinition,
@@ -33,6 +35,7 @@ import {
33
35
  bashToolDefinition,
34
36
  editToolDefinition,
35
37
  createToolDefinition,
38
+ multiEditToolDefinition,
36
39
  googleSearchToolDefinition,
37
40
  urlContextToolDefinition,
38
41
  parseXmlToolCall
@@ -87,6 +90,9 @@ export function createTools(configOptions) {
87
90
  if (configOptions.allowEdit && isToolAllowed('create')) {
88
91
  tools.createTool = createTool(configOptions);
89
92
  }
93
+ if (configOptions.allowEdit && isToolAllowed('multi_edit')) {
94
+ tools.multiEditTool = multiEditTool(configOptions);
95
+ }
90
96
  return tools;
91
97
  }
92
98
 
@@ -114,6 +120,7 @@ export {
114
120
  bashSchema,
115
121
  editSchema,
116
122
  createSchema,
123
+ multiEditSchema,
117
124
  attemptCompletionSchema,
118
125
  searchToolDefinition,
119
126
  queryToolDefinition,
@@ -125,6 +132,7 @@ export {
125
132
  bashToolDefinition,
126
133
  editToolDefinition,
127
134
  createToolDefinition,
135
+ multiEditToolDefinition,
128
136
  attemptCompletionToolDefinition,
129
137
  googleSearchToolDefinition,
130
138
  urlContextToolDefinition,
package/src/index.js CHANGED
@@ -44,13 +44,15 @@ import {
44
44
  import {
45
45
  editSchema,
46
46
  createSchema,
47
+ multiEditSchema,
47
48
  editToolDefinition,
48
- createToolDefinition
49
+ createToolDefinition,
50
+ multiEditToolDefinition
49
51
  } from './tools/edit.js';
50
52
  import { searchTool, queryTool, extractTool, delegateTool, analyzeAllTool } from './tools/vercel.js';
51
53
  import { createExecutePlanTool, getExecutePlanToolDefinition, createCleanupExecutePlanTool, getCleanupExecutePlanToolDefinition } from './tools/executePlan.js';
52
54
  import { bashTool } from './tools/bash.js';
53
- import { editTool, createTool } from './tools/edit.js';
55
+ import { editTool, createTool, multiEditTool } from './tools/edit.js';
54
56
  import { FileTracker } from './tools/fileTracker.js';
55
57
  import { ProbeAgent } from './agent/ProbeAgent.js';
56
58
  import { SimpleTelemetry, SimpleAppTracer, initializeSimpleTelemetryFromOptions } from './agent/simpleTelemetry.js';
@@ -99,6 +101,7 @@ export {
99
101
  bashTool,
100
102
  editTool,
101
103
  createTool,
104
+ multiEditTool,
102
105
  FileTracker,
103
106
  // Export tool instances
104
107
  listFilesToolInstance,
@@ -115,6 +118,7 @@ export {
115
118
  bashSchema,
116
119
  editSchema,
117
120
  createSchema,
121
+ multiEditSchema,
118
122
  // Export tool definitions
119
123
  searchToolDefinition,
120
124
  queryToolDefinition,
@@ -127,6 +131,7 @@ export {
127
131
  bashToolDefinition,
128
132
  editToolDefinition,
129
133
  createToolDefinition,
134
+ multiEditToolDefinition,
130
135
  googleSearchToolDefinition,
131
136
  urlContextToolDefinition,
132
137
  // Export parser function
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { z } from 'zod';
7
7
  import { resolve, isAbsolute } from 'path';
8
- import { editSchema, createSchema } from './edit.js';
8
+ import { editSchema, createSchema, multiEditSchema } from './edit.js';
9
9
  import { taskSchema } from '../agent/tasks/taskTool.js';
10
10
 
11
11
  // Common schemas for tool parameters (used for internal execution after XML parsing)
@@ -492,7 +492,8 @@ function getValidParamsForTool(toolName) {
492
492
  task: taskSchema,
493
493
  attempt_completion: attemptCompletionSchema,
494
494
  edit: editSchema,
495
- create: createSchema
495
+ create: createSchema,
496
+ multi_edit: multiEditSchema
496
497
  };
497
498
 
498
499
  const schema = schemaMap[toolName];
@@ -538,6 +539,15 @@ export function unescapeXmlEntities(str) {
538
539
  .replace(/&amp;/g, '&');
539
540
  }
540
541
 
542
+ // Parameters that contain arbitrary code/file content — use lastIndexOf for closing tag
543
+ // to handle cases where the content itself contains the closing tag string.
544
+ const RAW_CONTENT_PARAMS = new Set(['content', 'new_string', 'old_string']);
545
+
546
+ // Tools whose content can include their own closing tag string (e.g., file content
547
+ // containing </create> or </edit>). Use lastIndexOf for outer tag boundary, same
548
+ // strategy already used for attempt_completion.
549
+ const LAST_INDEX_TOOLS = new Set(['attempt_completion', 'create', 'edit']);
550
+
541
551
  // Simple XML parser helper - safer string-based approach
542
552
  export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
543
553
  // Find the tool that appears EARLIEST in the string
@@ -564,13 +574,13 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
564
574
  const closeTag = `</${toolName}>`;
565
575
  const openIndex = earliestOpenIndex;
566
576
 
567
- // For attempt_completion, use lastIndexOf to find the LAST occurrence of closing tag
568
- // This prevents issues where the content contains the closing tag string (e.g., in regex patterns)
569
- // For other tools, use indexOf from the opening tag position
577
+ // For tools that contain arbitrary content (file content, code), use lastIndexOf
578
+ // to find the LAST occurrence of the closing tag. This prevents issues where the
579
+ // content itself contains the closing tag string (e.g., file content with </create>).
580
+ // For other tools, use indexOf from the opening tag position.
570
581
  let closeIndex;
571
- if (toolName === 'attempt_completion') {
582
+ if (LAST_INDEX_TOOLS.has(toolName)) {
572
583
  // Find the last occurrence of the closing tag in the entire string
573
- // This assumes attempt_completion doesn't have nested tags of the same name
574
584
  closeIndex = xmlString.lastIndexOf(closeTag);
575
585
  // Make sure the closing tag is after the opening tag
576
586
  if (closeIndex !== -1 && closeIndex <= openIndex + openTag.length) {
@@ -610,7 +620,19 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
610
620
  continue; // Parameter not found
611
621
  }
612
622
 
613
- let paramCloseIndex = innerContent.indexOf(paramCloseTag, paramOpenIndex + paramOpenTag.length);
623
+ // For raw content params (file content, code), use lastIndexOf to find the
624
+ // LAST closing tag — the content itself may contain the closing tag string.
625
+ // For other params (file_path, overwrite, etc.), use indexOf (first match).
626
+ let paramCloseIndex;
627
+ if (RAW_CONTENT_PARAMS.has(paramName)) {
628
+ paramCloseIndex = innerContent.lastIndexOf(paramCloseTag);
629
+ // Ensure it's after the opening tag
630
+ if (paramCloseIndex !== -1 && paramCloseIndex <= paramOpenIndex + paramOpenTag.length) {
631
+ paramCloseIndex = -1;
632
+ }
633
+ } else {
634
+ paramCloseIndex = innerContent.indexOf(paramCloseTag, paramOpenIndex + paramOpenTag.length);
635
+ }
614
636
 
615
637
  // Handle unclosed parameter tags - use content until next tag or end of content
616
638
  if (paramCloseIndex === -1) {
@@ -626,23 +648,34 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
626
648
  paramCloseIndex = nextTagIndex;
627
649
  }
628
650
 
629
- let paramValue = unescapeXmlEntities(innerContent.substring(
651
+ const rawValue = innerContent.substring(
630
652
  paramOpenIndex + paramOpenTag.length,
631
653
  paramCloseIndex
632
- ).trim());
633
-
634
- // Basic type inference (can be improved)
635
- if (paramValue.toLowerCase() === 'true') {
636
- paramValue = true;
637
- } else if (paramValue.toLowerCase() === 'false') {
638
- paramValue = false;
639
- } else if (!isNaN(paramValue) && paramValue.trim() !== '') {
640
- // Check if it's potentially a number (handle integers and floats)
641
- const num = Number(paramValue);
642
- if (Number.isFinite(num)) { // Use Number.isFinite to avoid Infinity/NaN
643
- paramValue = num;
654
+ );
655
+
656
+ // For raw content params, preserve whitespace (only strip XML formatting newlines).
657
+ // For other params, trim all whitespace.
658
+ let paramValue;
659
+ if (RAW_CONTENT_PARAMS.has(paramName)) {
660
+ paramValue = unescapeXmlEntities(rawValue.replace(/^\n/, '').replace(/\n$/, ''));
661
+ } else {
662
+ paramValue = unescapeXmlEntities(rawValue.trim());
663
+ }
664
+
665
+ // Type coercion for non-content params only (content/new_string/old_string must stay strings)
666
+ if (!RAW_CONTENT_PARAMS.has(paramName)) {
667
+ if (paramValue.toLowerCase() === 'true') {
668
+ paramValue = true;
669
+ } else if (paramValue.toLowerCase() === 'false') {
670
+ paramValue = false;
671
+ } else if (!isNaN(paramValue) && paramValue.trim() !== '') {
672
+ // Check if it's potentially a number (handle integers and floats)
673
+ const num = Number(paramValue);
674
+ if (Number.isFinite(num)) { // Use Number.isFinite to avoid Infinity/NaN
675
+ paramValue = num;
676
+ }
677
+ // Keep as string if not a valid finite number
644
678
  }
645
- // Keep as string if not a valid finite number
646
679
  }
647
680
 
648
681
  params[paramName] = paramValue;
@@ -707,7 +740,7 @@ export function detectUnrecognizedToolCall(xmlString, validTools) {
707
740
  const knownToolNames = [
708
741
  'search', 'query', 'extract', 'listFiles', 'searchFiles',
709
742
  'listSkills', 'useSkill', 'readImage', 'edit',
710
- 'create', 'delegate', 'bash', 'task', 'attempt_completion',
743
+ 'create', 'multi_edit', 'delegate', 'bash', 'task', 'attempt_completion',
711
744
  'attempt_complete', 'read_file', 'write_file', 'run_command',
712
745
  'grep', 'find', 'cat', 'list_directory'
713
746
  ];