@probelabs/probe 0.6.0-rc159 → 0.6.0-rc162
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -1
- package/build/agent/FallbackManager.d.ts +176 -0
- package/build/agent/FallbackManager.js +545 -0
- package/build/agent/ProbeAgent.d.ts +7 -1
- package/build/agent/ProbeAgent.js +545 -89
- package/build/agent/RetryManager.d.ts +157 -0
- package/build/agent/RetryManager.js +334 -0
- package/build/agent/acp/tools.js +6 -2
- package/build/agent/contextCompactor.js +271 -0
- package/build/agent/index.js +2165 -250
- package/build/agent/probeTool.js +20 -2
- package/build/agent/schemaUtils.js +7 -0
- package/build/agent/tools.js +16 -0
- package/build/agent/xmlParsingUtils.js +24 -4
- package/build/index.js +13 -0
- package/build/tools/common.js +21 -4
- package/build/tools/edit.js +409 -0
- package/build/tools/index.js +11 -0
- package/cjs/agent/ProbeAgent.cjs +2620 -606
- package/cjs/index.cjs +2582 -563
- package/package.json +2 -2
- package/src/agent/FallbackManager.d.ts +176 -0
- package/src/agent/FallbackManager.js +545 -0
- package/src/agent/ProbeAgent.d.ts +7 -1
- package/src/agent/ProbeAgent.js +545 -89
- package/src/agent/RetryManager.d.ts +157 -0
- package/src/agent/RetryManager.js +334 -0
- package/src/agent/acp/tools.js +6 -2
- package/src/agent/contextCompactor.js +271 -0
- package/src/agent/index.js +30 -1
- package/src/agent/probeTool.js +20 -2
- package/src/agent/schemaUtils.js +7 -0
- package/src/agent/tools.js +16 -0
- package/src/agent/xmlParsingUtils.js +24 -4
- package/src/index.js +13 -0
- package/src/tools/common.js +21 -4
- package/src/tools/edit.js +409 -0
- package/src/tools/index.js +11 -0
- package/bin/binaries/probe-v0.6.0-rc159-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc159-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc159-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc159-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc159-x86_64-unknown-linux-musl.tar.gz +0 -0
package/build/agent/probeTool.js
CHANGED
|
@@ -204,12 +204,30 @@ export function createWrappedTools(baseTools) {
|
|
|
204
204
|
// Wrap bash tool
|
|
205
205
|
if (baseTools.bashTool) {
|
|
206
206
|
wrappedTools.bashToolInstance = wrapToolWithEmitter(
|
|
207
|
-
baseTools.bashTool,
|
|
208
|
-
'bash',
|
|
207
|
+
baseTools.bashTool,
|
|
208
|
+
'bash',
|
|
209
209
|
baseTools.bashTool.execute
|
|
210
210
|
);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
// Wrap edit tool
|
|
214
|
+
if (baseTools.editTool) {
|
|
215
|
+
wrappedTools.editToolInstance = wrapToolWithEmitter(
|
|
216
|
+
baseTools.editTool,
|
|
217
|
+
'edit',
|
|
218
|
+
baseTools.editTool.execute
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Wrap create tool
|
|
223
|
+
if (baseTools.createTool) {
|
|
224
|
+
wrappedTools.createToolInstance = wrapToolWithEmitter(
|
|
225
|
+
baseTools.createTool,
|
|
226
|
+
'create',
|
|
227
|
+
baseTools.createTool.execute
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
213
231
|
return wrappedTools;
|
|
214
232
|
}
|
|
215
233
|
|
|
@@ -100,6 +100,13 @@ export function decodeHtmlEntities(text) {
|
|
|
100
100
|
/**
|
|
101
101
|
* Clean AI response by extracting JSON content when response contains JSON
|
|
102
102
|
* Only processes responses that contain JSON structures { or [
|
|
103
|
+
*
|
|
104
|
+
* NOTE: This function handles both JSON extraction AND content stripping.
|
|
105
|
+
* Future improvement: Consider splitting into separate functions for better separation of concerns:
|
|
106
|
+
* - extractJsonContent() - Find and extract JSON from response
|
|
107
|
+
* - stripNonJsonContent() - Remove explanatory text while preserving validation-relevant content
|
|
108
|
+
* This would allow validation to run on full responses before content is discarded.
|
|
109
|
+
*
|
|
103
110
|
* @param {string} response - Raw AI response
|
|
104
111
|
* @returns {string} - Cleaned response with JSON boundaries extracted if applicable
|
|
105
112
|
*/
|
package/build/agent/tools.js
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
extractTool,
|
|
6
6
|
delegateTool,
|
|
7
7
|
bashTool,
|
|
8
|
+
editTool,
|
|
9
|
+
createTool,
|
|
8
10
|
DEFAULT_SYSTEM_MESSAGE,
|
|
9
11
|
attemptCompletionSchema,
|
|
10
12
|
attemptCompletionToolDefinition,
|
|
@@ -13,11 +15,15 @@ import {
|
|
|
13
15
|
extractSchema,
|
|
14
16
|
delegateSchema,
|
|
15
17
|
bashSchema,
|
|
18
|
+
editSchema,
|
|
19
|
+
createSchema,
|
|
16
20
|
searchToolDefinition,
|
|
17
21
|
queryToolDefinition,
|
|
18
22
|
extractToolDefinition,
|
|
19
23
|
delegateToolDefinition,
|
|
20
24
|
bashToolDefinition,
|
|
25
|
+
editToolDefinition,
|
|
26
|
+
createToolDefinition,
|
|
21
27
|
parseXmlToolCall
|
|
22
28
|
} from '../index.js';
|
|
23
29
|
import { randomUUID } from 'crypto';
|
|
@@ -37,6 +43,12 @@ export function createTools(configOptions) {
|
|
|
37
43
|
tools.bashTool = bashTool(configOptions);
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
// Add edit and create tools if enabled
|
|
47
|
+
if (configOptions.allowEdit) {
|
|
48
|
+
tools.editTool = editTool(configOptions);
|
|
49
|
+
tools.createTool = createTool(configOptions);
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
return tools;
|
|
41
53
|
}
|
|
42
54
|
|
|
@@ -48,12 +60,16 @@ export {
|
|
|
48
60
|
extractSchema,
|
|
49
61
|
delegateSchema,
|
|
50
62
|
bashSchema,
|
|
63
|
+
editSchema,
|
|
64
|
+
createSchema,
|
|
51
65
|
attemptCompletionSchema,
|
|
52
66
|
searchToolDefinition,
|
|
53
67
|
queryToolDefinition,
|
|
54
68
|
extractToolDefinition,
|
|
55
69
|
delegateToolDefinition,
|
|
56
70
|
bashToolDefinition,
|
|
71
|
+
editToolDefinition,
|
|
72
|
+
createToolDefinition,
|
|
57
73
|
attemptCompletionToolDefinition,
|
|
58
74
|
parseXmlToolCall
|
|
59
75
|
};
|
|
@@ -59,10 +59,30 @@ export function extractThinkingContent(xmlString) {
|
|
|
59
59
|
export function checkAttemptCompleteRecovery(cleanedXmlString, validTools = []) {
|
|
60
60
|
// Check for <attempt_completion> with content (with or without closing tag)
|
|
61
61
|
// This handles: "<attempt_completion>content" or "<attempt_completion>content</attempt_completion>"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
|
|
63
|
+
// IMPORTANT: Use greedy match ([\s\S]*) instead of non-greedy ([\s\S]*?) to handle cases
|
|
64
|
+
// where the content contains the string "</attempt_completion>" (e.g., in regex patterns or code examples).
|
|
65
|
+
// We want to find the LAST occurrence of </attempt_completion>, not the first one.
|
|
66
|
+
const openTagIndex = cleanedXmlString.indexOf('<attempt_completion>');
|
|
67
|
+
if (openTagIndex !== -1) {
|
|
68
|
+
const afterOpenTag = cleanedXmlString.substring(openTagIndex + '<attempt_completion>'.length);
|
|
69
|
+
const closeTagIndex = cleanedXmlString.lastIndexOf('</attempt_completion>');
|
|
70
|
+
|
|
71
|
+
let content;
|
|
72
|
+
let hasClosingTag = false;
|
|
73
|
+
|
|
74
|
+
if (closeTagIndex !== -1 && closeTagIndex >= openTagIndex + '<attempt_completion>'.length) {
|
|
75
|
+
// Found a closing tag at or after the opening tag - extract content between them
|
|
76
|
+
content = cleanedXmlString.substring(
|
|
77
|
+
openTagIndex + '<attempt_completion>'.length,
|
|
78
|
+
closeTagIndex
|
|
79
|
+
).trim();
|
|
80
|
+
hasClosingTag = true;
|
|
81
|
+
} else {
|
|
82
|
+
// No closing tag - use content from opening tag to end of string
|
|
83
|
+
content = afterOpenTag.trim();
|
|
84
|
+
hasClosingTag = false;
|
|
85
|
+
}
|
|
66
86
|
|
|
67
87
|
if (content) {
|
|
68
88
|
// If there's content after the tag, use it as the result
|
package/build/index.js
CHANGED
|
@@ -35,8 +35,15 @@ import {
|
|
|
35
35
|
bashToolDefinition,
|
|
36
36
|
parseXmlToolCall
|
|
37
37
|
} from './tools/common.js';
|
|
38
|
+
import {
|
|
39
|
+
editSchema,
|
|
40
|
+
createSchema,
|
|
41
|
+
editToolDefinition,
|
|
42
|
+
createToolDefinition
|
|
43
|
+
} from './tools/edit.js';
|
|
38
44
|
import { searchTool, queryTool, extractTool, delegateTool } from './tools/vercel.js';
|
|
39
45
|
import { bashTool } from './tools/bash.js';
|
|
46
|
+
import { editTool, createTool } from './tools/edit.js';
|
|
40
47
|
import { ProbeAgent } from './agent/ProbeAgent.js';
|
|
41
48
|
import { SimpleTelemetry, SimpleAppTracer, initializeSimpleTelemetryFromOptions } from './agent/simpleTelemetry.js';
|
|
42
49
|
import { listFilesToolInstance, searchFilesToolInstance } from './agent/probeTool.js';
|
|
@@ -72,6 +79,8 @@ export {
|
|
|
72
79
|
extractTool,
|
|
73
80
|
delegateTool,
|
|
74
81
|
bashTool,
|
|
82
|
+
editTool,
|
|
83
|
+
createTool,
|
|
75
84
|
// Export tool instances
|
|
76
85
|
listFilesToolInstance,
|
|
77
86
|
searchFilesToolInstance,
|
|
@@ -82,6 +91,8 @@ export {
|
|
|
82
91
|
delegateSchema,
|
|
83
92
|
attemptCompletionSchema,
|
|
84
93
|
bashSchema,
|
|
94
|
+
editSchema,
|
|
95
|
+
createSchema,
|
|
85
96
|
// Export tool definitions
|
|
86
97
|
searchToolDefinition,
|
|
87
98
|
queryToolDefinition,
|
|
@@ -89,6 +100,8 @@ export {
|
|
|
89
100
|
delegateToolDefinition,
|
|
90
101
|
attemptCompletionToolDefinition,
|
|
91
102
|
bashToolDefinition,
|
|
103
|
+
editToolDefinition,
|
|
104
|
+
createToolDefinition,
|
|
92
105
|
// Export parser function
|
|
93
106
|
parseXmlToolCall
|
|
94
107
|
};
|
package/build/tools/common.js
CHANGED
|
@@ -15,12 +15,13 @@ export const querySchema = z.object({
|
|
|
15
15
|
pattern: z.string().describe('AST pattern to search for. Use $NAME for variable names, $$$PARAMS for parameter lists, etc.'),
|
|
16
16
|
path: z.string().optional().default('.').describe('Path to search in'),
|
|
17
17
|
language: z.string().optional().default('rust').describe('Programming language to use for parsing'),
|
|
18
|
-
allow_tests: z.boolean().optional().default(
|
|
18
|
+
allow_tests: z.boolean().optional().default(true).describe('Allow test files in search results')
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
export const extractSchema = z.object({
|
|
22
22
|
targets: z.string().optional().describe('File paths or symbols to extract from. Formats: "file.js" (whole file), "file.js:42" (line 42), "file.js:10-20" (lines 10-20), "file.js#funcName" (symbol). Multiple targets separated by spaces.'),
|
|
23
|
-
input_content: z.string().optional().describe('Text content to extract file paths from (alternative to targets)')
|
|
23
|
+
input_content: z.string().optional().describe('Text content to extract file paths from (alternative to targets)'),
|
|
24
|
+
allow_tests: z.boolean().optional().default(true).describe('Include test files in extraction results')
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
export const delegateSchema = z.object({
|
|
@@ -163,7 +164,7 @@ Parameters:
|
|
|
163
164
|
- pattern: (required) AST pattern to search for. Use $NAME for variable names, $$$PARAMS for parameter lists, etc.
|
|
164
165
|
- path: (optional, default: '.') Path to search in.
|
|
165
166
|
- language: (optional, default: 'rust') Programming language to use for parsing.
|
|
166
|
-
- allow_tests: (optional, default:
|
|
167
|
+
- allow_tests: (optional, default: true) Allow test files in search results (true/false).
|
|
167
168
|
Usage Example:
|
|
168
169
|
|
|
169
170
|
<examples>
|
|
@@ -189,6 +190,7 @@ Full file extraction should be the LAST RESORT! Always prefer search.
|
|
|
189
190
|
Parameters:
|
|
190
191
|
- targets: (required) File paths or symbols to extract from. Formats: "file.js" (whole file), "file.js:42" (code block at line 42), "file.js:10-20" (lines 10-20), "file.js#funcName" (specific symbol). Multiple targets separated by spaces.
|
|
191
192
|
- input_content: (optional) Text content to extract file paths from (alternative to targets for processing diffs/logs).
|
|
193
|
+
- allow_tests: (optional, default: true) Include test files in extraction results.
|
|
192
194
|
|
|
193
195
|
Usage Example:
|
|
194
196
|
|
|
@@ -382,7 +384,22 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
|
|
|
382
384
|
continue; // Tool not found, try next tool
|
|
383
385
|
}
|
|
384
386
|
|
|
385
|
-
|
|
387
|
+
// For attempt_completion, use lastIndexOf to find the LAST occurrence of closing tag
|
|
388
|
+
// This prevents issues where the content contains the closing tag string (e.g., in regex patterns)
|
|
389
|
+
// For other tools, use indexOf from the opening tag position
|
|
390
|
+
let closeIndex;
|
|
391
|
+
if (toolName === 'attempt_completion') {
|
|
392
|
+
// Find the last occurrence of the closing tag in the entire string
|
|
393
|
+
// This assumes attempt_completion doesn't have nested tags of the same name
|
|
394
|
+
closeIndex = xmlString.lastIndexOf(closeTag);
|
|
395
|
+
// Make sure the closing tag is after the opening tag
|
|
396
|
+
if (closeIndex !== -1 && closeIndex <= openIndex + openTag.length) {
|
|
397
|
+
closeIndex = -1; // Invalid, treat as no closing tag
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
closeIndex = xmlString.indexOf(closeTag, openIndex + openTag.length);
|
|
401
|
+
}
|
|
402
|
+
|
|
386
403
|
let hasClosingTag = closeIndex !== -1;
|
|
387
404
|
|
|
388
405
|
// If no closing tag found, use content until end of string
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit and Create tools for file modification
|
|
3
|
+
* @module tools/edit
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { tool } from 'ai';
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
import { dirname, resolve, isAbsolute, sep } from 'path';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validates that a path is within allowed directories
|
|
13
|
+
* @param {string} filePath - Path to validate
|
|
14
|
+
* @param {string[]} allowedFolders - List of allowed folders
|
|
15
|
+
* @returns {boolean} True if path is allowed
|
|
16
|
+
*/
|
|
17
|
+
function isPathAllowed(filePath, allowedFolders) {
|
|
18
|
+
if (!allowedFolders || allowedFolders.length === 0) {
|
|
19
|
+
// If no restrictions, allow current directory and below
|
|
20
|
+
const resolvedPath = resolve(filePath);
|
|
21
|
+
const cwd = resolve(process.cwd());
|
|
22
|
+
// Ensure proper path separator to prevent path traversal
|
|
23
|
+
return resolvedPath === cwd || resolvedPath.startsWith(cwd + sep);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const resolvedPath = resolve(filePath);
|
|
27
|
+
return allowedFolders.some(folder => {
|
|
28
|
+
const allowedPath = resolve(folder);
|
|
29
|
+
// Ensure proper path separator to prevent path traversal
|
|
30
|
+
return resolvedPath === allowedPath || resolvedPath.startsWith(allowedPath + sep);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Common configuration for file tools
|
|
36
|
+
* @param {Object} options - Configuration options
|
|
37
|
+
* @returns {Object} Parsed configuration
|
|
38
|
+
*/
|
|
39
|
+
function parseFileToolOptions(options = {}) {
|
|
40
|
+
return {
|
|
41
|
+
debug: options.debug || false,
|
|
42
|
+
allowedFolders: options.allowedFolders || [],
|
|
43
|
+
defaultPath: options.defaultPath
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Edit tool generator - Claude Code style string replacement
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} [options] - Configuration options
|
|
51
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
52
|
+
* @param {string[]} [options.allowedFolders] - Allowed directories for file operations
|
|
53
|
+
* @param {string} [options.defaultPath] - Default working directory
|
|
54
|
+
* @returns {Object} Configured edit tool
|
|
55
|
+
*/
|
|
56
|
+
export const editTool = (options = {}) => {
|
|
57
|
+
const { debug, allowedFolders, defaultPath } = parseFileToolOptions(options);
|
|
58
|
+
|
|
59
|
+
return tool({
|
|
60
|
+
name: 'edit',
|
|
61
|
+
description: `Edit files using exact string replacement (Claude Code style).
|
|
62
|
+
|
|
63
|
+
This tool performs exact string replacements in files. It requires the old_string to match exactly what's in the file, including all whitespace and indentation.
|
|
64
|
+
|
|
65
|
+
Parameters:
|
|
66
|
+
- file_path: Path to the file to edit (absolute or relative)
|
|
67
|
+
- old_string: Exact text to find and replace (must be unique in the file unless replace_all is true)
|
|
68
|
+
- new_string: Text to replace with
|
|
69
|
+
- replace_all: (optional) Replace all occurrences instead of requiring uniqueness
|
|
70
|
+
|
|
71
|
+
Important:
|
|
72
|
+
- The old_string must match EXACTLY including whitespace
|
|
73
|
+
- If old_string appears multiple times and replace_all is false, the edit will fail
|
|
74
|
+
- Use larger context around the string to ensure uniqueness when needed`,
|
|
75
|
+
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
file_path: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'Path to the file to edit'
|
|
82
|
+
},
|
|
83
|
+
old_string: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'Exact text to find and replace'
|
|
86
|
+
},
|
|
87
|
+
new_string: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'Text to replace with'
|
|
90
|
+
},
|
|
91
|
+
replace_all: {
|
|
92
|
+
type: 'boolean',
|
|
93
|
+
description: 'Replace all occurrences (default: false)',
|
|
94
|
+
default: false
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
required: ['file_path', 'old_string', 'new_string']
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
execute: async ({ file_path, old_string, new_string, replace_all = false }) => {
|
|
101
|
+
try {
|
|
102
|
+
// Validate input parameters
|
|
103
|
+
if (!file_path || typeof file_path !== 'string' || file_path.trim() === '') {
|
|
104
|
+
return `Error editing file: Invalid file_path - must be a non-empty string`;
|
|
105
|
+
}
|
|
106
|
+
if (old_string === undefined || old_string === null || typeof old_string !== 'string') {
|
|
107
|
+
return `Error editing file: Invalid old_string - must be a string`;
|
|
108
|
+
}
|
|
109
|
+
if (new_string === undefined || new_string === null || typeof new_string !== 'string') {
|
|
110
|
+
return `Error editing file: Invalid new_string - must be a string`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Resolve the file path
|
|
114
|
+
const resolvedPath = isAbsolute(file_path) ? file_path : resolve(defaultPath || process.cwd(), file_path);
|
|
115
|
+
|
|
116
|
+
if (debug) {
|
|
117
|
+
console.error(`[Edit] Attempting to edit file: ${resolvedPath}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check if path is allowed
|
|
121
|
+
if (!isPathAllowed(resolvedPath, allowedFolders)) {
|
|
122
|
+
return `Error editing file: Permission denied - ${file_path} is outside allowed directories`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check if file exists
|
|
126
|
+
if (!existsSync(resolvedPath)) {
|
|
127
|
+
return `Error editing file: File not found - ${file_path}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Read the file
|
|
131
|
+
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
132
|
+
|
|
133
|
+
// Check if old_string exists in the file
|
|
134
|
+
if (!content.includes(old_string)) {
|
|
135
|
+
return `Error editing file: String not found - the specified old_string was not found in ${file_path}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Count occurrences
|
|
139
|
+
const occurrences = content.split(old_string).length - 1;
|
|
140
|
+
|
|
141
|
+
// Check uniqueness if not replacing all
|
|
142
|
+
if (!replace_all && occurrences > 1) {
|
|
143
|
+
return `Error editing file: Multiple occurrences found - the old_string appears ${occurrences} times. Use replace_all: true to replace all occurrences, or provide more context to make the string unique.`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Perform the replacement
|
|
147
|
+
let newContent;
|
|
148
|
+
if (replace_all) {
|
|
149
|
+
newContent = content.replaceAll(old_string, new_string);
|
|
150
|
+
} else {
|
|
151
|
+
newContent = content.replace(old_string, new_string);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check if replacement was made
|
|
155
|
+
if (newContent === content) {
|
|
156
|
+
return `Error editing file: No changes made - old_string and new_string might be the same`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Write the file back
|
|
160
|
+
await fs.writeFile(resolvedPath, newContent, 'utf-8');
|
|
161
|
+
|
|
162
|
+
const replacedCount = replace_all ? occurrences : 1;
|
|
163
|
+
|
|
164
|
+
if (debug) {
|
|
165
|
+
console.error(`[Edit] Successfully edited ${resolvedPath}, replaced ${replacedCount} occurrence(s)`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Return success message as a string (matching other tools pattern)
|
|
169
|
+
return `Successfully edited ${file_path} (${replacedCount} replacement${replacedCount !== 1 ? 's' : ''})`;
|
|
170
|
+
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error('[Edit] Error:', error);
|
|
173
|
+
return `Error editing file: ${error.message}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create tool generator - Create new files
|
|
181
|
+
*
|
|
182
|
+
* @param {Object} [options] - Configuration options
|
|
183
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
184
|
+
* @param {string[]} [options.allowedFolders] - Allowed directories for file operations
|
|
185
|
+
* @param {string} [options.defaultPath] - Default working directory
|
|
186
|
+
* @returns {Object} Configured create tool
|
|
187
|
+
*/
|
|
188
|
+
export const createTool = (options = {}) => {
|
|
189
|
+
const { debug, allowedFolders, defaultPath } = parseFileToolOptions(options);
|
|
190
|
+
|
|
191
|
+
return tool({
|
|
192
|
+
name: 'create',
|
|
193
|
+
description: `Create new files with specified content.
|
|
194
|
+
|
|
195
|
+
This tool creates new files in the filesystem. It will create parent directories if they don't exist.
|
|
196
|
+
|
|
197
|
+
Parameters:
|
|
198
|
+
- file_path: Path where the file should be created (absolute or relative)
|
|
199
|
+
- content: Content to write to the file
|
|
200
|
+
- overwrite: (optional) Whether to overwrite if file exists (default: false)
|
|
201
|
+
|
|
202
|
+
Important:
|
|
203
|
+
- By default, will fail if the file already exists
|
|
204
|
+
- Set overwrite: true to replace existing files
|
|
205
|
+
- Parent directories will be created automatically if needed`,
|
|
206
|
+
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
file_path: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
description: 'Path where the file should be created'
|
|
213
|
+
},
|
|
214
|
+
content: {
|
|
215
|
+
type: 'string',
|
|
216
|
+
description: 'Content to write to the file'
|
|
217
|
+
},
|
|
218
|
+
overwrite: {
|
|
219
|
+
type: 'boolean',
|
|
220
|
+
description: 'Overwrite if file exists (default: false)',
|
|
221
|
+
default: false
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
required: ['file_path', 'content']
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
execute: async ({ file_path, content, overwrite = false }) => {
|
|
228
|
+
try {
|
|
229
|
+
// Validate input parameters
|
|
230
|
+
if (!file_path || typeof file_path !== 'string' || file_path.trim() === '') {
|
|
231
|
+
return `Error creating file: Invalid file_path - must be a non-empty string`;
|
|
232
|
+
}
|
|
233
|
+
if (content === undefined || content === null || typeof content !== 'string') {
|
|
234
|
+
return `Error creating file: Invalid content - must be a string`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Resolve the file path
|
|
238
|
+
const resolvedPath = isAbsolute(file_path) ? file_path : resolve(defaultPath || process.cwd(), file_path);
|
|
239
|
+
|
|
240
|
+
if (debug) {
|
|
241
|
+
console.error(`[Create] Attempting to create file: ${resolvedPath}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check if path is allowed
|
|
245
|
+
if (!isPathAllowed(resolvedPath, allowedFolders)) {
|
|
246
|
+
return `Error creating file: Permission denied - ${file_path} is outside allowed directories`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check if file exists
|
|
250
|
+
if (existsSync(resolvedPath) && !overwrite) {
|
|
251
|
+
return `Error creating file: File already exists - ${file_path}. Use overwrite: true to replace it.`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Ensure parent directory exists
|
|
255
|
+
const dir = dirname(resolvedPath);
|
|
256
|
+
await fs.mkdir(dir, { recursive: true });
|
|
257
|
+
|
|
258
|
+
// Write the file
|
|
259
|
+
await fs.writeFile(resolvedPath, content, 'utf-8');
|
|
260
|
+
|
|
261
|
+
const action = existsSync(resolvedPath) && overwrite ? 'overwrote' : 'created';
|
|
262
|
+
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
263
|
+
|
|
264
|
+
if (debug) {
|
|
265
|
+
console.error(`[Create] Successfully ${action} ${resolvedPath}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Return success message as a string (matching other tools pattern)
|
|
269
|
+
return `Successfully ${action} ${file_path} (${bytes} bytes)`;
|
|
270
|
+
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error('[Create] Error:', error);
|
|
273
|
+
return `Error creating file: ${error.message}`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Export schemas for tool definitions
|
|
280
|
+
export const editSchema = {
|
|
281
|
+
type: 'object',
|
|
282
|
+
properties: {
|
|
283
|
+
file_path: {
|
|
284
|
+
type: 'string',
|
|
285
|
+
description: 'Path to the file to edit'
|
|
286
|
+
},
|
|
287
|
+
old_string: {
|
|
288
|
+
type: 'string',
|
|
289
|
+
description: 'Exact text to find and replace'
|
|
290
|
+
},
|
|
291
|
+
new_string: {
|
|
292
|
+
type: 'string',
|
|
293
|
+
description: 'Text to replace with'
|
|
294
|
+
},
|
|
295
|
+
replace_all: {
|
|
296
|
+
type: 'boolean',
|
|
297
|
+
description: 'Replace all occurrences (default: false)'
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
required: ['file_path', 'old_string', 'new_string']
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export const createSchema = {
|
|
304
|
+
type: 'object',
|
|
305
|
+
properties: {
|
|
306
|
+
file_path: {
|
|
307
|
+
type: 'string',
|
|
308
|
+
description: 'Path where the file should be created'
|
|
309
|
+
},
|
|
310
|
+
content: {
|
|
311
|
+
type: 'string',
|
|
312
|
+
description: 'Content to write to the file'
|
|
313
|
+
},
|
|
314
|
+
overwrite: {
|
|
315
|
+
type: 'boolean',
|
|
316
|
+
description: 'Overwrite if file exists (default: false)'
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
required: ['file_path', 'content']
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Tool descriptions for XML definitions
|
|
323
|
+
export const editDescription = 'Edit files using exact string replacement. Requires exact match including whitespace.';
|
|
324
|
+
export const createDescription = 'Create new files with specified content. Will create parent directories if needed.';
|
|
325
|
+
|
|
326
|
+
// XML tool definitions
|
|
327
|
+
export const editToolDefinition = `
|
|
328
|
+
## edit
|
|
329
|
+
Description: ${editDescription}
|
|
330
|
+
|
|
331
|
+
When to use:
|
|
332
|
+
- For precise, surgical edits to existing files
|
|
333
|
+
- When you need to change specific lines or blocks of code
|
|
334
|
+
- For renaming functions, variables, or updating configuration values
|
|
335
|
+
- When the exact text to replace is known and unique (or use replace_all for multiple occurrences)
|
|
336
|
+
|
|
337
|
+
When NOT to use:
|
|
338
|
+
- For creating new files (use 'create' tool instead)
|
|
339
|
+
- When you cannot determine the exact text to replace
|
|
340
|
+
- When changes span multiple locations that would be better handled together
|
|
341
|
+
|
|
342
|
+
Parameters:
|
|
343
|
+
- file_path: (required) Path to the file to edit
|
|
344
|
+
- old_string: (required) Exact text to find and replace (must match including whitespace, newlines, and indentation)
|
|
345
|
+
- new_string: (required) Text to replace with
|
|
346
|
+
- replace_all: (optional, default: false) Replace all occurrences if the string appears multiple times
|
|
347
|
+
|
|
348
|
+
Important notes:
|
|
349
|
+
- The old_string MUST match EXACTLY, including all whitespace, indentation, and line breaks
|
|
350
|
+
- If old_string appears multiple times and replace_all is false, the tool will fail
|
|
351
|
+
- Always verify the exact formatting of the text you want to replace
|
|
352
|
+
|
|
353
|
+
Examples:
|
|
354
|
+
<edit>
|
|
355
|
+
<file_path>src/main.js</file_path>
|
|
356
|
+
<old_string>function oldName() {
|
|
357
|
+
return 42;
|
|
358
|
+
}</old_string>
|
|
359
|
+
<new_string>function newName() {
|
|
360
|
+
return 42;
|
|
361
|
+
}</new_string>
|
|
362
|
+
</edit>
|
|
363
|
+
|
|
364
|
+
<edit>
|
|
365
|
+
<file_path>config.json</file_path>
|
|
366
|
+
<old_string>"debug": false</old_string>
|
|
367
|
+
<new_string>"debug": true</new_string>
|
|
368
|
+
<replace_all>true</replace_all>
|
|
369
|
+
</edit>`;
|
|
370
|
+
|
|
371
|
+
export const createToolDefinition = `
|
|
372
|
+
## create
|
|
373
|
+
Description: ${createDescription}
|
|
374
|
+
|
|
375
|
+
When to use:
|
|
376
|
+
- For creating brand new files from scratch
|
|
377
|
+
- When you need to add configuration files, documentation, or new modules
|
|
378
|
+
- For generating boilerplate code or templates
|
|
379
|
+
- When you have the complete content ready to write
|
|
380
|
+
|
|
381
|
+
When NOT to use:
|
|
382
|
+
- For editing existing files (use 'edit' tool instead)
|
|
383
|
+
- When a file already exists unless you explicitly want to overwrite it
|
|
384
|
+
|
|
385
|
+
Parameters:
|
|
386
|
+
- file_path: (required) Path where the file should be created
|
|
387
|
+
- content: (required) Complete content to write to the file
|
|
388
|
+
- overwrite: (optional, default: false) Whether to overwrite if file already exists
|
|
389
|
+
|
|
390
|
+
Important notes:
|
|
391
|
+
- Parent directories will be created automatically if they don't exist
|
|
392
|
+
- The tool will fail if the file already exists and overwrite is false
|
|
393
|
+
- Be careful with the overwrite option as it completely replaces existing files
|
|
394
|
+
|
|
395
|
+
Examples:
|
|
396
|
+
<create>
|
|
397
|
+
<file_path>src/newFile.js</file_path>
|
|
398
|
+
<content>export function hello() {
|
|
399
|
+
return "Hello, world!";
|
|
400
|
+
}</content>
|
|
401
|
+
</create>
|
|
402
|
+
|
|
403
|
+
<create>
|
|
404
|
+
<file_path>README.md</file_path>
|
|
405
|
+
<content># My Project
|
|
406
|
+
|
|
407
|
+
This is a new project.</content>
|
|
408
|
+
<overwrite>true</overwrite>
|
|
409
|
+
</create>`;
|
package/build/tools/index.js
CHANGED
|
@@ -6,6 +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
10
|
|
|
10
11
|
// Export LangChain tools
|
|
11
12
|
export { createSearchTool, createQueryTool, createExtractTool } from './langchain.js';
|
|
@@ -25,6 +26,16 @@ export {
|
|
|
25
26
|
attemptCompletionToolDefinition
|
|
26
27
|
} from './common.js';
|
|
27
28
|
|
|
29
|
+
// Export edit and create schemas
|
|
30
|
+
export {
|
|
31
|
+
editSchema,
|
|
32
|
+
createSchema,
|
|
33
|
+
editDescription,
|
|
34
|
+
createDescription,
|
|
35
|
+
editToolDefinition,
|
|
36
|
+
createToolDefinition
|
|
37
|
+
} from './edit.js';
|
|
38
|
+
|
|
28
39
|
// Export system message
|
|
29
40
|
export { DEFAULT_SYSTEM_MESSAGE } from './system-message.js';
|
|
30
41
|
|