@probelabs/probe-chat 0.6.0-rc100

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/probeTool.js ADDED
@@ -0,0 +1,714 @@
1
+ // Import tool generators from @probelabs/probe package
2
+ import { searchTool, queryTool, extractTool, DEFAULT_SYSTEM_MESSAGE, listFilesByLevel } from '@probelabs/probe';
3
+ import { exec, spawn } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import { randomUUID } from 'crypto';
6
+ import { EventEmitter } from 'events';
7
+ import fs from 'fs';
8
+ import { promises as fsPromises } from 'fs';
9
+ import path from 'path';
10
+ import os from 'os';
11
+ import { glob } from 'glob';
12
+
13
+ // Import the new pluggable implementation tool
14
+ import { createImplementTool } from './implement/core/ImplementTool.js';
15
+
16
+ // Create an event emitter for tool calls
17
+ export const toolCallEmitter = new EventEmitter();
18
+
19
+ // Map to track active tool executions by session ID
20
+ const activeToolExecutions = new Map();
21
+
22
+ // Function to check if a session has been cancelled
23
+ export function isSessionCancelled(sessionId) {
24
+ return activeToolExecutions.get(sessionId)?.cancelled || false;
25
+ }
26
+
27
+ // Function to cancel all tool executions for a session
28
+ export function cancelToolExecutions(sessionId) {
29
+ // Only log if not in non-interactive mode or if in debug mode
30
+ if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
31
+ console.log(`Cancelling tool executions for session: ${sessionId}`);
32
+ }
33
+ const sessionData = activeToolExecutions.get(sessionId);
34
+ if (sessionData) {
35
+ sessionData.cancelled = true;
36
+ // Only log if not in non-interactive mode or if in debug mode
37
+ if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
38
+ console.log(`Session ${sessionId} marked as cancelled`);
39
+ }
40
+ return true;
41
+ }
42
+ return false;
43
+ }
44
+
45
+ // Function to register a new tool execution
46
+ function registerToolExecution(sessionId) {
47
+ if (!sessionId) return;
48
+
49
+ if (!activeToolExecutions.has(sessionId)) {
50
+ activeToolExecutions.set(sessionId, { cancelled: false });
51
+ } else {
52
+ // Reset cancelled flag if session already exists for a new execution
53
+ activeToolExecutions.get(sessionId).cancelled = false;
54
+ }
55
+ }
56
+
57
+ // Function to clear tool execution data for a session
58
+ export function clearToolExecutionData(sessionId) {
59
+ if (!sessionId) return;
60
+
61
+ if (activeToolExecutions.has(sessionId)) {
62
+ activeToolExecutions.delete(sessionId);
63
+ // Only log if not in non-interactive mode or if in debug mode
64
+ if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
65
+ console.log(`Cleared tool execution data for session: ${sessionId}`);
66
+ }
67
+ }
68
+ }
69
+
70
+ // Generate a default session ID (less relevant now, session is managed per-chat)
71
+ const defaultSessionId = randomUUID();
72
+ // Only log session ID in debug mode
73
+ if (process.env.DEBUG_CHAT === '1') {
74
+ console.log(`Generated default session ID (probeTool.js): ${defaultSessionId}`);
75
+ }
76
+
77
+ // Create configured tools with the session ID
78
+ // Note: These configOptions are less critical now as sessionId is passed explicitly
79
+ const configOptions = {
80
+ sessionId: defaultSessionId,
81
+ debug: process.env.DEBUG_CHAT === '1'
82
+ };
83
+
84
+ // Create the base tools using the imported generators
85
+ const baseSearchTool = searchTool(configOptions);
86
+ const baseQueryTool = queryTool(configOptions);
87
+ const baseExtractTool = extractTool(configOptions);
88
+
89
+
90
+ // Wrap the tools to emit events and handle cancellation
91
+ const wrapToolWithEmitter = (tool, toolName, baseExecute) => {
92
+ return {
93
+ ...tool, // Spread schema, description etc.
94
+ execute: async (params) => { // The execute function now receives parsed params
95
+ const debug = process.env.DEBUG_CHAT === '1';
96
+ // Get the session ID from params (passed down from probeChat.js)
97
+ const toolSessionId = params.sessionId || defaultSessionId; // Fallback, but should always have sessionId
98
+
99
+ if (debug) {
100
+ console.log(`[DEBUG] probeTool: Executing ${toolName} for session ${toolSessionId}`);
101
+ console.log(`[DEBUG] probeTool: Received params:`, params);
102
+ }
103
+
104
+ // Register this tool execution (and reset cancel flag if needed)
105
+ registerToolExecution(toolSessionId);
106
+
107
+ // Check if this session has been cancelled *before* execution
108
+ if (isSessionCancelled(toolSessionId)) {
109
+ // Only log if not in non-interactive mode or if in debug mode
110
+ console.error(`Tool execution cancelled BEFORE starting for session ${toolSessionId}`);
111
+ throw new Error(`Tool execution cancelled for session ${toolSessionId}`);
112
+ }
113
+ // Only log if not in non-interactive mode or if in debug mode
114
+ console.error(`Executing ${toolName} for session ${toolSessionId}`); // Simplified log
115
+
116
+ // Remove sessionId from params before passing to base tool if it expects only schema params
117
+ const { sessionId, ...toolParams } = params;
118
+
119
+ try {
120
+ // Emit a tool call start event
121
+ const toolCallStartData = {
122
+ timestamp: new Date().toISOString(),
123
+ name: toolName,
124
+ args: toolParams, // Log schema params
125
+ status: 'started'
126
+ };
127
+ if (debug) {
128
+ console.log(`[DEBUG] probeTool: Emitting toolCallStart:${toolSessionId}`);
129
+ }
130
+ toolCallEmitter.emit(`toolCall:${toolSessionId}`, toolCallStartData);
131
+
132
+ // Execute the original tool's execute function with schema params
133
+ // Use a promise-based approach with cancellation check
134
+ let result = null;
135
+ let executionError = null;
136
+
137
+ const executionPromise = baseExecute(toolParams).catch(err => {
138
+ executionError = err; // Capture error
139
+ });
140
+
141
+ const checkInterval = 50; // Check every 50ms
142
+ while (result === null && executionError === null) {
143
+ if (isSessionCancelled(toolSessionId)) {
144
+ console.error(`Tool execution cancelled DURING execution for session ${toolSessionId}`);
145
+ // Attempt to signal cancellation if the underlying tool supports it (future enhancement)
146
+ // For now, just throw the cancellation error
147
+ throw new Error(`Tool execution cancelled for session ${toolSessionId}`);
148
+ }
149
+ // Check if promise is resolved or rejected
150
+ const status = await Promise.race([
151
+ executionPromise.then(() => 'resolved').catch(() => 'rejected'),
152
+ new Promise(resolve => setTimeout(() => resolve('pending'), checkInterval))
153
+ ]);
154
+
155
+ if (status === 'resolved') {
156
+ result = await executionPromise; // Get the result
157
+ } else if (status === 'rejected') {
158
+ // Error already captured by the catch block on executionPromise
159
+ break;
160
+ }
161
+ // If 'pending', continue loop
162
+ }
163
+
164
+ // If loop exited due to error
165
+ if (executionError) {
166
+ throw executionError;
167
+ }
168
+
169
+ // If loop exited due to cancellation within the loop
170
+ if (isSessionCancelled(toolSessionId)) {
171
+ // Only log if not in non-interactive mode or if in debug mode
172
+ if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
173
+ console.log(`Tool execution finished but session was cancelled for ${toolSessionId}`);
174
+ }
175
+ throw new Error(`Tool execution cancelled for session ${toolSessionId}`);
176
+ }
177
+
178
+
179
+ // Emit the tool call completion event
180
+ const toolCallData = {
181
+ timestamp: new Date().toISOString(),
182
+ name: toolName,
183
+ args: toolParams,
184
+ // Safely preview result
185
+ resultPreview: typeof result === 'string'
186
+ ? (result.length > 200 ? result.substring(0, 200) + '...' : result)
187
+ : (result ? JSON.stringify(result).substring(0, 200) + '...' : 'No Result'),
188
+ status: 'completed'
189
+ };
190
+ if (debug) {
191
+ console.log(`[DEBUG] probeTool: Emitting toolCall:${toolSessionId} (completed)`);
192
+ }
193
+ toolCallEmitter.emit(`toolCall:${toolSessionId}`, toolCallData);
194
+
195
+ return result;
196
+ } catch (error) {
197
+ // If it's a cancellation error, re-throw it directly
198
+ if (error.message.includes('cancelled for session')) {
199
+ // Only log if not in non-interactive mode or if in debug mode
200
+ if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
201
+ console.log(`Caught cancellation error for ${toolName} in session ${toolSessionId}`);
202
+ }
203
+ // Emit cancellation event? Or let the caller handle it? Let caller handle.
204
+ throw error;
205
+ }
206
+
207
+ // Handle other execution errors
208
+ if (debug) {
209
+ console.error(`[DEBUG] probeTool: Error executing ${toolName}:`, error);
210
+ }
211
+
212
+ // Emit a tool call error event
213
+ const toolCallErrorData = {
214
+ timestamp: new Date().toISOString(),
215
+ name: toolName,
216
+ args: toolParams,
217
+ error: error.message || 'Unknown error',
218
+ status: 'error'
219
+ };
220
+ if (debug) {
221
+ console.log(`[DEBUG] probeTool: Emitting toolCall:${toolSessionId} (error)`);
222
+ }
223
+ toolCallEmitter.emit(`toolCall:${toolSessionId}`, toolCallErrorData);
224
+
225
+ throw error; // Re-throw the error to be caught by probeChat.js loop
226
+ }
227
+ }
228
+ };
229
+ };
230
+
231
+ // Create the implement tool using the new pluggable system
232
+ const implementToolConfig = {
233
+ enabled: process.env.ALLOW_EDIT === '1' || process.argv.includes('--allow-edit'),
234
+ backendConfig: {
235
+ // Configuration can be extended here
236
+ }
237
+ };
238
+
239
+ const pluggableImplementTool = createImplementTool(implementToolConfig);
240
+
241
+ // Create a compatibility wrapper for the old interface
242
+ const baseImplementTool = {
243
+ name: "implement",
244
+ description: pluggableImplementTool.description,
245
+ inputSchema: pluggableImplementTool.inputSchema,
246
+ execute: async ({ task, autoCommits = false, prompt, sessionId }) => {
247
+ const debug = process.env.DEBUG_CHAT === '1';
248
+
249
+ if (debug) {
250
+ console.log(`[DEBUG] Executing implementation with task: ${task}`);
251
+ console.log(`[DEBUG] Auto-commits: ${autoCommits}`);
252
+ console.log(`[DEBUG] Session ID: ${sessionId}`);
253
+ if (prompt) console.log(`[DEBUG] Custom prompt: ${prompt}`);
254
+ }
255
+
256
+ // Check if the tool is enabled
257
+ if (!implementToolConfig.enabled) {
258
+ return {
259
+ success: false,
260
+ output: null,
261
+ error: 'Implementation tool is not enabled. Use --allow-edit flag to enable.',
262
+ command: null,
263
+ timestamp: new Date().toISOString(),
264
+ prompt: prompt || task
265
+ };
266
+ }
267
+
268
+ try {
269
+ // Use the new pluggable implementation tool
270
+ const result = await pluggableImplementTool.execute({
271
+ task: prompt || task, // Use prompt if provided, otherwise use task
272
+ autoCommit: autoCommits,
273
+ sessionId: sessionId,
274
+ // Pass through any additional options that might be useful
275
+ context: {
276
+ workingDirectory: process.cwd()
277
+ }
278
+ });
279
+
280
+ // The result is already in the expected format
281
+ return result;
282
+
283
+ } catch (error) {
284
+ // Handle any unexpected errors
285
+ console.error(`Error in implement tool:`, error);
286
+ return {
287
+ success: false,
288
+ output: null,
289
+ error: error.message || 'Unknown error in implementation tool',
290
+ command: null,
291
+ timestamp: new Date().toISOString(),
292
+ prompt: prompt || task
293
+ };
294
+ }
295
+ }
296
+ };
297
+
298
+ // Create the listFiles tool
299
+ const baseListFilesTool = {
300
+ name: "listFiles",
301
+ description: 'List files in a specified directory',
302
+ inputSchema: {
303
+ type: 'object',
304
+ properties: {
305
+ directory: {
306
+ type: 'string',
307
+ description: 'The directory path to list files from. Defaults to current directory if not specified.'
308
+ }
309
+ },
310
+ required: []
311
+ },
312
+ execute: async ({ directory = '.', sessionId }) => {
313
+ const debug = process.env.DEBUG_CHAT === '1';
314
+ const currentWorkingDir = process.cwd();
315
+
316
+ // Get allowed folders from environment variable
317
+ const allowedFoldersEnv = process.env.ALLOWED_FOLDERS;
318
+ let allowedFolders = [];
319
+
320
+ if (allowedFoldersEnv) {
321
+ allowedFolders = allowedFoldersEnv.split(',').map(folder => folder.trim()).filter(folder => folder.length > 0);
322
+ }
323
+
324
+ // Handle default directory behavior when ALLOWED_FOLDERS is set
325
+ let targetDirectory = directory;
326
+ if (allowedFolders.length > 0 && (directory === '.' || directory === './')) {
327
+ // Use the first allowed folder if directory is current directory
328
+ targetDirectory = allowedFolders[0];
329
+ if (debug) {
330
+ console.log(`[DEBUG] Redirecting from '${directory}' to first allowed folder: ${targetDirectory}`);
331
+ }
332
+ }
333
+
334
+ const targetDir = path.resolve(currentWorkingDir, targetDirectory);
335
+
336
+ // Validate that the target directory is within allowed folders
337
+ if (allowedFolders.length > 0) {
338
+ const isAllowed = allowedFolders.some(allowedFolder => {
339
+ const resolvedAllowedFolder = path.resolve(currentWorkingDir, allowedFolder);
340
+ return targetDir === resolvedAllowedFolder || targetDir.startsWith(resolvedAllowedFolder + path.sep);
341
+ });
342
+
343
+ if (!isAllowed) {
344
+ const error = `Access denied: Directory '${targetDirectory}' is not within allowed folders: ${allowedFolders.join(', ')}`;
345
+ if (debug) {
346
+ console.log(`[DEBUG] ${error}`);
347
+ }
348
+ return {
349
+ success: false,
350
+ directory: targetDir,
351
+ error: error,
352
+ timestamp: new Date().toISOString()
353
+ };
354
+ }
355
+ }
356
+
357
+ if (debug) {
358
+ console.log(`[DEBUG] Listing files in directory: ${targetDir}`);
359
+ }
360
+
361
+ try {
362
+ // Read the directory contents
363
+ const files = await fs.promises.readdir(targetDir, { withFileTypes: true });
364
+
365
+ // Format the results
366
+ const result = files.map(file => {
367
+ const isDirectory = file.isDirectory();
368
+ return {
369
+ name: file.name,
370
+ type: isDirectory ? 'directory' : 'file',
371
+ path: path.join(targetDirectory, file.name)
372
+ };
373
+ });
374
+
375
+ if (debug) {
376
+ console.log(`[DEBUG] Found ${result.length} files/directories in ${targetDir}`);
377
+ }
378
+
379
+ return {
380
+ success: true,
381
+ directory: targetDir,
382
+ files: result,
383
+ timestamp: new Date().toISOString()
384
+ };
385
+ } catch (error) {
386
+ console.error(`Error listing files in ${targetDir}:`, error);
387
+ return {
388
+ success: false,
389
+ directory: targetDir,
390
+ error: error.message || 'Unknown error listing files',
391
+ timestamp: new Date().toISOString()
392
+ };
393
+ }
394
+ }
395
+ };
396
+
397
+ // Create the searchFiles tool
398
+ const baseSearchFilesTool = {
399
+ name: "searchFiles",
400
+ description: 'Search for files using a glob pattern, recursively by default',
401
+ inputSchema: {
402
+ type: 'object',
403
+ properties: {
404
+ pattern: {
405
+ type: 'string',
406
+ description: 'The glob pattern to search for (e.g., "**/*.js", "*.md")'
407
+ },
408
+ directory: {
409
+ type: 'string',
410
+ description: 'The directory to search in. Defaults to current directory if not specified.'
411
+ },
412
+ recursive: {
413
+ type: 'boolean',
414
+ description: 'Whether to search recursively. Defaults to true.'
415
+ }
416
+ },
417
+ required: ['pattern']
418
+ },
419
+ execute: async ({ pattern, directory, recursive = true, sessionId }) => {
420
+ // Ensure directory defaults to current directory
421
+ directory = directory || '.';
422
+
423
+ const debug = process.env.DEBUG_CHAT === '1';
424
+ const currentWorkingDir = process.cwd();
425
+
426
+ // Get allowed folders from environment variable
427
+ const allowedFoldersEnv = process.env.ALLOWED_FOLDERS;
428
+ let allowedFolders = [];
429
+
430
+ if (allowedFoldersEnv) {
431
+ allowedFolders = allowedFoldersEnv.split(',').map(folder => folder.trim()).filter(folder => folder.length > 0);
432
+ }
433
+
434
+ // Handle default directory behavior when ALLOWED_FOLDERS is set
435
+ let targetDirectory = directory;
436
+ if (allowedFolders.length > 0 && (directory === '.' || directory === './')) {
437
+ // Use the first allowed folder if directory is current directory
438
+ targetDirectory = allowedFolders[0];
439
+ if (debug) {
440
+ console.log(`[DEBUG] Redirecting from '${directory}' to first allowed folder: ${targetDirectory}`);
441
+ }
442
+ }
443
+
444
+ const targetDir = path.resolve(currentWorkingDir, targetDirectory);
445
+
446
+ // Validate that the target directory is within allowed folders
447
+ if (allowedFolders.length > 0) {
448
+ const isAllowed = allowedFolders.some(allowedFolder => {
449
+ const resolvedAllowedFolder = path.resolve(currentWorkingDir, allowedFolder);
450
+ return targetDir === resolvedAllowedFolder || targetDir.startsWith(resolvedAllowedFolder + path.sep);
451
+ });
452
+
453
+ if (!isAllowed) {
454
+ const error = `Access denied: Directory '${targetDirectory}' is not within allowed folders: ${allowedFolders.join(', ')}`;
455
+ if (debug) {
456
+ console.log(`[DEBUG] ${error}`);
457
+ }
458
+ return {
459
+ success: false,
460
+ directory: targetDir,
461
+ pattern: pattern,
462
+ error: error,
463
+ timestamp: new Date().toISOString()
464
+ };
465
+ }
466
+ }
467
+
468
+ // Log execution parameters to stderr for visibility
469
+ console.error(`Executing searchFiles with params: pattern="${pattern}", directory="${targetDirectory}", recursive=${recursive}`);
470
+ console.error(`Resolved target directory: ${targetDir}`);
471
+ console.error(`Current working directory: ${currentWorkingDir}`);
472
+
473
+ if (debug) {
474
+ console.log(`[DEBUG] Searching for files with pattern: ${pattern}`);
475
+ console.log(`[DEBUG] In directory: ${targetDir}`);
476
+ console.log(`[DEBUG] Recursive: ${recursive}`);
477
+ }
478
+
479
+ // Validate pattern to prevent overly complex patterns
480
+ if (pattern.includes('**/**') || pattern.split('*').length > 10) {
481
+ console.error(`Pattern too complex: ${pattern}`);
482
+ return {
483
+ success: false,
484
+ directory: targetDir,
485
+ pattern: pattern,
486
+ error: 'Pattern too complex. Please use a simpler glob pattern.',
487
+ timestamp: new Date().toISOString()
488
+ };
489
+ }
490
+
491
+ try {
492
+ // Set glob options with timeout and limits
493
+ const options = {
494
+ cwd: targetDir,
495
+ dot: true, // Include dotfiles
496
+ nodir: true, // Only return files, not directories
497
+ absolute: false, // Return paths relative to the search directory
498
+ timeout: 10000, // 10 second timeout
499
+ maxDepth: recursive ? 10 : 1, // Limit recursion depth
500
+ };
501
+
502
+ // If not recursive, modify the pattern to only search the top level
503
+ const searchPattern = recursive ? pattern : pattern.replace(/^\*\*\//, '');
504
+
505
+ console.error(`Starting glob search with pattern: ${searchPattern} in ${targetDir}`);
506
+ console.error(`Glob options: ${JSON.stringify(options)}`);
507
+
508
+ // Use a safer approach with manual file searching if the pattern is simple enough
509
+ let files = [];
510
+
511
+ // For simple patterns like "*.js" or "bin/*.js", use a more direct approach
512
+ if (pattern.includes('*') && !pattern.includes('**') && pattern.split('/').length <= 2) {
513
+ console.error(`Using direct file search for simple pattern: ${pattern}`);
514
+
515
+ try {
516
+ // Handle patterns like "dir/*.ext" or "*.ext"
517
+ const parts = pattern.split('/');
518
+ let searchDir = targetDir;
519
+ let filePattern;
520
+
521
+ if (parts.length === 2) {
522
+ // Pattern like "dir/*.ext"
523
+ searchDir = path.join(targetDir, parts[0]);
524
+ filePattern = parts[1];
525
+ } else {
526
+ // Pattern like "*.ext"
527
+ filePattern = parts[0];
528
+ }
529
+
530
+ console.error(`Searching in directory: ${searchDir} for files matching: ${filePattern}`);
531
+
532
+ // Check if directory exists
533
+ try {
534
+ await fsPromises.access(searchDir);
535
+ } catch (err) {
536
+ console.error(`Directory does not exist: ${searchDir}`);
537
+ return {
538
+ success: true,
539
+ directory: targetDir,
540
+ pattern: pattern,
541
+ recursive: recursive,
542
+ files: [],
543
+ count: 0,
544
+ timestamp: new Date().toISOString()
545
+ };
546
+ }
547
+
548
+ // Read directory contents
549
+ const dirEntries = await fsPromises.readdir(searchDir, { withFileTypes: true });
550
+
551
+ // Convert glob pattern to regex
552
+ const regexPattern = filePattern
553
+ .replace(/\./g, '\\.')
554
+ .replace(/\*/g, '.*');
555
+ const regex = new RegExp(`^${regexPattern}$`);
556
+
557
+ // Filter files based on pattern
558
+ files = dirEntries
559
+ .filter(entry => entry.isFile() && regex.test(entry.name))
560
+ .map(entry => {
561
+ const relativePath = parts.length === 2
562
+ ? path.join(parts[0], entry.name)
563
+ : entry.name;
564
+ return relativePath;
565
+ });
566
+
567
+ console.error(`Direct search found ${files.length} files matching ${filePattern}`);
568
+ } catch (err) {
569
+ console.error(`Error in direct file search: ${err.message}`);
570
+ // Fall back to glob if direct search fails
571
+ console.error(`Falling back to glob search`);
572
+
573
+ // Create a promise that rejects after a timeout
574
+ const timeoutPromise = new Promise((_, reject) => {
575
+ setTimeout(() => reject(new Error('Search operation timed out after 10 seconds')), 10000);
576
+ });
577
+
578
+ // Use glob without promisify since it might already return a Promise
579
+ files = await Promise.race([
580
+ glob(searchPattern, options),
581
+ timeoutPromise
582
+ ]);
583
+ }
584
+ } else {
585
+ console.error(`Using glob for complex pattern: ${pattern}`);
586
+
587
+ // Create a promise that rejects after a timeout
588
+ const timeoutPromise = new Promise((_, reject) => {
589
+ setTimeout(() => reject(new Error('Search operation timed out after 10 seconds')), 10000);
590
+ });
591
+
592
+ // Use glob without promisify since it might already return a Promise
593
+ files = await Promise.race([
594
+ glob(searchPattern, options),
595
+ timeoutPromise
596
+ ]);
597
+ }
598
+
599
+ console.error(`Search completed, found ${files.length} files in ${targetDir}`);
600
+ console.error(`Pattern: ${pattern}, Recursive: ${recursive}`);
601
+
602
+ if (debug) {
603
+ console.log(`[DEBUG] Found ${files.length} files matching pattern ${pattern}`);
604
+ }
605
+
606
+ // Limit the number of results to prevent memory issues
607
+ const maxResults = 1000;
608
+ const limitedFiles = files.length > maxResults ? files.slice(0, maxResults) : files;
609
+
610
+ if (files.length > maxResults) {
611
+ console.warn(`Warning: Limited results to ${maxResults} files out of ${files.length} total matches`);
612
+ }
613
+
614
+ return {
615
+ success: true,
616
+ directory: targetDir,
617
+ pattern: pattern,
618
+ recursive: recursive,
619
+ files: limitedFiles.map(file => path.join(targetDirectory, file)),
620
+ count: limitedFiles.length,
621
+ totalMatches: files.length,
622
+ limited: files.length > maxResults,
623
+ timestamp: new Date().toISOString()
624
+ };
625
+ } catch (error) {
626
+ console.error(`Error searching files with pattern "${pattern}" in ${targetDir}:`, error);
627
+ console.error(`Search parameters: directory="${targetDirectory}", recursive=${recursive}, sessionId=${sessionId}`);
628
+ return {
629
+ success: false,
630
+ directory: targetDir,
631
+ pattern: pattern,
632
+ error: error.message || 'Unknown error searching files',
633
+ timestamp: new Date().toISOString()
634
+ };
635
+ }
636
+ }
637
+ };
638
+
639
+ // Export the wrapped tool instances
640
+ export const searchToolInstance = wrapToolWithEmitter(baseSearchTool, 'search', baseSearchTool.execute);
641
+ export const queryToolInstance = wrapToolWithEmitter(baseQueryTool, 'query', baseQueryTool.execute);
642
+ export const extractToolInstance = wrapToolWithEmitter(baseExtractTool, 'extract', baseExtractTool.execute);
643
+ export const implementToolInstance = wrapToolWithEmitter(baseImplementTool, 'implement', baseImplementTool.execute);
644
+ export const listFilesToolInstance = wrapToolWithEmitter(baseListFilesTool, 'listFiles', baseListFilesTool.execute);
645
+ export const searchFilesToolInstance = wrapToolWithEmitter(baseSearchFilesTool, 'searchFiles', baseSearchFilesTool.execute);
646
+
647
+ // --- Backward Compatibility Layer (probeTool mapping to searchToolInstance) ---
648
+ // This might be less relevant if the AI is strictly using the new XML format,
649
+ // but keep it for potential direct API calls or older UI elements.
650
+ export const probeTool = {
651
+ ...searchToolInstance, // Inherit schema description etc. from the wrapped search tool
652
+ name: "search", // Explicitly set name
653
+ description: 'DEPRECATED: Use <search> tool instead. Search code using keywords.',
654
+ // parameters: searchSchema, // Use the imported schema
655
+ execute: async (params) => { // Expects { keywords, folder, ..., sessionId }
656
+ const debug = process.env.DEBUG_CHAT === '1';
657
+ if (debug) {
658
+ console.log(`[DEBUG] probeTool (Compatibility Layer) executing for session ${params.sessionId}`);
659
+ }
660
+
661
+ // Map old params ('keywords', 'folder') to new ones ('query', 'path')
662
+ const { keywords, folder, sessionId, ...rest } = params;
663
+ const mappedParams = {
664
+ query: keywords,
665
+ path: folder || '.', // Default path if folder is missing
666
+ sessionId: sessionId, // Pass session ID through
667
+ ...rest // Pass other params like allow_tests, maxResults etc.
668
+ };
669
+
670
+ if (debug) {
671
+ console.log("[DEBUG] probeTool mapped params: ", mappedParams);
672
+ }
673
+
674
+ // Call the *wrapped* searchToolInstance execute function
675
+ // It will handle cancellation checks and event emitting internally
676
+ try {
677
+ // Note: The name emitted by searchToolInstance will be 'search', not 'probeTool' or 'searchCode'
678
+ const result = await searchToolInstance.execute(mappedParams);
679
+
680
+ // Format the result for backward compatibility if needed by caller
681
+ // The raw result from searchToolInstance is likely just the search results array/string
682
+ const formattedResult = {
683
+ results: result, // Assuming result is the direct data
684
+ command: `probe search --query "${keywords}" --path "${folder || '.'}"`, // Reconstruct approx command
685
+ timestamp: new Date().toISOString()
686
+ };
687
+ if (debug) {
688
+ console.log("[DEBUG] probeTool compatibility layer returning formatted result.");
689
+ }
690
+ return formattedResult;
691
+
692
+ } catch (error) {
693
+ if (debug) {
694
+ console.error(`[DEBUG] Error in probeTool compatibility layer:`, error);
695
+ }
696
+ // Error is already emitted by the wrapped searchToolInstance, just re-throw
697
+ throw error;
698
+ }
699
+ }
700
+ };
701
+ // Export necessary items
702
+ export { DEFAULT_SYSTEM_MESSAGE, listFilesByLevel };
703
+ // Export the tool generator functions if needed elsewhere
704
+ export { searchTool, queryTool, extractTool };
705
+
706
+ // Export capabilities information for the new tools
707
+ export const toolCapabilities = {
708
+ search: "Search code using keywords and patterns",
709
+ query: "Query code with structured parameters for more precise results",
710
+ extract: "Extract code blocks and context from files",
711
+ implement: "Implement features or fix bugs using aider (requires --allow-edit)",
712
+ listFiles: "List files and directories in a specified location",
713
+ searchFiles: "Find files matching a glob pattern with recursive search capability"
714
+ };