@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.
Files changed (43) hide show
  1. package/README.md +169 -1
  2. package/build/agent/FallbackManager.d.ts +176 -0
  3. package/build/agent/FallbackManager.js +545 -0
  4. package/build/agent/ProbeAgent.d.ts +7 -1
  5. package/build/agent/ProbeAgent.js +545 -89
  6. package/build/agent/RetryManager.d.ts +157 -0
  7. package/build/agent/RetryManager.js +334 -0
  8. package/build/agent/acp/tools.js +6 -2
  9. package/build/agent/contextCompactor.js +271 -0
  10. package/build/agent/index.js +2165 -250
  11. package/build/agent/probeTool.js +20 -2
  12. package/build/agent/schemaUtils.js +7 -0
  13. package/build/agent/tools.js +16 -0
  14. package/build/agent/xmlParsingUtils.js +24 -4
  15. package/build/index.js +13 -0
  16. package/build/tools/common.js +21 -4
  17. package/build/tools/edit.js +409 -0
  18. package/build/tools/index.js +11 -0
  19. package/cjs/agent/ProbeAgent.cjs +2620 -606
  20. package/cjs/index.cjs +2582 -563
  21. package/package.json +2 -2
  22. package/src/agent/FallbackManager.d.ts +176 -0
  23. package/src/agent/FallbackManager.js +545 -0
  24. package/src/agent/ProbeAgent.d.ts +7 -1
  25. package/src/agent/ProbeAgent.js +545 -89
  26. package/src/agent/RetryManager.d.ts +157 -0
  27. package/src/agent/RetryManager.js +334 -0
  28. package/src/agent/acp/tools.js +6 -2
  29. package/src/agent/contextCompactor.js +271 -0
  30. package/src/agent/index.js +30 -1
  31. package/src/agent/probeTool.js +20 -2
  32. package/src/agent/schemaUtils.js +7 -0
  33. package/src/agent/tools.js +16 -0
  34. package/src/agent/xmlParsingUtils.js +24 -4
  35. package/src/index.js +13 -0
  36. package/src/tools/common.js +21 -4
  37. package/src/tools/edit.js +409 -0
  38. package/src/tools/index.js +11 -0
  39. package/bin/binaries/probe-v0.6.0-rc159-aarch64-apple-darwin.tar.gz +0 -0
  40. package/bin/binaries/probe-v0.6.0-rc159-aarch64-unknown-linux-musl.tar.gz +0 -0
  41. package/bin/binaries/probe-v0.6.0-rc159-x86_64-apple-darwin.tar.gz +0 -0
  42. package/bin/binaries/probe-v0.6.0-rc159-x86_64-pc-windows-msvc.zip +0 -0
  43. package/bin/binaries/probe-v0.6.0-rc159-x86_64-unknown-linux-musl.tar.gz +0 -0
@@ -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
  */
@@ -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
- const attemptCompletionMatch = cleanedXmlString.match(/<attempt_completion>([\s\S]*?)(?:<\/attempt_completion>|$)/);
63
- if (attemptCompletionMatch) {
64
- const content = attemptCompletionMatch[1].trim();
65
- const hasClosingTag = cleanedXmlString.includes('</attempt_completion>');
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
  };
@@ -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(false).describe('Allow test files in search results')
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: false) Allow test files in search results (true/false).
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
- let closeIndex = xmlString.indexOf(closeTag, openIndex + openTag.length);
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>`;
@@ -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