@probelabs/probe 0.6.0-rc103 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc103",
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",
@@ -2,6 +2,7 @@
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';
@@ -91,8 +92,14 @@ export class ProbeAgent {
91
92
  this.maxResponseTokens = options.maxResponseTokens || parseInt(process.env.MAX_RESPONSE_TOKENS || '0', 10) || null;
92
93
  this.disableMermaidValidation = !!options.disableMermaidValidation;
93
94
 
94
- // Search configuration
95
- this.allowedFolders = options.path ? [options.path] : [process.cwd()];
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
+ }
96
103
 
97
104
  // API configuration
98
105
  this.clientApiProvider = options.provider || null;
@@ -186,12 +193,18 @@ export class ProbeAgent {
186
193
  const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
187
194
  const openaiApiKey = process.env.OPENAI_API_KEY;
188
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;
189
201
 
190
202
  // Get custom API URLs if provided
191
203
  const llmBaseUrl = process.env.LLM_BASE_URL;
192
204
  const anthropicApiUrl = process.env.ANTHROPIC_API_URL || llmBaseUrl;
193
205
  const openaiApiUrl = process.env.OPENAI_API_URL || llmBaseUrl;
194
206
  const googleApiUrl = process.env.GOOGLE_API_URL || llmBaseUrl;
207
+ const awsBedrockBaseUrl = process.env.AWS_BEDROCK_BASE_URL || llmBaseUrl;
195
208
 
196
209
  // Get model override if provided
197
210
  const modelName = process.env.MODEL_NAME;
@@ -200,7 +213,12 @@ export class ProbeAgent {
200
213
  const forceProvider = this.clientApiProvider || (process.env.FORCE_PROVIDER ? process.env.FORCE_PROVIDER.toLowerCase() : null);
201
214
 
202
215
  if (this.debug) {
203
- console.log(`[DEBUG] Available API keys: Anthropic=${!!anthropicApiKey}, OpenAI=${!!openaiApiKey}, Google=${!!googleApiKey}`);
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}`);
204
222
  console.log(`[DEBUG] Force provider: ${forceProvider || '(not set)'}`);
205
223
  if (modelName) console.log(`[DEBUG] Model override: ${modelName}`);
206
224
  }
@@ -216,6 +234,9 @@ export class ProbeAgent {
216
234
  } else if (forceProvider === 'google' && googleApiKey) {
217
235
  this.initializeGoogleModel(googleApiKey, googleApiUrl, modelName);
218
236
  return;
237
+ } else if (forceProvider === 'bedrock' && ((awsAccessKeyId && awsSecretAccessKey && awsRegion) || awsApiKey)) {
238
+ this.initializeBedrockModel(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken, awsApiKey, awsBedrockBaseUrl, modelName);
239
+ return;
219
240
  }
220
241
  console.warn(`WARNING: Forced provider "${forceProvider}" selected but required API key is missing or invalid! Falling back to auto-detection.`);
221
242
  }
@@ -227,8 +248,10 @@ export class ProbeAgent {
227
248
  this.initializeOpenAIModel(openaiApiKey, openaiApiUrl, modelName);
228
249
  } else if (googleApiKey) {
229
250
  this.initializeGoogleModel(googleApiKey, googleApiUrl, modelName);
251
+ } else if ((awsAccessKeyId && awsSecretAccessKey && awsRegion) || awsApiKey) {
252
+ this.initializeBedrockModel(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken, awsApiKey, awsBedrockBaseUrl, modelName);
230
253
  } else {
231
- throw new Error('No API key provided. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY environment variable.');
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.');
232
255
  }
233
256
  }
234
257
 
@@ -281,6 +304,46 @@ export class ProbeAgent {
281
304
  }
282
305
  }
283
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
+
284
347
  /**
285
348
  * Process assistant response content and detect/load image references
286
349
  * @param {string} content - The assistant's response content
@@ -1060,8 +1123,12 @@ When troubleshooting:
1060
1123
  } else if (this.toolImplementations[toolName]) {
1061
1124
  // Execute native tool
1062
1125
  try {
1063
- // Add sessionId to params for tool execution
1064
- const toolParams = { ...params, sessionId: this.sessionId };
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
+ };
1065
1132
 
1066
1133
  // Emit tool start event
1067
1134
  this.events.emit('toolCall', {
@@ -1214,8 +1281,10 @@ Remember: Use proper XML format with BOTH opening and closing tags:
1214
1281
  <parameter>value</parameter>
1215
1282
  </tool_name>
1216
1283
 
1217
- Or for quick completion if your previous response was already correct:
1218
- <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.`;
1219
1288
  }
1220
1289
 
1221
1290
  currentMessages.push({
@@ -1733,4 +1802,4 @@ Convert your previous response content into actual JSON data that follows this s
1733
1802
  console.log(`[DEBUG] Agent cancelled for session ${this.sessionId}`);
1734
1803
  }
1735
1804
  }
1736
- }
1805
+ }
@@ -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)
@@ -296,17 +300,22 @@ class ProbeAgentMcpServer {
296
300
  tools: [
297
301
  {
298
302
  name: 'search_code',
299
- description: "Search code and answer questions about the codebase using an AI agent. This tool provides intelligent responses based on code analysis.",
303
+ description: "AI agent that answers free-form questions about codebases. Ask detailed questions in natural language.",
300
304
  inputSchema: {
301
305
  type: 'object',
302
306
  properties: {
303
307
  query: {
304
308
  type: 'string',
305
- description: 'The question or request about the codebase.',
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?"',
306
310
  },
307
311
  path: {
308
312
  type: 'string',
309
- description: 'Optional path to the directory to search in. Defaults to current directory.',
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.',
310
319
  },
311
320
  prompt: {
312
321
  type: 'string',
@@ -315,34 +324,6 @@ class ProbeAgentMcpServer {
315
324
  system_prompt: {
316
325
  type: 'string',
317
326
  description: 'Optional custom system prompt (text or file path).',
318
- },
319
- provider: {
320
- type: 'string',
321
- description: 'Optional AI provider to force: anthropic, openai, google.',
322
- },
323
- model: {
324
- type: 'string',
325
- description: 'Optional model name override.',
326
- },
327
- allow_edit: {
328
- type: 'boolean',
329
- description: 'Enable code modification capabilities.',
330
- },
331
- max_iterations: {
332
- type: 'number',
333
- description: 'Maximum number of tool iterations (default: 30).',
334
- },
335
- max_response_tokens: {
336
- type: 'number',
337
- description: 'Maximum tokens for AI response (overrides model defaults).',
338
- },
339
- schema: {
340
- type: 'string',
341
- description: 'Optional output schema (JSON, XML, or any format - text or file path).',
342
- },
343
- no_mermaid_validation: {
344
- type: 'boolean',
345
- description: 'Disable automatic mermaid diagram validation and fixing.',
346
327
  }
347
328
  },
348
329
  required: ['query']
@@ -404,7 +385,7 @@ class ProbeAgentMcpServer {
404
385
 
405
386
  // Create agent with configuration
406
387
  const agentConfig = {
407
- path: args.path || process.cwd(),
388
+ path: args.path || (args.allowed_folders && args.allowed_folders[0]) || process.cwd(),
408
389
  promptType: args.prompt || 'code-explorer',
409
390
  customPrompt: systemPrompt,
410
391
  provider: args.provider,
@@ -643,6 +624,7 @@ async function main() {
643
624
  // Create and configure agent
644
625
  const agentConfig = {
645
626
  path: config.path,
627
+ allowedFolders: config.allowedFolders,
646
628
  promptType: config.prompt,
647
629
  customPrompt: systemPrompt,
648
630
  allowEdit: config.allowEdit,
@@ -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: process.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: directory,
256
+ cwd: targetDir,
239
257
  ignore: ['node_modules/**', '.git/**'],
240
258
  absolute: false
241
259
  };
@@ -141,12 +141,45 @@ export function parseXmlToolCallWithThinking(xmlString, validTools) {
141
141
  // Remove thinking tags and their content from the XML string
142
142
  let cleanedXmlString = xmlString.replace(/<thinking>[\s\S]*?<\/thinking>/g, '').trim();
143
143
 
144
- // Check for attempt_complete shorthand (single tag with no closing tag and no parameters)
145
- const attemptCompleteMatch = cleanedXmlString.match(/^<attempt_complete>\s*$/);
146
- if (attemptCompleteMatch) {
147
- // Convert shorthand to full attempt_completion format with special marker
144
+ // Enhanced recovery logic for attempt_complete shorthand
145
+ // Check for various forms of attempt_complete tags:
146
+ // 1. Perfect shorthand: <attempt_complete>
147
+ // 2. Self-closing: <attempt_complete/>
148
+ // 3. Empty with closing tag: <attempt_complete></attempt_complete>
149
+ // 4. Incomplete: <attempt_complete (missing closing bracket)
150
+ // 5. With trailing content that should be ignored
151
+ const attemptCompletePatterns = [
152
+ // Standard shorthand with optional whitespace
153
+ /^<attempt_complete>\s*$/,
154
+ // Empty with proper closing tag (common case from the logs)
155
+ /^<attempt_complete>\s*<\/attempt_complete>\s*$/,
156
+ // Self-closing variant
157
+ /^<attempt_complete\s*\/>\s*$/,
158
+ // Incomplete opening tag (missing closing bracket)
159
+ /^<attempt_complete\s*$/,
160
+ // With trailing content (extract just the tag part) - must come after empty tag pattern
161
+ /^<attempt_complete>(.*)$/s,
162
+ // Self-closing with trailing content
163
+ /^<attempt_complete\s*\/>(.*)$/s
164
+ ];
165
+
166
+ for (const pattern of attemptCompletePatterns) {
167
+ const match = cleanedXmlString.match(pattern);
168
+ if (match) {
169
+ // Convert any form of attempt_complete to the standard format
170
+ return {
171
+ toolName: 'attempt_completion',
172
+ params: { result: '__PREVIOUS_RESPONSE__' }
173
+ };
174
+ }
175
+ }
176
+
177
+ // Additional recovery: check if the string contains attempt_complete anywhere
178
+ // and treat the entire response as a completion signal if no other tool tags are found
179
+ if (cleanedXmlString.includes('<attempt_complete') && !hasOtherToolTags(cleanedXmlString, validTools)) {
180
+ // This handles malformed cases where attempt_complete appears but is broken
148
181
  return {
149
- toolName: 'attempt_completion',
182
+ toolName: 'attempt_completion',
150
183
  params: { result: '__PREVIOUS_RESPONSE__' }
151
184
  };
152
185
  }
@@ -160,4 +193,23 @@ export function parseXmlToolCallWithThinking(xmlString, validTools) {
160
193
  }
161
194
 
162
195
  return parsedTool;
196
+ }
197
+
198
+ /**
199
+ * Helper function to check if the XML string contains other tool tags
200
+ * @param {string} xmlString - The XML string to check
201
+ * @param {string[]} validTools - List of valid tool names
202
+ * @returns {boolean} - True if other tool tags are found
203
+ */
204
+ function hasOtherToolTags(xmlString, validTools = []) {
205
+ const defaultTools = ['search', 'query', 'extract', 'listFiles', 'searchFiles', 'implement', 'attempt_completion'];
206
+ const toolsToCheck = validTools.length > 0 ? validTools : defaultTools;
207
+
208
+ // Check for any tool tags other than attempt_complete variants
209
+ for (const tool of toolsToCheck) {
210
+ if (tool !== 'attempt_completion' && xmlString.includes(`<${tool}`)) {
211
+ return true;
212
+ }
213
+ }
214
+ return false;
163
215
  }
package/src/utils.js CHANGED
@@ -43,7 +43,7 @@ export async function getBinaryPath(options = {}) {
43
43
 
44
44
  // Check bin directory
45
45
  const isWindows = process.platform === 'win32';
46
- const binaryName = isWindows ? 'probe.exe' : 'probe';
46
+ const binaryName = isWindows ? 'probe.exe' : 'probe-binary';
47
47
  const binaryPath = path.join(binDir, binaryName);
48
48
 
49
49
  if (fs.existsSync(binaryPath) && !forceDownload) {