@probelabs/probe 0.6.0-rc258 → 0.6.0-rc260
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/bin/binaries/probe-v0.6.0-rc260-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc260-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc260-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/{probe-v0.6.0-rc258-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc260-x86_64-pc-windows-msvc.zip} +0 -0
- package/bin/binaries/probe-v0.6.0-rc260-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.js +13 -0
- package/build/agent/bashCommandUtils.js +3 -0
- package/build/agent/bashPermissions.js +23 -0
- package/build/agent/index.js +223 -26
- package/build/agent/probeTool.js +9 -0
- package/build/agent/schemaUtils.js +40 -4
- package/build/agent/tools.js +8 -0
- package/build/index.js +7 -2
- package/build/tools/common.js +56 -23
- package/build/tools/edit.js +139 -6
- package/build/tools/index.js +5 -2
- package/cjs/agent/ProbeAgent.cjs +296 -99
- package/cjs/index.cjs +306 -99
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +13 -0
- package/src/agent/bashCommandUtils.js +3 -0
- package/src/agent/bashPermissions.js +23 -0
- package/src/agent/probeTool.js +9 -0
- package/src/agent/schemaUtils.js +40 -4
- package/src/agent/tools.js +8 -0
- package/src/index.js +7 -2
- package/src/tools/common.js +56 -23
- package/src/tools/edit.js +139 -6
- package/src/tools/index.js +5 -2
- package/bin/binaries/probe-v0.6.0-rc258-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc258-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc258-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc258-x86_64-unknown-linux-musl.tar.gz +0 -0
package/package.json
CHANGED
package/src/agent/ProbeAgent.js
CHANGED
|
@@ -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');
|
|
@@ -127,6 +127,8 @@ export function parseSimpleCommand(command) {
|
|
|
127
127
|
/&&/, // Logical AND
|
|
128
128
|
/\|\|/, // Logical OR
|
|
129
129
|
/(?<!\\);/, // Command separator (but not escaped \;)
|
|
130
|
+
/\n/, // Newline command separator (multi-line commands)
|
|
131
|
+
/\r/, // Carriage return (CRLF line endings)
|
|
130
132
|
/&$/, // Background execution
|
|
131
133
|
/\$\(/, // Command substitution $()
|
|
132
134
|
/`/, // Command substitution ``
|
|
@@ -260,6 +262,7 @@ export function isComplexPattern(pattern) {
|
|
|
260
262
|
/&&/, // Logical AND
|
|
261
263
|
/\|\|/, // Logical OR
|
|
262
264
|
/;/, // Command separator
|
|
265
|
+
/\n/, // Newline command separator
|
|
263
266
|
/&$/, // Background execution
|
|
264
267
|
/\$\(/, // Command substitution $()
|
|
265
268
|
/`/, // Command substitution ``
|
|
@@ -402,6 +402,29 @@ export class BashPermissionChecker {
|
|
|
402
402
|
i++;
|
|
403
403
|
continue;
|
|
404
404
|
}
|
|
405
|
+
// Check for newline (command separator in multi-line scripts)
|
|
406
|
+
// Also handle \r\n (CRLF) line endings
|
|
407
|
+
if (char === '\n' || (char === '\r' && nextChar === '\n')) {
|
|
408
|
+
if (current.trim()) {
|
|
409
|
+
components.push(current.trim());
|
|
410
|
+
}
|
|
411
|
+
current = '';
|
|
412
|
+
if (char === '\r') {
|
|
413
|
+
i += 2; // Skip \r\n
|
|
414
|
+
} else {
|
|
415
|
+
i++;
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (char === '\r') {
|
|
420
|
+
// Bare \r without \n
|
|
421
|
+
if (current.trim()) {
|
|
422
|
+
components.push(current.trim());
|
|
423
|
+
}
|
|
424
|
+
current = '';
|
|
425
|
+
i++;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
405
428
|
}
|
|
406
429
|
|
|
407
430
|
current += char;
|
package/src/agent/probeTool.js
CHANGED
|
@@ -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
|
|
package/src/agent/schemaUtils.js
CHANGED
|
@@ -266,6 +266,36 @@ function normalizeJsonQuotes(str) {
|
|
|
266
266
|
return result;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Check if a code block is embedded within a structured markdown document.
|
|
271
|
+
* Used to avoid extracting embedded JSON examples from markdown text.
|
|
272
|
+
* Issue #456: attempt_completion results containing markdown with JSON code block
|
|
273
|
+
* documentation examples were having the examples extracted as structured output.
|
|
274
|
+
*
|
|
275
|
+
* Normal AI behavior: AI wraps JSON answer in a code block with brief explanation text.
|
|
276
|
+
* Issue #456: AI returns a markdown document that contains JSON code blocks as examples.
|
|
277
|
+
*
|
|
278
|
+
* We distinguish these by checking if the surrounding text contains markdown structural
|
|
279
|
+
* elements (headings) which indicate a document rather than a brief explanation.
|
|
280
|
+
*
|
|
281
|
+
* @param {string} text - The full trimmed text
|
|
282
|
+
* @param {RegExpMatchArray} match - The regex match for the code block
|
|
283
|
+
* @returns {boolean} - true if the code block is embedded in a markdown document
|
|
284
|
+
*/
|
|
285
|
+
function isCodeBlockEmbeddedInDocument(text, match) {
|
|
286
|
+
const beforeBlock = text.substring(0, match.index).trim();
|
|
287
|
+
const afterBlock = text.substring(match.index + match[0].length).trim();
|
|
288
|
+
|
|
289
|
+
// Check if surrounding text contains markdown headings (lines starting with #)
|
|
290
|
+
// This is a strong signal that the content is a structured document, not just explanation text
|
|
291
|
+
const hasMarkdownHeadings = /^#{1,6}\s/m.test(beforeBlock) || /^#{1,6}\s/m.test(afterBlock);
|
|
292
|
+
if (hasMarkdownHeadings) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
269
299
|
/**
|
|
270
300
|
* Clean AI response by extracting JSON content when response contains JSON
|
|
271
301
|
* Only processes responses that contain JSON structures { or [
|
|
@@ -311,15 +341,20 @@ export function cleanSchemaResponse(response) {
|
|
|
311
341
|
}
|
|
312
342
|
|
|
313
343
|
// First, look for JSON after code block markers - similar to mermaid extraction
|
|
344
|
+
// Only extract from code blocks when they are the primary content (not embedded examples in markdown).
|
|
345
|
+
// Issue #456: When attempt_completion result contains markdown with embedded JSON code blocks
|
|
346
|
+
// as documentation examples, extracting those blocks produces wrong structured output.
|
|
347
|
+
// We check that there's no significant text content outside the code block.
|
|
348
|
+
|
|
314
349
|
// Try with json language specifier
|
|
315
350
|
const jsonBlockMatch = trimmed.match(/```json\s*\n([\s\S]*?)\n```/);
|
|
316
|
-
if (jsonBlockMatch) {
|
|
351
|
+
if (jsonBlockMatch && !isCodeBlockEmbeddedInDocument(trimmed, jsonBlockMatch)) {
|
|
317
352
|
return normalizeJsonQuotes(jsonBlockMatch[1].trim());
|
|
318
353
|
}
|
|
319
354
|
|
|
320
355
|
// Try any code block with JSON content
|
|
321
356
|
const anyBlockMatch = trimmed.match(/```\s*\n([{\[][\s\S]*?[}\]])\s*```/);
|
|
322
|
-
if (anyBlockMatch) {
|
|
357
|
+
if (anyBlockMatch && !isCodeBlockEmbeddedInDocument(trimmed, anyBlockMatch)) {
|
|
323
358
|
return normalizeJsonQuotes(anyBlockMatch[1].trim());
|
|
324
359
|
}
|
|
325
360
|
|
|
@@ -331,7 +366,7 @@ export function cleanSchemaResponse(response) {
|
|
|
331
366
|
|
|
332
367
|
for (const pattern of codeBlockPatterns) {
|
|
333
368
|
const match = trimmed.match(pattern);
|
|
334
|
-
if (match) {
|
|
369
|
+
if (match && !isCodeBlockEmbeddedInDocument(trimmed, match)) {
|
|
335
370
|
return normalizeJsonQuotes(match[1].trim());
|
|
336
371
|
}
|
|
337
372
|
}
|
|
@@ -345,10 +380,11 @@ export function cleanSchemaResponse(response) {
|
|
|
345
380
|
}
|
|
346
381
|
|
|
347
382
|
// Look for code block start followed immediately by JSON
|
|
383
|
+
// Only extract if the code block is not embedded in a structured markdown document
|
|
348
384
|
const codeBlockStartPattern = /```(?:json)?\s*\n?\s*([{\[])/;
|
|
349
385
|
const codeBlockMatch = trimmed.match(codeBlockStartPattern);
|
|
350
386
|
|
|
351
|
-
if (codeBlockMatch) {
|
|
387
|
+
if (codeBlockMatch && !isCodeBlockEmbeddedInDocument(trimmed, codeBlockMatch)) {
|
|
352
388
|
const startIndex = codeBlockMatch.index + codeBlockMatch[0].length - 1; // Position of the bracket
|
|
353
389
|
|
|
354
390
|
// Find the matching closing bracket
|
package/src/agent/tools.js
CHANGED
|
@@ -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
|
package/src/tools/common.js
CHANGED
|
@@ -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(/&/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
|
|
568
|
-
//
|
|
569
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
651
|
+
const rawValue = innerContent.substring(
|
|
630
652
|
paramOpenIndex + paramOpenTag.length,
|
|
631
653
|
paramCloseIndex
|
|
632
|
-
)
|
|
633
|
-
|
|
634
|
-
//
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
paramValue =
|
|
639
|
-
} else
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
];
|
package/src/tools/edit.js
CHANGED
|
@@ -363,7 +363,7 @@ Parameters:
|
|
|
363
363
|
required: ['file_path', 'new_string']
|
|
364
364
|
},
|
|
365
365
|
|
|
366
|
-
execute: async ({ file_path, old_string, new_string, replace_all = false, symbol, position, start_line, end_line }) => {
|
|
366
|
+
execute: async ({ file_path, old_string, new_string, replace_all = false, symbol, position, start_line, end_line, workingDirectory }) => {
|
|
367
367
|
try {
|
|
368
368
|
// Validate input parameters
|
|
369
369
|
if (!file_path || typeof file_path !== 'string' || file_path.trim() === '') {
|
|
@@ -373,8 +373,9 @@ Parameters:
|
|
|
373
373
|
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).`;
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
// Resolve the file path
|
|
377
|
-
const
|
|
376
|
+
// Resolve the file path (workingDirectory from runtime takes priority over cwd from tool creation)
|
|
377
|
+
const effectiveCwd = workingDirectory || cwd || process.cwd();
|
|
378
|
+
const resolvedPath = isAbsolute(file_path) ? file_path : resolve(effectiveCwd, file_path);
|
|
378
379
|
|
|
379
380
|
if (debug) {
|
|
380
381
|
console.error(`[Edit] Attempting to edit file: ${resolvedPath}`);
|
|
@@ -530,7 +531,7 @@ Important:
|
|
|
530
531
|
required: ['file_path', 'content']
|
|
531
532
|
},
|
|
532
533
|
|
|
533
|
-
execute: async ({ file_path, content, overwrite = false }) => {
|
|
534
|
+
execute: async ({ file_path, content, overwrite = false, workingDirectory }) => {
|
|
534
535
|
try {
|
|
535
536
|
// Validate input parameters
|
|
536
537
|
if (!file_path || typeof file_path !== 'string' || file_path.trim() === '') {
|
|
@@ -540,8 +541,9 @@ Important:
|
|
|
540
541
|
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).`;
|
|
541
542
|
}
|
|
542
543
|
|
|
543
|
-
// Resolve the file path
|
|
544
|
-
const
|
|
544
|
+
// Resolve the file path (workingDirectory from runtime takes priority over cwd from tool creation)
|
|
545
|
+
const effectiveCwd = workingDirectory || cwd || process.cwd();
|
|
546
|
+
const resolvedPath = isAbsolute(file_path) ? file_path : resolve(effectiveCwd, file_path);
|
|
545
547
|
|
|
546
548
|
if (debug) {
|
|
547
549
|
console.error(`[Create] Attempting to create file: ${resolvedPath}`);
|
|
@@ -587,6 +589,86 @@ Important:
|
|
|
587
589
|
});
|
|
588
590
|
};
|
|
589
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Multi-edit tool — apply multiple edit operations in a single call.
|
|
594
|
+
* Reuses the edit tool internally; edits are applied sequentially.
|
|
595
|
+
*
|
|
596
|
+
* @param {Object} [options] - Same configuration as editTool
|
|
597
|
+
* @returns {Object} Configured multi_edit tool
|
|
598
|
+
*/
|
|
599
|
+
export const multiEditTool = (options = {}) => {
|
|
600
|
+
const editInstance = editTool(options);
|
|
601
|
+
|
|
602
|
+
return tool({
|
|
603
|
+
name: 'multi_edit',
|
|
604
|
+
description: 'Apply multiple file edits in a single tool call. Accepts a JSON array of edit operations.',
|
|
605
|
+
|
|
606
|
+
inputSchema: {
|
|
607
|
+
type: 'object',
|
|
608
|
+
properties: {
|
|
609
|
+
edits: {
|
|
610
|
+
type: 'string',
|
|
611
|
+
description: 'JSON array of edit operations. Each object supports: file_path, old_string, new_string, replace_all, symbol, position, start_line, end_line.'
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
required: ['edits']
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
execute: async ({ edits: rawEdits }) => {
|
|
618
|
+
let edits;
|
|
619
|
+
if (typeof rawEdits === 'string') {
|
|
620
|
+
try {
|
|
621
|
+
edits = JSON.parse(rawEdits);
|
|
622
|
+
} catch (e) {
|
|
623
|
+
return `Error: Invalid JSON in edits parameter - ${e.message}. Provide a raw JSON array between <edits> tags.`;
|
|
624
|
+
}
|
|
625
|
+
} else if (Array.isArray(rawEdits)) {
|
|
626
|
+
edits = rawEdits;
|
|
627
|
+
} else {
|
|
628
|
+
return 'Error: edits must be a JSON array of edit operations.';
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
632
|
+
return 'Error: edits must be a non-empty JSON array.';
|
|
633
|
+
}
|
|
634
|
+
if (edits.length > 50) {
|
|
635
|
+
return `Error: Too many edits (${edits.length}). Maximum 50 per batch.`;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const results = [];
|
|
639
|
+
let successCount = 0;
|
|
640
|
+
let failCount = 0;
|
|
641
|
+
|
|
642
|
+
for (let i = 0; i < edits.length; i++) {
|
|
643
|
+
const editOp = edits[i];
|
|
644
|
+
if (!editOp || typeof editOp !== 'object' || Array.isArray(editOp)) {
|
|
645
|
+
results.push(`[${i + 1}] FAIL: Invalid edit operation - must be an object`);
|
|
646
|
+
failCount++;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
const result = await editInstance.execute(editOp);
|
|
651
|
+
const isError = typeof result === 'string' && result.startsWith('Error');
|
|
652
|
+
if (isError) {
|
|
653
|
+
results.push(`[${i + 1}] FAIL: ${result}`);
|
|
654
|
+
failCount++;
|
|
655
|
+
} else {
|
|
656
|
+
results.push(`[${i + 1}] OK: ${result}`);
|
|
657
|
+
successCount++;
|
|
658
|
+
}
|
|
659
|
+
} catch (error) {
|
|
660
|
+
results.push(`[${i + 1}] FAIL: ${error.message}`);
|
|
661
|
+
failCount++;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const summary = `Multi-edit: ${successCount}/${edits.length} succeeded` +
|
|
666
|
+
(failCount > 0 ? `, ${failCount} failed` : '');
|
|
667
|
+
return summary + '\n\n' + results.join('\n');
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
};
|
|
671
|
+
|
|
590
672
|
// Export schemas for tool definitions
|
|
591
673
|
export const editSchema = {
|
|
592
674
|
type: 'object',
|
|
@@ -647,9 +729,21 @@ export const createSchema = {
|
|
|
647
729
|
required: ['file_path', 'content']
|
|
648
730
|
};
|
|
649
731
|
|
|
732
|
+
export const multiEditSchema = {
|
|
733
|
+
type: 'object',
|
|
734
|
+
properties: {
|
|
735
|
+
edits: {
|
|
736
|
+
type: 'string',
|
|
737
|
+
description: 'JSON array of edit operations'
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
required: ['edits']
|
|
741
|
+
};
|
|
742
|
+
|
|
650
743
|
// Tool descriptions for XML definitions
|
|
651
744
|
export const 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.';
|
|
652
745
|
export const createDescription = 'Create new files with specified content. Will create parent directories if needed.';
|
|
746
|
+
export const 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.';
|
|
653
747
|
|
|
654
748
|
// XML tool definitions
|
|
655
749
|
export const editToolDefinition = `
|
|
@@ -808,3 +902,42 @@ Examples:
|
|
|
808
902
|
This is a new project.</content>
|
|
809
903
|
<overwrite>true</overwrite>
|
|
810
904
|
</create>`;
|
|
905
|
+
|
|
906
|
+
export const multiEditToolDefinition = `
|
|
907
|
+
## multi_edit
|
|
908
|
+
Description: ${multiEditDescription}
|
|
909
|
+
|
|
910
|
+
Apply multiple edits in one call. Each operation in the array uses the same parameters as the edit tool:
|
|
911
|
+
- file_path, old_string, new_string (text mode)
|
|
912
|
+
- file_path, symbol, new_string (symbol replace)
|
|
913
|
+
- file_path, symbol, new_string, position (symbol insert)
|
|
914
|
+
- file_path, start_line, new_string (line-targeted)
|
|
915
|
+
|
|
916
|
+
Edits are applied sequentially. Failures do not stop remaining edits. Maximum 50 edits per call.
|
|
917
|
+
|
|
918
|
+
Parameters:
|
|
919
|
+
- edits: (required) JSON array of edit objects. Place raw JSON between tags.
|
|
920
|
+
|
|
921
|
+
When to use multi_edit vs edit:
|
|
922
|
+
- Use edit for a single change to one file
|
|
923
|
+
- Use multi_edit when making 2+ related changes across files (e.g., rename a function and update all call sites)
|
|
924
|
+
- Use multi_edit for coordinated multi-file refactoring where order matters
|
|
925
|
+
|
|
926
|
+
Examples:
|
|
927
|
+
|
|
928
|
+
Multiple text replacements across files:
|
|
929
|
+
<multi_edit>
|
|
930
|
+
<edits>[
|
|
931
|
+
{"file_path": "src/main.js", "old_string": "return false;", "new_string": "return true;"},
|
|
932
|
+
{"file_path": "src/config.js", "old_string": "debug: false", "new_string": "debug: true"}
|
|
933
|
+
]</edits>
|
|
934
|
+
</multi_edit>
|
|
935
|
+
|
|
936
|
+
Mixed edit modes in one batch:
|
|
937
|
+
<multi_edit>
|
|
938
|
+
<edits>[
|
|
939
|
+
{"file_path": "src/utils.js", "symbol": "oldHelper", "new_string": "function newHelper() { return 42; }"},
|
|
940
|
+
{"file_path": "src/main.js", "old_string": "oldHelper()", "new_string": "newHelper()", "replace_all": true},
|
|
941
|
+
{"file_path": "src/index.js", "start_line": "10", "end_line": "12", "new_string": "export { newHelper };"}
|
|
942
|
+
]</edits>
|
|
943
|
+
</multi_edit>`;
|
package/src/tools/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Export Vercel AI SDK tool generators
|
|
7
7
|
export { searchTool, queryTool, extractTool, delegateTool } from './vercel.js';
|
|
8
8
|
export { bashTool } from './bash.js';
|
|
9
|
-
export { editTool, createTool } from './edit.js';
|
|
9
|
+
export { editTool, createTool, multiEditTool } from './edit.js';
|
|
10
10
|
|
|
11
11
|
// Export LangChain tools
|
|
12
12
|
export { createSearchTool, createQueryTool, createExtractTool } from './langchain.js';
|
|
@@ -39,10 +39,13 @@ export {
|
|
|
39
39
|
export {
|
|
40
40
|
editSchema,
|
|
41
41
|
createSchema,
|
|
42
|
+
multiEditSchema,
|
|
42
43
|
editDescription,
|
|
43
44
|
createDescription,
|
|
45
|
+
multiEditDescription,
|
|
44
46
|
editToolDefinition,
|
|
45
|
-
createToolDefinition
|
|
47
|
+
createToolDefinition,
|
|
48
|
+
multiEditToolDefinition
|
|
46
49
|
} from './edit.js';
|
|
47
50
|
|
|
48
51
|
// Export system message
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|