@probelabs/probe 0.6.0-rc102 → 0.6.0-rc104
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/build/agent/ProbeAgent.js +330 -11
- package/build/agent/index.js +28142 -448
- package/build/agent/probeTool.js +23 -5
- package/build/agent/tools.js +57 -5
- package/build/mcp/index.js +23 -144
- package/build/mcp/index.ts +22 -161
- package/build/utils.js +1 -1
- package/cjs/agent/ProbeAgent.cjs +27948 -270
- package/cjs/index.cjs +27996 -318
- package/package.json +2 -1
- package/src/agent/ProbeAgent.js +330 -11
- package/src/agent/index.js +39 -45
- package/src/agent/probeTool.js +23 -5
- package/src/agent/tools.js +57 -5
- package/src/mcp/index.ts +22 -161
- package/src/utils.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@probelabs/probe",
|
|
3
|
-
"version": "0.6.0-
|
|
3
|
+
"version": "0.6.0-rc104",
|
|
4
4
|
"description": "Node.js wrapper for the probe code search tool",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"author": "probelabs",
|
|
69
69
|
"license": "ISC",
|
|
70
70
|
"dependencies": {
|
|
71
|
+
"@ai-sdk/amazon-bedrock": "^1.0.8",
|
|
71
72
|
"@ai-sdk/anthropic": "^2.0.8",
|
|
72
73
|
"@ai-sdk/google": "^2.0.14",
|
|
73
74
|
"@ai-sdk/openai": "^2.0.10",
|
package/src/agent/ProbeAgent.js
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
3
3
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
4
4
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
5
|
+
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
|
|
5
6
|
import { streamText } from 'ai';
|
|
6
7
|
import { randomUUID } from 'crypto';
|
|
7
8
|
import { EventEmitter } from 'events';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { readFile, stat } from 'fs/promises';
|
|
11
|
+
import { resolve, isAbsolute } from 'path';
|
|
8
12
|
import { TokenCounter } from './tokenCounter.js';
|
|
9
13
|
import {
|
|
10
14
|
createTools,
|
|
@@ -46,6 +50,12 @@ import {
|
|
|
46
50
|
const MAX_TOOL_ITERATIONS = parseInt(process.env.MAX_TOOL_ITERATIONS || '30', 10);
|
|
47
51
|
const MAX_HISTORY_MESSAGES = 100;
|
|
48
52
|
|
|
53
|
+
// Supported image file extensions
|
|
54
|
+
const SUPPORTED_IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'svg'];
|
|
55
|
+
|
|
56
|
+
// Maximum image file size (20MB) to prevent OOM attacks
|
|
57
|
+
const MAX_IMAGE_FILE_SIZE = 20 * 1024 * 1024;
|
|
58
|
+
|
|
49
59
|
/**
|
|
50
60
|
* ProbeAgent class to handle AI interactions with code search capabilities
|
|
51
61
|
*/
|
|
@@ -82,8 +92,14 @@ export class ProbeAgent {
|
|
|
82
92
|
this.maxResponseTokens = options.maxResponseTokens || parseInt(process.env.MAX_RESPONSE_TOKENS || '0', 10) || null;
|
|
83
93
|
this.disableMermaidValidation = !!options.disableMermaidValidation;
|
|
84
94
|
|
|
85
|
-
// Search configuration
|
|
86
|
-
|
|
95
|
+
// Search configuration - support both path (single) and allowedFolders (array)
|
|
96
|
+
if (options.allowedFolders && Array.isArray(options.allowedFolders)) {
|
|
97
|
+
this.allowedFolders = options.allowedFolders;
|
|
98
|
+
} else if (options.path) {
|
|
99
|
+
this.allowedFolders = [options.path];
|
|
100
|
+
} else {
|
|
101
|
+
this.allowedFolders = [process.cwd()];
|
|
102
|
+
}
|
|
87
103
|
|
|
88
104
|
// API configuration
|
|
89
105
|
this.clientApiProvider = options.provider || null;
|
|
@@ -105,6 +121,10 @@ export class ProbeAgent {
|
|
|
105
121
|
// Initialize chat history
|
|
106
122
|
this.history = [];
|
|
107
123
|
|
|
124
|
+
// Initialize image tracking for agentic loop
|
|
125
|
+
this.pendingImages = new Map(); // Map<imagePath, base64Data> to avoid reloading
|
|
126
|
+
this.currentImages = []; // Currently active images for AI calls
|
|
127
|
+
|
|
108
128
|
// Initialize event emitter for tool execution updates
|
|
109
129
|
this.events = new EventEmitter();
|
|
110
130
|
|
|
@@ -173,12 +193,18 @@ export class ProbeAgent {
|
|
|
173
193
|
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
|
174
194
|
const openaiApiKey = process.env.OPENAI_API_KEY;
|
|
175
195
|
const googleApiKey = process.env.GOOGLE_API_KEY;
|
|
196
|
+
const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
197
|
+
const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
198
|
+
const awsRegion = process.env.AWS_REGION;
|
|
199
|
+
const awsSessionToken = process.env.AWS_SESSION_TOKEN;
|
|
200
|
+
const awsApiKey = process.env.AWS_BEDROCK_API_KEY;
|
|
176
201
|
|
|
177
202
|
// Get custom API URLs if provided
|
|
178
203
|
const llmBaseUrl = process.env.LLM_BASE_URL;
|
|
179
204
|
const anthropicApiUrl = process.env.ANTHROPIC_API_URL || llmBaseUrl;
|
|
180
205
|
const openaiApiUrl = process.env.OPENAI_API_URL || llmBaseUrl;
|
|
181
206
|
const googleApiUrl = process.env.GOOGLE_API_URL || llmBaseUrl;
|
|
207
|
+
const awsBedrockBaseUrl = process.env.AWS_BEDROCK_BASE_URL || llmBaseUrl;
|
|
182
208
|
|
|
183
209
|
// Get model override if provided
|
|
184
210
|
const modelName = process.env.MODEL_NAME;
|
|
@@ -187,7 +213,12 @@ export class ProbeAgent {
|
|
|
187
213
|
const forceProvider = this.clientApiProvider || (process.env.FORCE_PROVIDER ? process.env.FORCE_PROVIDER.toLowerCase() : null);
|
|
188
214
|
|
|
189
215
|
if (this.debug) {
|
|
190
|
-
|
|
216
|
+
const hasAwsCredentials = !!(awsAccessKeyId && awsSecretAccessKey && awsRegion);
|
|
217
|
+
const hasAwsApiKey = !!awsApiKey;
|
|
218
|
+
console.log(`[DEBUG] Available API keys: Anthropic=${!!anthropicApiKey}, OpenAI=${!!openaiApiKey}, Google=${!!googleApiKey}, AWS Bedrock=${hasAwsCredentials || hasAwsApiKey}`);
|
|
219
|
+
if (hasAwsCredentials) console.log(`[DEBUG] AWS credentials: AccessKey=${!!awsAccessKeyId}, SecretKey=${!!awsSecretAccessKey}, Region=${awsRegion}, SessionToken=${!!awsSessionToken}`);
|
|
220
|
+
if (hasAwsApiKey) console.log(`[DEBUG] AWS API Key provided`);
|
|
221
|
+
if (awsBedrockBaseUrl) console.log(`[DEBUG] AWS Bedrock base URL: ${awsBedrockBaseUrl}`);
|
|
191
222
|
console.log(`[DEBUG] Force provider: ${forceProvider || '(not set)'}`);
|
|
192
223
|
if (modelName) console.log(`[DEBUG] Model override: ${modelName}`);
|
|
193
224
|
}
|
|
@@ -203,6 +234,9 @@ export class ProbeAgent {
|
|
|
203
234
|
} else if (forceProvider === 'google' && googleApiKey) {
|
|
204
235
|
this.initializeGoogleModel(googleApiKey, googleApiUrl, modelName);
|
|
205
236
|
return;
|
|
237
|
+
} else if (forceProvider === 'bedrock' && ((awsAccessKeyId && awsSecretAccessKey && awsRegion) || awsApiKey)) {
|
|
238
|
+
this.initializeBedrockModel(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken, awsApiKey, awsBedrockBaseUrl, modelName);
|
|
239
|
+
return;
|
|
206
240
|
}
|
|
207
241
|
console.warn(`WARNING: Forced provider "${forceProvider}" selected but required API key is missing or invalid! Falling back to auto-detection.`);
|
|
208
242
|
}
|
|
@@ -214,8 +248,10 @@ export class ProbeAgent {
|
|
|
214
248
|
this.initializeOpenAIModel(openaiApiKey, openaiApiUrl, modelName);
|
|
215
249
|
} else if (googleApiKey) {
|
|
216
250
|
this.initializeGoogleModel(googleApiKey, googleApiUrl, modelName);
|
|
251
|
+
} else if ((awsAccessKeyId && awsSecretAccessKey && awsRegion) || awsApiKey) {
|
|
252
|
+
this.initializeBedrockModel(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken, awsApiKey, awsBedrockBaseUrl, modelName);
|
|
217
253
|
} else {
|
|
218
|
-
throw new Error('No API key provided. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, or
|
|
254
|
+
throw new Error('No API key provided. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION), or AWS_BEDROCK_API_KEY environment variables.');
|
|
219
255
|
}
|
|
220
256
|
}
|
|
221
257
|
|
|
@@ -268,6 +304,266 @@ export class ProbeAgent {
|
|
|
268
304
|
}
|
|
269
305
|
}
|
|
270
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Initialize AWS Bedrock model
|
|
309
|
+
*/
|
|
310
|
+
initializeBedrockModel(accessKeyId, secretAccessKey, region, sessionToken, apiKey, baseURL, modelName) {
|
|
311
|
+
// Build configuration object, only including defined values
|
|
312
|
+
const config = {};
|
|
313
|
+
|
|
314
|
+
// Authentication - prefer API key if provided, otherwise use AWS credentials
|
|
315
|
+
if (apiKey) {
|
|
316
|
+
config.apiKey = apiKey;
|
|
317
|
+
} else if (accessKeyId && secretAccessKey) {
|
|
318
|
+
config.accessKeyId = accessKeyId;
|
|
319
|
+
config.secretAccessKey = secretAccessKey;
|
|
320
|
+
if (sessionToken) {
|
|
321
|
+
config.sessionToken = sessionToken;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Region is required for AWS credentials but optional for API key
|
|
326
|
+
if (region) {
|
|
327
|
+
config.region = region;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Optional base URL
|
|
331
|
+
if (baseURL) {
|
|
332
|
+
config.baseURL = baseURL;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.provider = createAmazonBedrock(config);
|
|
336
|
+
this.model = modelName || 'anthropic.claude-sonnet-4-20250514-v1:0';
|
|
337
|
+
this.apiType = 'bedrock';
|
|
338
|
+
|
|
339
|
+
if (this.debug) {
|
|
340
|
+
const authMethod = apiKey ? 'API Key' : 'AWS Credentials';
|
|
341
|
+
const regionInfo = region ? ` (Region: ${region})` : '';
|
|
342
|
+
const baseUrlInfo = baseURL ? ` (Base URL: ${baseURL})` : '';
|
|
343
|
+
console.log(`Using AWS Bedrock API with model: ${this.model}${regionInfo} [Auth: ${authMethod}]${baseUrlInfo}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Process assistant response content and detect/load image references
|
|
349
|
+
* @param {string} content - The assistant's response content
|
|
350
|
+
* @returns {Promise<void>}
|
|
351
|
+
*/
|
|
352
|
+
async processImageReferences(content) {
|
|
353
|
+
if (!content) return;
|
|
354
|
+
|
|
355
|
+
// Enhanced pattern to detect image file mentions in various contexts
|
|
356
|
+
// Looks for: "image", "file", "screenshot", etc. followed by path-like strings with image extensions
|
|
357
|
+
const extensionsPattern = `(?:${SUPPORTED_IMAGE_EXTENSIONS.join('|')})`;
|
|
358
|
+
const imagePatterns = [
|
|
359
|
+
// Direct file path mentions: "./screenshot.png", "/path/to/image.jpg", etc.
|
|
360
|
+
new RegExp(`(?:\\.?\\.\\/)?[^\\s"'<>\\[\\]]+\\\.${extensionsPattern}(?!\\w)`, 'gi'),
|
|
361
|
+
// Contextual mentions: "look at image.png", "the file screenshot.jpg shows"
|
|
362
|
+
new RegExp(`(?:image|file|screenshot|diagram|photo|picture|graphic)\\s*:?\\s*([^\\s"'<>\\[\\]]+\\.${extensionsPattern})(?!\\w)`, 'gi'),
|
|
363
|
+
// Tool result mentions: often contain file paths
|
|
364
|
+
new RegExp(`(?:found|saved|created|generated).*?([^\\s"'<>\\[\\]]+\\.${extensionsPattern})(?!\\w)`, 'gi')
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
const foundPaths = new Set();
|
|
368
|
+
|
|
369
|
+
// Extract potential image paths using all patterns
|
|
370
|
+
for (const pattern of imagePatterns) {
|
|
371
|
+
let match;
|
|
372
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
373
|
+
// For patterns with capture groups, use the captured path; otherwise use the full match
|
|
374
|
+
const imagePath = match[1] || match[0];
|
|
375
|
+
if (imagePath && imagePath.length > 0) {
|
|
376
|
+
foundPaths.add(imagePath.trim());
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (foundPaths.size === 0) return;
|
|
382
|
+
|
|
383
|
+
if (this.debug) {
|
|
384
|
+
console.log(`[DEBUG] Found ${foundPaths.size} potential image references:`, Array.from(foundPaths));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Process each found path
|
|
388
|
+
for (const imagePath of foundPaths) {
|
|
389
|
+
await this.loadImageIfValid(imagePath);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Load and cache an image if it's valid and accessible
|
|
395
|
+
* @param {string} imagePath - Path to the image file
|
|
396
|
+
* @returns {Promise<boolean>} - True if image was loaded successfully
|
|
397
|
+
*/
|
|
398
|
+
async loadImageIfValid(imagePath) {
|
|
399
|
+
try {
|
|
400
|
+
// Skip if already loaded
|
|
401
|
+
if (this.pendingImages.has(imagePath)) {
|
|
402
|
+
if (this.debug) {
|
|
403
|
+
console.log(`[DEBUG] Image already loaded: ${imagePath}`);
|
|
404
|
+
}
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Security validation: check if path is within any allowed directory
|
|
409
|
+
const allowedDirs = this.allowedFolders && this.allowedFolders.length > 0 ? this.allowedFolders : [process.cwd()];
|
|
410
|
+
|
|
411
|
+
let absolutePath;
|
|
412
|
+
let isPathAllowed = false;
|
|
413
|
+
|
|
414
|
+
// If absolute path, check if it's within any allowed directory
|
|
415
|
+
if (isAbsolute(imagePath)) {
|
|
416
|
+
absolutePath = imagePath;
|
|
417
|
+
isPathAllowed = allowedDirs.some(dir => absolutePath.startsWith(resolve(dir)));
|
|
418
|
+
} else {
|
|
419
|
+
// For relative paths, try resolving against each allowed directory
|
|
420
|
+
for (const dir of allowedDirs) {
|
|
421
|
+
const resolvedPath = resolve(dir, imagePath);
|
|
422
|
+
if (resolvedPath.startsWith(resolve(dir))) {
|
|
423
|
+
absolutePath = resolvedPath;
|
|
424
|
+
isPathAllowed = true;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Security check: ensure path is within at least one allowed directory
|
|
431
|
+
if (!isPathAllowed) {
|
|
432
|
+
if (this.debug) {
|
|
433
|
+
console.log(`[DEBUG] Image path outside allowed directories: ${imagePath}`);
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Check if file exists and get file stats
|
|
439
|
+
let fileStats;
|
|
440
|
+
try {
|
|
441
|
+
fileStats = await stat(absolutePath);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (this.debug) {
|
|
444
|
+
console.log(`[DEBUG] Image file not found: ${absolutePath}`);
|
|
445
|
+
}
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Validate file size to prevent OOM attacks
|
|
450
|
+
if (fileStats.size > MAX_IMAGE_FILE_SIZE) {
|
|
451
|
+
if (this.debug) {
|
|
452
|
+
console.log(`[DEBUG] Image file too large: ${absolutePath} (${fileStats.size} bytes, max: ${MAX_IMAGE_FILE_SIZE})`);
|
|
453
|
+
}
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Validate file extension
|
|
458
|
+
const extension = absolutePath.toLowerCase().split('.').pop();
|
|
459
|
+
if (!SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
|
|
460
|
+
if (this.debug) {
|
|
461
|
+
console.log(`[DEBUG] Unsupported image format: ${extension}`);
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Determine MIME type
|
|
467
|
+
const mimeTypes = {
|
|
468
|
+
'png': 'image/png',
|
|
469
|
+
'jpg': 'image/jpeg',
|
|
470
|
+
'jpeg': 'image/jpeg',
|
|
471
|
+
'webp': 'image/webp',
|
|
472
|
+
'gif': 'image/gif',
|
|
473
|
+
'bmp': 'image/bmp',
|
|
474
|
+
'svg': 'image/svg+xml'
|
|
475
|
+
};
|
|
476
|
+
const mimeType = mimeTypes[extension];
|
|
477
|
+
|
|
478
|
+
// Read and encode file asynchronously
|
|
479
|
+
const fileBuffer = await readFile(absolutePath);
|
|
480
|
+
const base64Data = fileBuffer.toString('base64');
|
|
481
|
+
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
|
482
|
+
|
|
483
|
+
// Cache the loaded image
|
|
484
|
+
this.pendingImages.set(imagePath, dataUrl);
|
|
485
|
+
|
|
486
|
+
if (this.debug) {
|
|
487
|
+
console.log(`[DEBUG] Successfully loaded image: ${imagePath} (${fileBuffer.length} bytes)`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return true;
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (this.debug) {
|
|
493
|
+
console.log(`[DEBUG] Failed to load image ${imagePath}: ${error.message}`);
|
|
494
|
+
}
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Get all currently loaded images as an array for AI model consumption
|
|
501
|
+
* @returns {Array<string>} - Array of base64 data URLs
|
|
502
|
+
*/
|
|
503
|
+
getCurrentImages() {
|
|
504
|
+
return Array.from(this.pendingImages.values());
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Clear loaded images (useful for new conversations)
|
|
509
|
+
*/
|
|
510
|
+
clearLoadedImages() {
|
|
511
|
+
this.pendingImages.clear();
|
|
512
|
+
this.currentImages = [];
|
|
513
|
+
if (this.debug) {
|
|
514
|
+
console.log('[DEBUG] Cleared all loaded images');
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Prepare messages for AI consumption, adding images to the latest user message if available
|
|
520
|
+
* @param {Array} messages - Current conversation messages
|
|
521
|
+
* @returns {Array} - Messages formatted for AI SDK with potential image content
|
|
522
|
+
*/
|
|
523
|
+
prepareMessagesWithImages(messages) {
|
|
524
|
+
const loadedImages = this.getCurrentImages();
|
|
525
|
+
|
|
526
|
+
// If no images loaded, return messages as-is
|
|
527
|
+
if (loadedImages.length === 0) {
|
|
528
|
+
return messages;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Clone messages to avoid mutating the original
|
|
532
|
+
const messagesWithImages = [...messages];
|
|
533
|
+
|
|
534
|
+
// Find the last user message to attach images to
|
|
535
|
+
const lastUserMessageIndex = messagesWithImages.map(m => m.role).lastIndexOf('user');
|
|
536
|
+
|
|
537
|
+
if (lastUserMessageIndex === -1) {
|
|
538
|
+
if (this.debug) {
|
|
539
|
+
console.log('[DEBUG] No user messages found to attach images to');
|
|
540
|
+
}
|
|
541
|
+
return messages;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const lastUserMessage = messagesWithImages[lastUserMessageIndex];
|
|
545
|
+
|
|
546
|
+
// Convert to multimodal format if we have images
|
|
547
|
+
if (typeof lastUserMessage.content === 'string') {
|
|
548
|
+
messagesWithImages[lastUserMessageIndex] = {
|
|
549
|
+
...lastUserMessage,
|
|
550
|
+
content: [
|
|
551
|
+
{ type: 'text', text: lastUserMessage.content },
|
|
552
|
+
...loadedImages.map(imageData => ({
|
|
553
|
+
type: 'image',
|
|
554
|
+
image: imageData
|
|
555
|
+
}))
|
|
556
|
+
]
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
if (this.debug) {
|
|
560
|
+
console.log(`[DEBUG] Added ${loadedImages.length} images to the latest user message`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return messagesWithImages;
|
|
565
|
+
}
|
|
566
|
+
|
|
271
567
|
/**
|
|
272
568
|
* Initialize mock model for testing
|
|
273
569
|
*/
|
|
@@ -695,9 +991,12 @@ When troubleshooting:
|
|
|
695
991
|
try {
|
|
696
992
|
// Wrap AI request with tracing if available
|
|
697
993
|
const executeAIRequest = async () => {
|
|
994
|
+
// Prepare messages with potential image content
|
|
995
|
+
const messagesForAI = this.prepareMessagesWithImages(currentMessages);
|
|
996
|
+
|
|
698
997
|
const result = await streamText({
|
|
699
998
|
model: this.provider(this.model),
|
|
700
|
-
messages:
|
|
999
|
+
messages: messagesForAI,
|
|
701
1000
|
maxTokens: maxResponseTokens,
|
|
702
1001
|
temperature: 0.3,
|
|
703
1002
|
});
|
|
@@ -741,6 +1040,11 @@ When troubleshooting:
|
|
|
741
1040
|
console.log(`[DEBUG] Assistant response (${assistantResponseContent.length} chars): ${assistantPreview}`);
|
|
742
1041
|
}
|
|
743
1042
|
|
|
1043
|
+
// Process image references in assistant response for next iteration
|
|
1044
|
+
if (assistantResponseContent) {
|
|
1045
|
+
await this.processImageReferences(assistantResponseContent);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
744
1048
|
// Parse tool call from response with valid tools list
|
|
745
1049
|
const validTools = [
|
|
746
1050
|
'search', 'query', 'extract', 'listFiles', 'searchFiles', 'attempt_completion'
|
|
@@ -819,8 +1123,12 @@ When troubleshooting:
|
|
|
819
1123
|
} else if (this.toolImplementations[toolName]) {
|
|
820
1124
|
// Execute native tool
|
|
821
1125
|
try {
|
|
822
|
-
// Add sessionId to params for tool execution
|
|
823
|
-
const toolParams = {
|
|
1126
|
+
// Add sessionId and workingDirectory to params for tool execution
|
|
1127
|
+
const toolParams = {
|
|
1128
|
+
...params,
|
|
1129
|
+
sessionId: this.sessionId,
|
|
1130
|
+
workingDirectory: (this.allowedFolders && this.allowedFolders[0]) || process.cwd()
|
|
1131
|
+
};
|
|
824
1132
|
|
|
825
1133
|
// Emit tool start event
|
|
826
1134
|
this.events.emit('toolCall', {
|
|
@@ -898,11 +1206,20 @@ When troubleshooting:
|
|
|
898
1206
|
|
|
899
1207
|
// Add assistant response and tool result to conversation
|
|
900
1208
|
currentMessages.push({ role: 'assistant', content: assistantResponseContent });
|
|
1209
|
+
|
|
1210
|
+
const toolResultContent = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult, null, 2);
|
|
1211
|
+
const toolResultMessage = `<tool_result>\n${toolResultContent}\n</tool_result>`;
|
|
1212
|
+
|
|
901
1213
|
currentMessages.push({
|
|
902
1214
|
role: 'user',
|
|
903
|
-
content:
|
|
1215
|
+
content: toolResultMessage
|
|
904
1216
|
});
|
|
905
1217
|
|
|
1218
|
+
// Process tool result for image references
|
|
1219
|
+
if (toolResultContent) {
|
|
1220
|
+
await this.processImageReferences(toolResultContent);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
906
1223
|
if (this.debug) {
|
|
907
1224
|
console.log(`[DEBUG] Tool ${toolName} executed successfully. Result length: ${typeof toolResult === 'string' ? toolResult.length : JSON.stringify(toolResult).length}`);
|
|
908
1225
|
}
|
|
@@ -964,8 +1281,10 @@ Remember: Use proper XML format with BOTH opening and closing tags:
|
|
|
964
1281
|
<parameter>value</parameter>
|
|
965
1282
|
</tool_name>
|
|
966
1283
|
|
|
967
|
-
Or for quick completion if your previous response was already correct:
|
|
968
|
-
<attempt_complete
|
|
1284
|
+
Or for quick completion if your previous response was already correct and complete:
|
|
1285
|
+
<attempt_complete>
|
|
1286
|
+
|
|
1287
|
+
IMPORTANT: When using <attempt_complete>, this must be the ONLY content in your response. No additional text, explanations, or other content should be included. This tag signals to reuse your previous response as the final answer.`;
|
|
969
1288
|
}
|
|
970
1289
|
|
|
971
1290
|
currentMessages.push({
|
|
@@ -1483,4 +1802,4 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
1483
1802
|
console.log(`[DEBUG] Agent cancelled for session ${this.sessionId}`);
|
|
1484
1803
|
}
|
|
1485
1804
|
}
|
|
1486
|
-
}
|
|
1805
|
+
}
|
package/src/agent/index.js
CHANGED
|
@@ -112,6 +112,7 @@ function parseArgs() {
|
|
|
112
112
|
acp: false,
|
|
113
113
|
question: null,
|
|
114
114
|
path: null,
|
|
115
|
+
allowedFolders: null,
|
|
115
116
|
prompt: null,
|
|
116
117
|
systemPrompt: null,
|
|
117
118
|
schema: null,
|
|
@@ -145,6 +146,8 @@ function parseArgs() {
|
|
|
145
146
|
config.allowEdit = true;
|
|
146
147
|
} else if (arg === '--path' && i + 1 < args.length) {
|
|
147
148
|
config.path = args[++i];
|
|
149
|
+
} else if (arg === '--allowed-folders' && i + 1 < args.length) {
|
|
150
|
+
config.allowedFolders = args[++i].split(',').map(dir => dir.trim());
|
|
148
151
|
} else if (arg === '--prompt' && i + 1 < args.length) {
|
|
149
152
|
config.prompt = args[++i];
|
|
150
153
|
} else if (arg === '--system-prompt' && i + 1 < args.length) {
|
|
@@ -201,6 +204,7 @@ Usage:
|
|
|
201
204
|
|
|
202
205
|
Options:
|
|
203
206
|
--path <dir> Search directory (default: current)
|
|
207
|
+
--allowed-folders <dirs> Comma-separated list of allowed directories for file operations
|
|
204
208
|
--prompt <type> Persona: code-explorer, engineer, code-review, support, architect
|
|
205
209
|
--system-prompt <text|file> Custom system prompt (text or file path)
|
|
206
210
|
--schema <schema|file> Output schema (JSON, XML, any format - text or file path)
|
|
@@ -266,6 +270,9 @@ class ProbeAgentMcpServer {
|
|
|
266
270
|
}
|
|
267
271
|
);
|
|
268
272
|
|
|
273
|
+
// Don't initialize AI agent on startup - lazy initialize when needed
|
|
274
|
+
this.agent = null;
|
|
275
|
+
|
|
269
276
|
this.setupToolHandlers();
|
|
270
277
|
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
|
271
278
|
process.on('SIGINT', async () => {
|
|
@@ -293,17 +300,22 @@ class ProbeAgentMcpServer {
|
|
|
293
300
|
tools: [
|
|
294
301
|
{
|
|
295
302
|
name: 'search_code',
|
|
296
|
-
description: "
|
|
303
|
+
description: "AI agent that answers free-form questions about codebases. Ask detailed questions in natural language.",
|
|
297
304
|
inputSchema: {
|
|
298
305
|
type: 'object',
|
|
299
306
|
properties: {
|
|
300
307
|
query: {
|
|
301
308
|
type: 'string',
|
|
302
|
-
description: '
|
|
309
|
+
description: 'A detailed, free-form question about the codebase in natural language. Be specific and descriptive. Example: "How does the authentication system work and where is user session management implemented?"',
|
|
303
310
|
},
|
|
304
311
|
path: {
|
|
305
312
|
type: 'string',
|
|
306
|
-
description: '
|
|
313
|
+
description: 'Absolute path to the directory to search in (e.g., "/Users/username/projects/myproject").',
|
|
314
|
+
},
|
|
315
|
+
allowed_folders: {
|
|
316
|
+
type: 'array',
|
|
317
|
+
items: { type: 'string' },
|
|
318
|
+
description: 'Optional list of allowed directories for file operations. Defaults to current directory if not specified.',
|
|
307
319
|
},
|
|
308
320
|
prompt: {
|
|
309
321
|
type: 'string',
|
|
@@ -312,34 +324,6 @@ class ProbeAgentMcpServer {
|
|
|
312
324
|
system_prompt: {
|
|
313
325
|
type: 'string',
|
|
314
326
|
description: 'Optional custom system prompt (text or file path).',
|
|
315
|
-
},
|
|
316
|
-
provider: {
|
|
317
|
-
type: 'string',
|
|
318
|
-
description: 'Optional AI provider to force: anthropic, openai, google.',
|
|
319
|
-
},
|
|
320
|
-
model: {
|
|
321
|
-
type: 'string',
|
|
322
|
-
description: 'Optional model name override.',
|
|
323
|
-
},
|
|
324
|
-
allow_edit: {
|
|
325
|
-
type: 'boolean',
|
|
326
|
-
description: 'Enable code modification capabilities.',
|
|
327
|
-
},
|
|
328
|
-
max_iterations: {
|
|
329
|
-
type: 'number',
|
|
330
|
-
description: 'Maximum number of tool iterations (default: 30).',
|
|
331
|
-
},
|
|
332
|
-
max_response_tokens: {
|
|
333
|
-
type: 'number',
|
|
334
|
-
description: 'Maximum tokens for AI response (overrides model defaults).',
|
|
335
|
-
},
|
|
336
|
-
schema: {
|
|
337
|
-
type: 'string',
|
|
338
|
-
description: 'Optional output schema (JSON, XML, or any format - text or file path).',
|
|
339
|
-
},
|
|
340
|
-
no_mermaid_validation: {
|
|
341
|
-
type: 'boolean',
|
|
342
|
-
description: 'Disable automatic mermaid diagram validation and fixing.',
|
|
343
327
|
}
|
|
344
328
|
},
|
|
345
329
|
required: ['query']
|
|
@@ -393,20 +377,29 @@ class ProbeAgentMcpServer {
|
|
|
393
377
|
}
|
|
394
378
|
}
|
|
395
379
|
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
380
|
+
// Lazy initialize AI agent only when tool is called
|
|
381
|
+
if (!this.agent) {
|
|
382
|
+
if (process.env.DEBUG === '1') {
|
|
383
|
+
console.error('[DEBUG] Initializing AI agent on first MCP tool call');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Create agent with configuration
|
|
387
|
+
const agentConfig = {
|
|
388
|
+
path: args.path || (args.allowed_folders && args.allowed_folders[0]) || process.cwd(),
|
|
389
|
+
promptType: args.prompt || 'code-explorer',
|
|
390
|
+
customPrompt: systemPrompt,
|
|
391
|
+
provider: args.provider,
|
|
392
|
+
model: args.model,
|
|
393
|
+
allowEdit: !!args.allow_edit,
|
|
394
|
+
debug: process.env.DEBUG === '1',
|
|
395
|
+
maxResponseTokens: args.max_response_tokens,
|
|
396
|
+
disableMermaidValidation: !!args.no_mermaid_validation
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
this.agent = new ProbeAgent(agentConfig);
|
|
400
|
+
}
|
|
408
401
|
|
|
409
|
-
const agent =
|
|
402
|
+
const agent = this.agent;
|
|
410
403
|
let result = await agent.answer(query, [], { schema });
|
|
411
404
|
|
|
412
405
|
// If schema is provided, make a follow-up request to format the output
|
|
@@ -479,7 +472,7 @@ class ProbeAgentMcpServer {
|
|
|
479
472
|
}
|
|
480
473
|
|
|
481
474
|
// Get token usage for debugging
|
|
482
|
-
const tokenUsage = agent.getTokenUsage();
|
|
475
|
+
const tokenUsage = this.agent.getTokenUsage();
|
|
483
476
|
console.error(`Token usage: ${JSON.stringify(tokenUsage)}`);
|
|
484
477
|
|
|
485
478
|
return {
|
|
@@ -631,6 +624,7 @@ async function main() {
|
|
|
631
624
|
// Create and configure agent
|
|
632
625
|
const agentConfig = {
|
|
633
626
|
path: config.path,
|
|
627
|
+
allowedFolders: config.allowedFolders,
|
|
634
628
|
promptType: config.prompt,
|
|
635
629
|
customPrompt: systemPrompt,
|
|
636
630
|
allowEdit: config.allowEdit,
|
package/src/agent/probeTool.js
CHANGED
|
@@ -207,14 +207,24 @@ export function createWrappedTools(baseTools) {
|
|
|
207
207
|
// Simple file listing tool
|
|
208
208
|
export const listFilesTool = {
|
|
209
209
|
execute: async (params) => {
|
|
210
|
-
const { directory = '.' } = params;
|
|
210
|
+
const { directory = '.', workingDirectory } = params;
|
|
211
|
+
|
|
212
|
+
// Use the provided working directory, or fall back to process.cwd()
|
|
213
|
+
const baseCwd = workingDirectory || process.cwd();
|
|
214
|
+
|
|
215
|
+
// Security: Validate path to prevent traversal attacks
|
|
216
|
+
const secureBaseDir = path.resolve(baseCwd);
|
|
217
|
+
const targetDir = path.resolve(secureBaseDir, directory);
|
|
218
|
+
if (!targetDir.startsWith(secureBaseDir + path.sep) && targetDir !== secureBaseDir) {
|
|
219
|
+
throw new Error('Path traversal attempt detected. Access denied.');
|
|
220
|
+
}
|
|
211
221
|
|
|
212
222
|
try {
|
|
213
223
|
const files = await listFilesByLevel({
|
|
214
|
-
directory,
|
|
224
|
+
directory: targetDir,
|
|
215
225
|
maxFiles: 100,
|
|
216
226
|
respectGitignore: !process.env.PROBE_NO_GITIGNORE || process.env.PROBE_NO_GITIGNORE === '',
|
|
217
|
-
cwd:
|
|
227
|
+
cwd: secureBaseDir
|
|
218
228
|
});
|
|
219
229
|
|
|
220
230
|
return files;
|
|
@@ -227,15 +237,23 @@ export const listFilesTool = {
|
|
|
227
237
|
// Simple file search tool
|
|
228
238
|
export const searchFilesTool = {
|
|
229
239
|
execute: async (params) => {
|
|
230
|
-
const { pattern, directory = '.', recursive = true } = params;
|
|
240
|
+
const { pattern, directory = '.', recursive = true, workingDirectory } = params;
|
|
231
241
|
|
|
232
242
|
if (!pattern) {
|
|
233
243
|
throw new Error('Pattern is required for file search');
|
|
234
244
|
}
|
|
235
245
|
|
|
246
|
+
// Security: Validate path to prevent traversal attacks
|
|
247
|
+
const baseCwd = workingDirectory || process.cwd();
|
|
248
|
+
const secureBaseDir = path.resolve(baseCwd);
|
|
249
|
+
const targetDir = path.resolve(secureBaseDir, directory);
|
|
250
|
+
if (!targetDir.startsWith(secureBaseDir + path.sep) && targetDir !== secureBaseDir) {
|
|
251
|
+
throw new Error('Path traversal attempt detected. Access denied.');
|
|
252
|
+
}
|
|
253
|
+
|
|
236
254
|
try {
|
|
237
255
|
const options = {
|
|
238
|
-
cwd:
|
|
256
|
+
cwd: targetDir,
|
|
239
257
|
ignore: ['node_modules/**', '.git/**'],
|
|
240
258
|
absolute: false
|
|
241
259
|
};
|