@probelabs/probe 0.6.0-rc228 → 0.6.0-rc230

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.
@@ -58,6 +58,8 @@ import {
58
58
  implementToolDefinition,
59
59
  editToolDefinition,
60
60
  createToolDefinition,
61
+ googleSearchToolDefinition,
62
+ urlContextToolDefinition,
61
63
  attemptCompletionSchema,
62
64
  parseXmlToolCallWithThinking
63
65
  } from './tools.js';
@@ -404,6 +406,10 @@ export class ProbeAgent {
404
406
  // Initialize the AI model
405
407
  this.initializeModel();
406
408
 
409
+ // Gemini built-in tools (provider-defined, server-side)
410
+ // These are enabled automatically when the provider is Google
411
+ this._geminiToolsEnabled = this._initializeGeminiBuiltinTools();
412
+
407
413
  // Note: MCP initialization is now done in initialize() method
408
414
  // Constructor must remain synchronous for backward compatibility
409
415
  }
@@ -1320,6 +1326,15 @@ export class ProbeAgent {
1320
1326
  abortSignal: controller.signal
1321
1327
  };
1322
1328
 
1329
+ // Strip Gemini provider-defined tools when falling back to non-Google provider
1330
+ // These tools have no execute function and would cause errors on other providers
1331
+ if (config.provider !== 'google' && fallbackOptions.tools) {
1332
+ delete fallbackOptions.tools;
1333
+ if (this.debug) {
1334
+ console.error(`[DEBUG] Stripped Gemini built-in tools for fallback to ${config.provider} provider`);
1335
+ }
1336
+ }
1337
+
1323
1338
  const providerRetryManager = new RetryManager({
1324
1339
  maxRetries: config.maxRetries ?? this.retryConfig.maxRetries ?? 3,
1325
1340
  initialDelay: this.retryConfig.initialDelay ?? 1000,
@@ -1442,6 +1457,83 @@ export class ProbeAgent {
1442
1457
  }
1443
1458
  }
1444
1459
 
1460
+ /**
1461
+ * Initialize Gemini built-in tools (gemini_google_search, gemini_url_context).
1462
+ * These are provider-defined tools that execute server-side on Google's infrastructure.
1463
+ * They are only available when the provider is Google Gemini.
1464
+ * @returns {{ googleSearch: boolean, urlContext: boolean }} Which tools were enabled
1465
+ * @private
1466
+ */
1467
+ _initializeGeminiBuiltinTools() {
1468
+ const isToolAllowed = (toolName) => this.allowedTools.isEnabled(toolName);
1469
+ const result = { googleSearch: false, urlContext: false };
1470
+
1471
+ if (this.apiType !== 'google') {
1472
+ // Log info about unavailability for non-Google providers
1473
+ if (isToolAllowed('gemini_google_search') || isToolAllowed('gemini_url_context')) {
1474
+ if (this.debug) {
1475
+ console.error(`[DEBUG] Gemini built-in tools (gemini_google_search, gemini_url_context) are not available: provider is '${this.apiType}', not 'google'. These tools require the Google Gemini provider.`);
1476
+ }
1477
+ }
1478
+ return result;
1479
+ }
1480
+
1481
+ // Check SDK support
1482
+ if (!this.provider || !this.provider.tools) {
1483
+ console.error('[ProbeAgent] Gemini built-in tools unavailable: @ai-sdk/google does not expose provider.tools. Upgrade to @ai-sdk/google v2.0.14+.');
1484
+ return result;
1485
+ }
1486
+
1487
+ if (isToolAllowed('gemini_google_search')) {
1488
+ result.googleSearch = true;
1489
+ if (this.debug) {
1490
+ console.error('[DEBUG] Gemini built-in tool enabled: gemini_google_search');
1491
+ }
1492
+ }
1493
+
1494
+ if (isToolAllowed('gemini_url_context')) {
1495
+ result.urlContext = true;
1496
+ if (this.debug) {
1497
+ console.error('[DEBUG] Gemini built-in tool enabled: gemini_url_context');
1498
+ }
1499
+ }
1500
+
1501
+ return result;
1502
+ }
1503
+
1504
+ /**
1505
+ * Build Gemini provider-defined tools object for streamText().
1506
+ * Returns undefined if no Gemini tools are enabled.
1507
+ * @returns {Object|undefined}
1508
+ * @private
1509
+ */
1510
+ _buildGeminiProviderTools() {
1511
+ if (this.apiType !== 'google' || !this._geminiToolsEnabled) {
1512
+ return undefined;
1513
+ }
1514
+
1515
+ const { googleSearch, urlContext } = this._geminiToolsEnabled;
1516
+ if (!googleSearch && !urlContext) {
1517
+ return undefined;
1518
+ }
1519
+
1520
+ if (!this.provider || !this.provider.tools) {
1521
+ return undefined;
1522
+ }
1523
+
1524
+ const tools = {};
1525
+ const providerTools = this.provider.tools;
1526
+
1527
+ if (googleSearch && providerTools.googleSearch) {
1528
+ tools.google_search = providerTools.googleSearch({});
1529
+ }
1530
+ if (urlContext && providerTools.urlContext) {
1531
+ tools.url_context = providerTools.urlContext({});
1532
+ }
1533
+
1534
+ return Object.keys(tools).length > 0 ? tools : undefined;
1535
+ }
1536
+
1445
1537
  /**
1446
1538
  * Initialize AWS Bedrock model
1447
1539
  */
@@ -2420,6 +2512,14 @@ ${extractGuidance}
2420
2512
  toolDefinitions += `${analyzeAllToolDefinition}\n`;
2421
2513
  }
2422
2514
 
2515
+ // Gemini built-in tools (only when using Google provider)
2516
+ if (this._geminiToolsEnabled?.googleSearch && isToolAllowed('gemini_google_search')) {
2517
+ toolDefinitions += `${googleSearchToolDefinition}\n`;
2518
+ }
2519
+ if (this._geminiToolsEnabled?.urlContext && isToolAllowed('gemini_url_context')) {
2520
+ toolDefinitions += `${urlContextToolDefinition}\n`;
2521
+ }
2522
+
2423
2523
  // Build XML tool guidelines with dynamic examples based on allowed tools
2424
2524
  // Build examples only for allowed tools
2425
2525
  let toolExamples = '';
@@ -2497,6 +2597,12 @@ The configuration is loaded from src/config.js lines 15-25 which contains the da
2497
2597
  availableToolsList += '- attempt_completion: Finalize the task and provide the result to the user.\n';
2498
2598
  availableToolsList += '- attempt_complete: Quick completion using previous response (shorthand).\n';
2499
2599
  }
2600
+ if (this._geminiToolsEnabled?.googleSearch && isToolAllowed('gemini_google_search')) {
2601
+ availableToolsList += '- gemini_google_search: (auto) Web search via Google — invoked automatically by the model when it needs current information.\n';
2602
+ }
2603
+ if (this._geminiToolsEnabled?.urlContext && isToolAllowed('gemini_url_context')) {
2604
+ availableToolsList += '- gemini_url_context: (auto) URL content reader via Google — automatically fetches and reads URLs mentioned in the conversation.\n';
2605
+ }
2500
2606
 
2501
2607
  let xmlToolGuidelines = `
2502
2608
  # Tool Use Formatting
@@ -3049,12 +3155,21 @@ Follow these instructions carefully:
3049
3155
  // Prepare messages with potential image content
3050
3156
  const messagesForAI = this.prepareMessagesWithImages(currentMessages);
3051
3157
 
3052
- const result = await this.streamTextWithRetryAndFallback({
3158
+ // Build streamText options, including Gemini provider-defined tools if applicable
3159
+ const streamOptions = {
3053
3160
  model: this.provider ? this.provider(this.model) : this.model,
3054
3161
  messages: messagesForAI,
3055
3162
  maxTokens: maxResponseTokens,
3056
3163
  temperature: 0.3,
3057
- });
3164
+ };
3165
+
3166
+ // Inject Gemini built-in tools (gemini_google_search, gemini_url_context) when using Google provider
3167
+ const geminiProviderTools = this._buildGeminiProviderTools();
3168
+ if (geminiProviderTools) {
3169
+ streamOptions.tools = geminiProviderTools;
3170
+ }
3171
+
3172
+ const result = await this.streamTextWithRetryAndFallback(streamOptions);
3058
3173
 
3059
3174
  // Get the promise reference BEFORE consuming stream (doesn't lock it)
3060
3175
  const usagePromise = result.usage;
@@ -3585,6 +3700,12 @@ Follow these instructions carefully:
3585
3700
 
3586
3701
  let toolResultContent = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult, null, 2);
3587
3702
 
3703
+ // Convert absolute workspace paths to relative in tool results
3704
+ if (this.workspaceRoot && toolResultContent) {
3705
+ const wsPrefix = this.workspaceRoot.endsWith(sep) ? this.workspaceRoot : this.workspaceRoot + sep;
3706
+ toolResultContent = toolResultContent.split(wsPrefix).join('');
3707
+ }
3708
+
3588
3709
  // Truncate if output exceeds token limit
3589
3710
  try {
3590
3711
  const truncateResult = await truncateIfNeeded(toolResultContent, this.tokenCounter, this.sessionId, this.maxOutputTokens);
@@ -3314,6 +3314,29 @@ async function validateCwdPath(inputPath, defaultPath = process.cwd()) {
3314
3314
  try {
3315
3315
  const stats = await fs5.stat(normalizedPath);
3316
3316
  if (!stats.isDirectory()) {
3317
+ const resolvedPath = safeRealpath(normalizedPath);
3318
+ const dirPath = path4.dirname(resolvedPath);
3319
+ try {
3320
+ const dirStats = await fs5.stat(dirPath);
3321
+ if (dirStats.isDirectory()) {
3322
+ return safeRealpath(dirPath);
3323
+ }
3324
+ } catch (dirError) {
3325
+ if (dirError.code === "ENOENT") {
3326
+ throw new PathError(`Parent directory does not exist for file: ${normalizedPath}`, {
3327
+ suggestion: "The specified path is a file whose parent directory does not exist.",
3328
+ details: { path: normalizedPath, parentPath: dirPath, type: "file" }
3329
+ });
3330
+ }
3331
+ if (dirError.code === "EACCES") {
3332
+ throw new PathError(`Permission denied accessing parent directory: ${dirPath}`, {
3333
+ recoverable: false,
3334
+ suggestion: "Permission denied accessing the parent directory of the specified file.",
3335
+ details: { path: normalizedPath, parentPath: dirPath, type: "file" }
3336
+ });
3337
+ }
3338
+ throw dirError;
3339
+ }
3317
3340
  throw new PathError(`Path is not a directory: ${normalizedPath}`, {
3318
3341
  suggestion: "The specified path is a file, not a directory. Please provide a directory path for searching.",
3319
3342
  details: { path: normalizedPath, type: "file" }
@@ -9807,7 +9830,7 @@ function resolveTargetPath(target, cwd) {
9807
9830
  }
9808
9831
  return filePart + suffix;
9809
9832
  }
9810
- var searchSchema, querySchema, extractSchema, delegateSchema, listSkillsSchema, useSkillSchema, bashSchema, analyzeAllSchema, attemptCompletionSchema, searchToolDefinition, queryToolDefinition, extractToolDefinition, delegateToolDefinition, attemptCompletionToolDefinition, analyzeAllToolDefinition, bashToolDefinition, searchDescription, queryDescription, extractDescription, delegateDescription, analyzeAllDescription, DEFAULT_VALID_TOOLS;
9833
+ var searchSchema, querySchema, extractSchema, delegateSchema, listSkillsSchema, useSkillSchema, bashSchema, analyzeAllSchema, attemptCompletionSchema, searchToolDefinition, queryToolDefinition, extractToolDefinition, delegateToolDefinition, attemptCompletionToolDefinition, analyzeAllToolDefinition, bashToolDefinition, googleSearchToolDefinition, urlContextToolDefinition, searchDescription, queryDescription, extractDescription, delegateDescription, analyzeAllDescription, DEFAULT_VALID_TOOLS;
9811
9834
  var init_common = __esm({
9812
9835
  "src/tools/common.js"() {
9813
9836
  "use strict";
@@ -10163,6 +10186,28 @@ User: Check system info
10163
10186
  </bash>
10164
10187
 
10165
10188
  </examples>
10189
+ `;
10190
+ googleSearchToolDefinition = `
10191
+ ## gemini_google_search (Gemini Built-in)
10192
+ Description: Web search powered by Google. This is a built-in Gemini capability that automatically searches the web when the model needs current information. The model decides when to search and integrates results directly into its response with source citations.
10193
+
10194
+ This tool is invoked automatically by the model \u2014 you do NOT need to use XML tool calls for it. Simply ask questions that require up-to-date or real-world information and the model will search the web as needed.
10195
+
10196
+ Capabilities:
10197
+ - Real-time web search with grounded citations
10198
+ - Automatic query generation and result synthesis
10199
+ - Source attribution with URLs
10200
+ `;
10201
+ urlContextToolDefinition = `
10202
+ ## gemini_url_context (Gemini Built-in)
10203
+ Description: URL content reader powered by Google. This is a built-in Gemini capability that automatically fetches and analyzes the content of URLs mentioned in the conversation. When you include URLs in your message, the model can read and understand their content.
10204
+
10205
+ This tool is invoked automatically by the model \u2014 you do NOT need to use XML tool calls for it. Simply include URLs in your message and the model will fetch and analyze their content.
10206
+
10207
+ Capabilities:
10208
+ - Fetch and read web page content from URLs in the prompt
10209
+ - Supports up to 20 URLs per request
10210
+ - Processes HTML content (does not execute JavaScript)
10166
10211
  `;
10167
10212
  searchDescription = "Search code in the repository. Free-form questions are accepted, but Elasticsearch-style keyword queries work best. Use this tool first for any code-related questions.";
10168
10213
  queryDescription = "Search code using ast-grep structural pattern matching. Use this tool to find specific code structures like functions, classes, or methods.";
@@ -10288,6 +10333,7 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
10288
10333
  `Options: exact=${exact ? "true" : "false"}, language=${language || "auto"}, allow_tests=${allowTests ? "true" : "false"}.`,
10289
10334
  "",
10290
10335
  'Return ONLY valid JSON: {"targets": ["path/to/file.ext#Symbol", "path/to/file.ext:line", "path/to/file.ext:start-end"]}',
10336
+ 'IMPORTANT: Use ABSOLUTE file paths in targets (e.g., "/full/path/to/file.ext#Symbol"). If you only have relative paths, make them relative to the search path above.',
10291
10337
  "Prefer #Symbol when a function/class name is clear; otherwise use line numbers.",
10292
10338
  "Deduplicate targets. Do NOT explain or answer - ONLY return the JSON targets."
10293
10339
  ].join("\n");
@@ -10409,11 +10455,11 @@ var init_vercel = __esm({
10409
10455
  }
10410
10456
  return await runRawSearch();
10411
10457
  }
10412
- const effectiveCwd = options.cwd || ".";
10413
- const resolvedTargets = targets.map((target) => resolveTargetPath(target, effectiveCwd));
10458
+ const resolutionBase = searchPaths[0] || options.cwd || ".";
10459
+ const resolvedTargets = targets.map((target) => resolveTargetPath(target, resolutionBase));
10414
10460
  const extractOptions = {
10415
10461
  files: resolvedTargets,
10416
- cwd: effectiveCwd,
10462
+ cwd: resolutionBase,
10417
10463
  allowTests: allow_tests ?? true
10418
10464
  };
10419
10465
  if (outline) {
@@ -59410,7 +59456,13 @@ var init_config = __esm({
59410
59456
  __filename4 = fileURLToPath6(import.meta.url);
59411
59457
  __dirname4 = dirname3(__filename4);
59412
59458
  DEFAULT_TIMEOUT = 3e4;
59413
- MAX_TIMEOUT = 6e5;
59459
+ MAX_TIMEOUT = (() => {
59460
+ if (process.env.MCP_MAX_TIMEOUT) {
59461
+ const parsed = parseInt(process.env.MCP_MAX_TIMEOUT, 10);
59462
+ if (!isNaN(parsed) && parsed >= 3e4 && parsed <= 72e5) return parsed;
59463
+ }
59464
+ return 18e5;
59465
+ })();
59414
59466
  DEFAULT_CONFIG = {
59415
59467
  mcpServers: {
59416
59468
  // Example probe server configuration
@@ -70515,6 +70567,7 @@ var init_ProbeAgent = __esm({
70515
70567
  this.fallbackManager = null;
70516
70568
  this.engine = null;
70517
70569
  this.initializeModel();
70570
+ this._geminiToolsEnabled = this._initializeGeminiBuiltinTools();
70518
70571
  }
70519
70572
  /**
70520
70573
  * Parse allowedTools configuration
@@ -71247,6 +71300,12 @@ var init_ProbeAgent = __esm({
71247
71300
  model: provider(model),
71248
71301
  abortSignal: controller.signal
71249
71302
  };
71303
+ if (config.provider !== "google" && fallbackOptions.tools) {
71304
+ delete fallbackOptions.tools;
71305
+ if (this.debug) {
71306
+ console.error(`[DEBUG] Stripped Gemini built-in tools for fallback to ${config.provider} provider`);
71307
+ }
71308
+ }
71250
71309
  const providerRetryManager = new RetryManager({
71251
71310
  maxRetries: config.maxRetries ?? this.retryConfig.maxRetries ?? 3,
71252
71311
  initialDelay: this.retryConfig.initialDelay ?? 1e3,
@@ -71350,6 +71409,69 @@ var init_ProbeAgent = __esm({
71350
71409
  console.log(`Using Google API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl})` : ""}`);
71351
71410
  }
71352
71411
  }
71412
+ /**
71413
+ * Initialize Gemini built-in tools (gemini_google_search, gemini_url_context).
71414
+ * These are provider-defined tools that execute server-side on Google's infrastructure.
71415
+ * They are only available when the provider is Google Gemini.
71416
+ * @returns {{ googleSearch: boolean, urlContext: boolean }} Which tools were enabled
71417
+ * @private
71418
+ */
71419
+ _initializeGeminiBuiltinTools() {
71420
+ const isToolAllowed = (toolName) => this.allowedTools.isEnabled(toolName);
71421
+ const result = { googleSearch: false, urlContext: false };
71422
+ if (this.apiType !== "google") {
71423
+ if (isToolAllowed("gemini_google_search") || isToolAllowed("gemini_url_context")) {
71424
+ if (this.debug) {
71425
+ console.error(`[DEBUG] Gemini built-in tools (gemini_google_search, gemini_url_context) are not available: provider is '${this.apiType}', not 'google'. These tools require the Google Gemini provider.`);
71426
+ }
71427
+ }
71428
+ return result;
71429
+ }
71430
+ if (!this.provider || !this.provider.tools) {
71431
+ console.error("[ProbeAgent] Gemini built-in tools unavailable: @ai-sdk/google does not expose provider.tools. Upgrade to @ai-sdk/google v2.0.14+.");
71432
+ return result;
71433
+ }
71434
+ if (isToolAllowed("gemini_google_search")) {
71435
+ result.googleSearch = true;
71436
+ if (this.debug) {
71437
+ console.error("[DEBUG] Gemini built-in tool enabled: gemini_google_search");
71438
+ }
71439
+ }
71440
+ if (isToolAllowed("gemini_url_context")) {
71441
+ result.urlContext = true;
71442
+ if (this.debug) {
71443
+ console.error("[DEBUG] Gemini built-in tool enabled: gemini_url_context");
71444
+ }
71445
+ }
71446
+ return result;
71447
+ }
71448
+ /**
71449
+ * Build Gemini provider-defined tools object for streamText().
71450
+ * Returns undefined if no Gemini tools are enabled.
71451
+ * @returns {Object|undefined}
71452
+ * @private
71453
+ */
71454
+ _buildGeminiProviderTools() {
71455
+ if (this.apiType !== "google" || !this._geminiToolsEnabled) {
71456
+ return void 0;
71457
+ }
71458
+ const { googleSearch, urlContext } = this._geminiToolsEnabled;
71459
+ if (!googleSearch && !urlContext) {
71460
+ return void 0;
71461
+ }
71462
+ if (!this.provider || !this.provider.tools) {
71463
+ return void 0;
71464
+ }
71465
+ const tools2 = {};
71466
+ const providerTools = this.provider.tools;
71467
+ if (googleSearch && providerTools.googleSearch) {
71468
+ tools2.google_search = providerTools.googleSearch({});
71469
+ }
71470
+ if (urlContext && providerTools.urlContext) {
71471
+ tools2.url_context = providerTools.urlContext({});
71472
+ }
71473
+ return Object.keys(tools2).length > 0 ? tools2 : void 0;
71474
+ }
71353
71475
  /**
71354
71476
  * Initialize AWS Bedrock model
71355
71477
  */
@@ -72138,6 +72260,14 @@ Workspace: ${this.allowedFolders.join(", ")}`;
72138
72260
  }
72139
72261
  if (isToolAllowed("analyze_all")) {
72140
72262
  toolDefinitions += `${analyzeAllToolDefinition}
72263
+ `;
72264
+ }
72265
+ if (this._geminiToolsEnabled?.googleSearch && isToolAllowed("gemini_google_search")) {
72266
+ toolDefinitions += `${googleSearchToolDefinition}
72267
+ `;
72268
+ }
72269
+ if (this._geminiToolsEnabled?.urlContext && isToolAllowed("gemini_url_context")) {
72270
+ toolDefinitions += `${urlContextToolDefinition}
72141
72271
  `;
72142
72272
  }
72143
72273
  let toolExamples = "";
@@ -72214,6 +72344,12 @@ The configuration is loaded from src/config.js lines 15-25 which contains the da
72214
72344
  availableToolsList += "- attempt_completion: Finalize the task and provide the result to the user.\n";
72215
72345
  availableToolsList += "- attempt_complete: Quick completion using previous response (shorthand).\n";
72216
72346
  }
72347
+ if (this._geminiToolsEnabled?.googleSearch && isToolAllowed("gemini_google_search")) {
72348
+ availableToolsList += "- gemini_google_search: (auto) Web search via Google \u2014 invoked automatically by the model when it needs current information.\n";
72349
+ }
72350
+ if (this._geminiToolsEnabled?.urlContext && isToolAllowed("gemini_url_context")) {
72351
+ availableToolsList += "- gemini_url_context: (auto) URL content reader via Google \u2014 automatically fetches and reads URLs mentioned in the conversation.\n";
72352
+ }
72217
72353
  let xmlToolGuidelines = `
72218
72354
  # Tool Use Formatting
72219
72355
 
@@ -72664,12 +72800,17 @@ You are working with a workspace. Available paths: ${workspaceDesc}
72664
72800
  try {
72665
72801
  const executeAIRequest = async () => {
72666
72802
  const messagesForAI = this.prepareMessagesWithImages(currentMessages);
72667
- const result = await this.streamTextWithRetryAndFallback({
72803
+ const streamOptions = {
72668
72804
  model: this.provider ? this.provider(this.model) : this.model,
72669
72805
  messages: messagesForAI,
72670
72806
  maxTokens: maxResponseTokens,
72671
72807
  temperature: 0.3
72672
- });
72808
+ };
72809
+ const geminiProviderTools = this._buildGeminiProviderTools();
72810
+ if (geminiProviderTools) {
72811
+ streamOptions.tools = geminiProviderTools;
72812
+ }
72813
+ const result = await this.streamTextWithRetryAndFallback(streamOptions);
72673
72814
  const usagePromise = result.usage;
72674
72815
  for await (const delta of result.textStream) {
72675
72816
  assistantResponseContent += delta;
@@ -73059,6 +73200,10 @@ ${errorXml}
73059
73200
  }
73060
73201
  currentMessages.push({ role: "assistant", content: assistantResponseContent });
73061
73202
  let toolResultContent = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult, null, 2);
73203
+ if (this.workspaceRoot && toolResultContent) {
73204
+ const wsPrefix = this.workspaceRoot.endsWith(sep5) ? this.workspaceRoot : this.workspaceRoot + sep5;
73205
+ toolResultContent = toolResultContent.split(wsPrefix).join("");
73206
+ }
73062
73207
  try {
73063
73208
  const truncateResult = await truncateIfNeeded(toolResultContent, this.tokenCounter, this.sessionId, this.maxOutputTokens);
73064
73209
  if (truncateResult.truncated) {
@@ -15,7 +15,13 @@ const __dirname = dirname(__filename);
15
15
  * Timeout configuration constants
16
16
  */
17
17
  export const DEFAULT_TIMEOUT = 30000; // 30 seconds
18
- export const MAX_TIMEOUT = 600000; // 10 minutes max to prevent resource exhaustion
18
+ export const MAX_TIMEOUT = (() => {
19
+ if (process.env.MCP_MAX_TIMEOUT) {
20
+ const parsed = parseInt(process.env.MCP_MAX_TIMEOUT, 10);
21
+ if (!isNaN(parsed) && parsed >= 30000 && parsed <= 7200000) return parsed;
22
+ }
23
+ return 1800000; // 30 minutes default - workflow tools (code checkouts, AI exploration) need time
24
+ })();
19
25
 
20
26
  /**
21
27
  * Validate and normalize a timeout value
@@ -27,6 +27,8 @@ import {
27
27
  bashToolDefinition,
28
28
  editToolDefinition,
29
29
  createToolDefinition,
30
+ googleSearchToolDefinition,
31
+ urlContextToolDefinition,
30
32
  parseXmlToolCall
31
33
  } from '../index.js';
32
34
  import { randomUUID } from 'crypto';
@@ -108,6 +110,8 @@ export {
108
110
  editToolDefinition,
109
111
  createToolDefinition,
110
112
  attemptCompletionToolDefinition,
113
+ googleSearchToolDefinition,
114
+ urlContextToolDefinition,
111
115
  parseXmlToolCall
112
116
  };
113
117
 
package/build/index.js CHANGED
@@ -35,6 +35,8 @@ import {
35
35
  analyzeAllToolDefinition,
36
36
  attemptCompletionToolDefinition,
37
37
  bashToolDefinition,
38
+ googleSearchToolDefinition,
39
+ urlContextToolDefinition,
38
40
  parseXmlToolCall
39
41
  } from './tools/common.js';
40
42
  import {
@@ -114,6 +116,8 @@ export {
114
116
  bashToolDefinition,
115
117
  editToolDefinition,
116
118
  createToolDefinition,
119
+ googleSearchToolDefinition,
120
+ urlContextToolDefinition,
117
121
  // Export parser function
118
122
  parseXmlToolCall,
119
123
  // Export task management
@@ -386,6 +386,30 @@ User: Check system info
386
386
  </examples>
387
387
  `;
388
388
 
389
+ export const googleSearchToolDefinition = `
390
+ ## gemini_google_search (Gemini Built-in)
391
+ Description: Web search powered by Google. This is a built-in Gemini capability that automatically searches the web when the model needs current information. The model decides when to search and integrates results directly into its response with source citations.
392
+
393
+ This tool is invoked automatically by the model — you do NOT need to use XML tool calls for it. Simply ask questions that require up-to-date or real-world information and the model will search the web as needed.
394
+
395
+ Capabilities:
396
+ - Real-time web search with grounded citations
397
+ - Automatic query generation and result synthesis
398
+ - Source attribution with URLs
399
+ `;
400
+
401
+ export const urlContextToolDefinition = `
402
+ ## gemini_url_context (Gemini Built-in)
403
+ Description: URL content reader powered by Google. This is a built-in Gemini capability that automatically fetches and analyzes the content of URLs mentioned in the conversation. When you include URLs in your message, the model can read and understand their content.
404
+
405
+ This tool is invoked automatically by the model — you do NOT need to use XML tool calls for it. Simply include URLs in your message and the model will fetch and analyze their content.
406
+
407
+ Capabilities:
408
+ - Fetch and read web page content from URLs in the prompt
409
+ - Supports up to 20 URLs per request
410
+ - Processes HTML content (does not execute JavaScript)
411
+ `;
412
+
389
413
  export const searchDescription = 'Search code in the repository. Free-form questions are accepted, but Elasticsearch-style keyword queries work best. Use this tool first for any code-related questions.';
390
414
  export const queryDescription = 'Search code using ast-grep structural pattern matching. Use this tool to find specific code structures like functions, classes, or methods.';
391
415
  export const extractDescription = 'Extract code blocks from files based on file paths and optional line numbers. Use this tool to see complete context after finding relevant files.';
@@ -140,6 +140,7 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
140
140
  `Options: exact=${exact ? 'true' : 'false'}, language=${language || 'auto'}, allow_tests=${allowTests ? 'true' : 'false'}.`,
141
141
  '',
142
142
  'Return ONLY valid JSON: {"targets": ["path/to/file.ext#Symbol", "path/to/file.ext:line", "path/to/file.ext:start-end"]}',
143
+ 'IMPORTANT: Use ABSOLUTE file paths in targets (e.g., "/full/path/to/file.ext#Symbol"). If you only have relative paths, make them relative to the search path above.',
143
144
  'Prefer #Symbol when a function/class name is clear; otherwise use line numbers.',
144
145
  'Deduplicate targets. Do NOT explain or answer - ONLY return the JSON targets.'
145
146
  ].join('\n');
@@ -267,11 +268,14 @@ export const searchTool = (options = {}) => {
267
268
  return await runRawSearch();
268
269
  }
269
270
 
270
- const effectiveCwd = options.cwd || '.';
271
- const resolvedTargets = targets.map(target => resolveTargetPath(target, effectiveCwd));
271
+ // Resolve relative paths against the actual search directory, not the general cwd.
272
+ // The delegate returns paths relative to where the search was performed (searchPaths[0]),
273
+ // which may differ from options.cwd when the user specifies a path parameter.
274
+ const resolutionBase = searchPaths[0] || options.cwd || '.';
275
+ const resolvedTargets = targets.map(target => resolveTargetPath(target, resolutionBase));
272
276
  const extractOptions = {
273
277
  files: resolvedTargets,
274
- cwd: effectiveCwd,
278
+ cwd: resolutionBase,
275
279
  allowTests: allow_tests ?? true
276
280
  };
277
281
 
@@ -55,9 +55,9 @@ export function safeRealpath(inputPath) {
55
55
  * - Does NOT restrict access to specific directories (that's the responsibility
56
56
  * of higher-level components like ProbeAgent with allowedFolders)
57
57
  *
58
- * @param {string} inputPath - The path to validate
58
+ * @param {string} inputPath - The path to validate (can be a file or directory; file paths are resolved to their parent directory)
59
59
  * @param {string} [defaultPath] - Default path to use if inputPath is not provided
60
- * @returns {Promise<string>} Normalized absolute path
60
+ * @returns {Promise<string>} Normalized absolute directory path. If inputPath is a file, returns its parent directory.
61
61
  * @throws {PathError} If the path is invalid or doesn't exist
62
62
  */
63
63
  export async function validateCwdPath(inputPath, defaultPath = process.cwd()) {
@@ -72,6 +72,32 @@ export async function validateCwdPath(inputPath, defaultPath = process.cwd()) {
72
72
  try {
73
73
  const stats = await fs.stat(normalizedPath);
74
74
  if (!stats.isDirectory()) {
75
+ // If the path is a file, resolve to its parent directory
76
+ // This handles cases where a file path is passed as cwd
77
+ // Use safeRealpath to resolve symlinks before extracting parent directory
78
+ const resolvedPath = safeRealpath(normalizedPath);
79
+ const dirPath = path.dirname(resolvedPath);
80
+ try {
81
+ const dirStats = await fs.stat(dirPath);
82
+ if (dirStats.isDirectory()) {
83
+ return safeRealpath(dirPath);
84
+ }
85
+ } catch (dirError) {
86
+ if (dirError.code === 'ENOENT') {
87
+ throw new PathError(`Parent directory does not exist for file: ${normalizedPath}`, {
88
+ suggestion: 'The specified path is a file whose parent directory does not exist.',
89
+ details: { path: normalizedPath, parentPath: dirPath, type: 'file' }
90
+ });
91
+ }
92
+ if (dirError.code === 'EACCES') {
93
+ throw new PathError(`Permission denied accessing parent directory: ${dirPath}`, {
94
+ recoverable: false,
95
+ suggestion: 'Permission denied accessing the parent directory of the specified file.',
96
+ details: { path: normalizedPath, parentPath: dirPath, type: 'file' }
97
+ });
98
+ }
99
+ throw dirError;
100
+ }
75
101
  throw new PathError(`Path is not a directory: ${normalizedPath}`, {
76
102
  suggestion: 'The specified path is a file, not a directory. Please provide a directory path for searching.',
77
103
  details: { path: normalizedPath, type: 'file' }