@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/README.md +338 -0
- package/TRACING.md +226 -0
- package/appTracer.js +947 -0
- package/auth.js +76 -0
- package/bin/probe-chat.js +13 -0
- package/cancelRequest.js +84 -0
- package/fileSpanExporter.js +183 -0
- package/implement/README.md +228 -0
- package/implement/backends/AiderBackend.js +750 -0
- package/implement/backends/BaseBackend.js +276 -0
- package/implement/backends/ClaudeCodeBackend.js +767 -0
- package/implement/backends/MockBackend.js +237 -0
- package/implement/backends/registry.js +85 -0
- package/implement/core/BackendManager.js +567 -0
- package/implement/core/ImplementTool.js +354 -0
- package/implement/core/config.js +428 -0
- package/implement/core/timeouts.js +58 -0
- package/implement/core/utils.js +496 -0
- package/implement/types/BackendTypes.js +126 -0
- package/index.html +3751 -0
- package/index.js +582 -0
- package/logo.png +0 -0
- package/package.json +101 -0
- package/probeChat.js +269 -0
- package/probeTool.js +714 -0
- package/storage/JsonChatStorage.js +476 -0
- package/telemetry.js +287 -0
- package/test/integration/chatFlows.test.js +320 -0
- package/test/integration/toolCalling.test.js +471 -0
- package/test/mocks/mockLLMProvider.js +269 -0
- package/test/test-backends.js +90 -0
- package/test/testUtils.js +530 -0
- package/test/unit/backendTimeout.test.js +161 -0
- package/test/verify-tests.js +118 -0
- package/tokenCounter.js +419 -0
- package/tokenUsageDisplay.js +134 -0
- package/tools.js +186 -0
- package/webServer.js +1103 -0
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
|
+
};
|