@probelabs/probe 0.6.0-rc305 → 0.6.0-rc307

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.
@@ -38,7 +38,7 @@ import { TokenCounter } from './tokenCounter.js';
38
38
  import { truncateForSpan } from './simpleTelemetry.js';
39
39
  import { InMemoryStorageAdapter } from './storage/InMemoryStorageAdapter.js';
40
40
  import { HookManager, HOOK_TYPES } from './hooks/HookManager.js';
41
- import { SUPPORTED_IMAGE_EXTENSIONS, IMAGE_MIME_TYPES, isFormatSupportedByProvider } from './imageConfig.js';
41
+ import { SUPPORTED_IMAGE_EXTENSIONS, SUPPORTED_MEDIA_EXTENSIONS, MEDIA_MIME_TYPES, IMAGE_MIME_TYPES, isFormatSupportedByProvider, isImageExtension, isDocumentExtension } from './mediaConfig.js';
42
42
  import {
43
43
  createTools,
44
44
  searchSchema,
@@ -55,6 +55,7 @@ import {
55
55
  listFilesSchema,
56
56
  searchFilesSchema,
57
57
  readImageSchema,
58
+ readMediaSchema,
58
59
  listSkillsSchema,
59
60
  useSkillSchema
60
61
  } from './tools.js';
@@ -121,6 +122,8 @@ const MAX_HISTORY_MESSAGES = 100;
121
122
 
122
123
  // Maximum image file size (20MB) to prevent OOM attacks
123
124
  const MAX_IMAGE_FILE_SIZE = 20 * 1024 * 1024;
125
+ // Maximum document file size (32MB) — Claude's limit is 32MB, OpenAI 50MB, Gemini 50MB
126
+ const MAX_DOCUMENT_FILE_SIZE = 32 * 1024 * 1024;
124
127
 
125
128
  /**
126
129
  * Truncate a string for debug logging, showing first and last portion.
@@ -882,6 +885,18 @@ export class ProbeAgent {
882
885
  outputBuffer: this._outputBuffer,
883
886
  concurrencyLimiter: this.concurrencyLimiter, // Global AI concurrency limiter
884
887
  isToolAllowed,
888
+ // MCP config for delegate subagents — these are set in constructor before tools init,
889
+ // so they're available here. The delegate tool closure needs them to pass to subagents
890
+ // so they can create their own MCPXmlBridge instances.
891
+ enableMcp: this.enableMcp,
892
+ mcpConfig: this.mcpConfig,
893
+ mcpConfigPath: this.mcpConfigPath,
894
+ // Pass parent's prompt settings so delegate subagents inherit the same persona/capabilities.
895
+ // Without promptType, delegate() defaulted to 'code-researcher' which doesn't exist,
896
+ // causing fallback to the read-only 'code-explorer' prompt.
897
+ promptType: this.promptType,
898
+ customPrompt: this.customPrompt,
899
+ completionPrompt: this.completionPrompt,
885
900
  // Lazy MCP getters — MCP is initialized after tools are created, so we use
886
901
  // getter functions that resolve at call-time to get the current MCP state
887
902
  getMcpBridge: () => this.mcpBridge,
@@ -946,40 +961,43 @@ export class ProbeAgent {
946
961
  }
947
962
  }
948
963
 
949
- // Image loading tool
950
- if (isToolAllowed('readImage')) {
951
- this.toolImplementations.readImage = {
952
- execute: async (params) => {
953
- const imagePath = params.path;
954
- if (!imagePath) {
955
- throw new Error('Image path is required');
956
- }
964
+ // Media loading tool (images + PDFs)
965
+ const readMediaExecute = async (params) => {
966
+ const mediaPath = params.path;
967
+ if (!mediaPath) {
968
+ throw new Error('File path is required');
969
+ }
957
970
 
958
- // Validate extension before attempting to load
959
- // Use basename to prevent path traversal attacks (e.g., 'malicious.jpg/../../../etc/passwd')
960
- const filename = basename(imagePath);
961
- const extension = filename.toLowerCase().split('.').pop();
971
+ // Validate extension before attempting to load
972
+ // Use basename to prevent path traversal attacks (e.g., 'malicious.jpg/../../../etc/passwd')
973
+ const filename = basename(mediaPath);
974
+ const extension = filename.toLowerCase().split('.').pop();
962
975
 
963
- // Always validate extension is in allowed list (defense-in-depth)
964
- if (!extension || !SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
965
- throw new Error(`Invalid or unsupported image extension: ${extension}. Supported formats: ${SUPPORTED_IMAGE_EXTENSIONS.join(', ')}`);
966
- }
976
+ // Always validate extension is in allowed list (defense-in-depth)
977
+ if (!extension || !SUPPORTED_MEDIA_EXTENSIONS.includes(extension)) {
978
+ throw new Error(`Unsupported file format: ${extension}. Supported formats: ${SUPPORTED_MEDIA_EXTENSIONS.join(', ')}`);
979
+ }
967
980
 
968
- // Check provider-specific format restrictions (e.g., SVG not supported by Google Gemini)
969
- if (this.apiType && !isFormatSupportedByProvider(extension, this.apiType)) {
970
- throw new Error(`Image format '${extension}' is not supported by the current AI provider (${this.apiType}). Try using a different image format like PNG or JPEG.`);
971
- }
981
+ // Check provider-specific format restrictions (e.g., SVG not supported by Google Gemini)
982
+ if (this.apiType && !isFormatSupportedByProvider(extension, this.apiType)) {
983
+ throw new Error(`File format '${extension}' is not supported by the current AI provider (${this.apiType}). Try converting to a different format.`);
984
+ }
972
985
 
973
- // Load the image using the existing loadImageIfValid method
974
- const loaded = await this.loadImageIfValid(imagePath);
986
+ // Load the media file
987
+ const loaded = await this.loadMediaIfValid(mediaPath);
975
988
 
976
- if (!loaded) {
977
- throw new Error(`Failed to load image: ${imagePath}. The file may not exist, be too large, have an unsupported format, or be outside allowed directories.`);
978
- }
989
+ if (!loaded) {
990
+ throw new Error(`Failed to load file: ${mediaPath}. The file may not exist, be too large, have an unsupported format, or be outside allowed directories.`);
991
+ }
979
992
 
980
- return `Image loaded successfully: ${imagePath}. The image is now available for analysis in the conversation.`;
981
- }
982
- };
993
+ const mediaType = isDocumentExtension(extension) ? 'Document' : 'Image';
994
+ return `${mediaType} loaded successfully: ${mediaPath}. The file is now available for analysis in the conversation.`;
995
+ };
996
+
997
+ if (isToolAllowed('readMedia') || isToolAllowed('readImage')) {
998
+ this.toolImplementations.readMedia = { execute: readMediaExecute };
999
+ // Keep readImage as backward-compatible alias
1000
+ this.toolImplementations.readImage = { execute: readMediaExecute };
983
1001
  }
984
1002
 
985
1003
  // Add bash tool if enabled and allowed
@@ -2207,9 +2225,9 @@ export class ProbeAgent {
2207
2225
  schema: searchFilesSchema,
2208
2226
  description: 'Find files matching a glob pattern with recursive search capability.'
2209
2227
  },
2210
- readImage: {
2211
- schema: readImageSchema,
2212
- description: 'Read and load an image file for AI analysis.'
2228
+ readMedia: {
2229
+ schema: readMediaSchema,
2230
+ description: 'Read and load a media file (image or PDF document) for AI analysis. Supports: png, jpg, jpeg, webp, bmp, svg, pdf.'
2213
2231
  },
2214
2232
  listSkills: {
2215
2233
  schema: listSkillsSchema,
@@ -2371,7 +2389,7 @@ export class ProbeAgent {
2371
2389
 
2372
2390
  // Enhanced pattern to detect image file mentions in various contexts
2373
2391
  // Looks for: "image", "file", "screenshot", etc. followed by path-like strings with image extensions
2374
- const extensionsPattern = `(?:${SUPPORTED_IMAGE_EXTENSIONS.join('|')})`;
2392
+ const extensionsPattern = `(?:${SUPPORTED_MEDIA_EXTENSIONS.join('|')})`;
2375
2393
  const imagePatterns = [
2376
2394
  // Direct file path mentions: "./screenshot.png", "/path/to/image.jpg", etc.
2377
2395
  new RegExp(`(?:\\.?\\.\\/)?[^\\s"'<>\\[\\]]+\\\.${extensionsPattern}(?!\\w)`, 'gi'),
@@ -2492,43 +2510,36 @@ export class ProbeAgent {
2492
2510
  }
2493
2511
 
2494
2512
  /**
2495
- * Load and cache an image if it's valid and accessible
2496
- * @param {string} imagePath - Path to the image file
2497
- * @returns {Promise<boolean>} - True if image was loaded successfully
2513
+ * Load and cache a media file (image or PDF) if it's valid and accessible
2514
+ * @param {string} mediaPath - Path to the media file
2515
+ * @returns {Promise<boolean>} - True if file was loaded successfully
2498
2516
  */
2499
- async loadImageIfValid(imagePath) {
2517
+ async loadMediaIfValid(mediaPath) {
2500
2518
  try {
2501
2519
  // Skip if already loaded
2502
- if (this.pendingImages.has(imagePath)) {
2520
+ if (this.pendingImages.has(mediaPath)) {
2503
2521
  if (this.debug) {
2504
- console.log(`[DEBUG] Image already loaded: ${imagePath}`);
2522
+ console.log(`[DEBUG] Media already loaded: ${mediaPath}`);
2505
2523
  }
2506
2524
  return true;
2507
2525
  }
2508
2526
 
2509
2527
  // Security validation: check if path is within any allowed directory
2510
- // Use safeRealpath() to resolve symlinks and handle path traversal attempts (e.g., '/allowed/../etc/passwd')
2511
- // This prevents symlink bypass attacks (e.g., /tmp -> /private/tmp on macOS)
2512
2528
  const allowedDirs = this.allowedFolders && this.allowedFolders.length > 0 ? this.allowedFolders : [process.cwd()];
2513
2529
 
2514
2530
  let absolutePath;
2515
2531
  let isPathAllowed = false;
2516
2532
 
2517
- // If absolute path, check if it's within any allowed directory
2518
- if (isAbsolute(imagePath)) {
2519
- // Use safeRealpath to resolve symlinks for security
2520
- absolutePath = safeRealpath(resolve(imagePath));
2533
+ if (isAbsolute(mediaPath)) {
2534
+ absolutePath = safeRealpath(resolve(mediaPath));
2521
2535
  isPathAllowed = allowedDirs.some(dir => {
2522
2536
  const resolvedDir = safeRealpath(dir);
2523
- // Ensure the path is within the allowed directory (add separator to prevent prefix attacks)
2524
2537
  return absolutePath === resolvedDir || absolutePath.startsWith(resolvedDir + sep);
2525
2538
  });
2526
2539
  } else {
2527
- // For relative paths, try resolving against each allowed directory
2528
2540
  for (const dir of allowedDirs) {
2529
2541
  const resolvedDir = safeRealpath(dir);
2530
- const resolvedPath = safeRealpath(resolve(dir, imagePath));
2531
- // Ensure the resolved path is within the allowed directory
2542
+ const resolvedPath = safeRealpath(resolve(dir, mediaPath));
2532
2543
  if (resolvedPath === resolvedDir || resolvedPath.startsWith(resolvedDir + sep)) {
2533
2544
  absolutePath = resolvedPath;
2534
2545
  isPathAllowed = true;
@@ -2536,137 +2547,162 @@ export class ProbeAgent {
2536
2547
  }
2537
2548
  }
2538
2549
  }
2539
-
2540
- // Security check: ensure path is within at least one allowed directory
2550
+
2541
2551
  if (!isPathAllowed) {
2542
2552
  if (this.debug) {
2543
- console.log(`[DEBUG] Image path outside allowed directories: ${imagePath}`);
2553
+ console.log(`[DEBUG] Media path outside allowed directories: ${mediaPath}`);
2544
2554
  }
2545
2555
  return false;
2546
2556
  }
2547
2557
 
2548
- // Check if file exists and get file stats
2549
2558
  let fileStats;
2550
2559
  try {
2551
2560
  fileStats = await stat(absolutePath);
2552
2561
  } catch (error) {
2553
2562
  if (this.debug) {
2554
- console.log(`[DEBUG] Image file not found: ${absolutePath}`);
2563
+ console.log(`[DEBUG] Media file not found: ${absolutePath}`);
2555
2564
  }
2556
2565
  return false;
2557
2566
  }
2558
2567
 
2559
- // Validate file size to prevent OOM attacks
2560
- if (fileStats.size > MAX_IMAGE_FILE_SIZE) {
2568
+ const extension = absolutePath.toLowerCase().split('.').pop();
2569
+ if (!SUPPORTED_MEDIA_EXTENSIONS.includes(extension)) {
2561
2570
  if (this.debug) {
2562
- console.log(`[DEBUG] Image file too large: ${absolutePath} (${fileStats.size} bytes, max: ${MAX_IMAGE_FILE_SIZE})`);
2571
+ console.log(`[DEBUG] Unsupported media format: ${extension}`);
2563
2572
  }
2564
2573
  return false;
2565
2574
  }
2566
2575
 
2567
- // Validate file extension
2568
- const extension = absolutePath.toLowerCase().split('.').pop();
2569
- if (!SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
2576
+ // Apply size limit based on media type
2577
+ const maxSize = isDocumentExtension(extension) ? MAX_DOCUMENT_FILE_SIZE : MAX_IMAGE_FILE_SIZE;
2578
+ if (fileStats.size > maxSize) {
2570
2579
  if (this.debug) {
2571
- console.log(`[DEBUG] Unsupported image format: ${extension}`);
2580
+ console.log(`[DEBUG] Media file too large: ${absolutePath} (${fileStats.size} bytes, max: ${maxSize})`);
2572
2581
  }
2573
2582
  return false;
2574
2583
  }
2575
2584
 
2576
- // Note: Provider-specific format validation (e.g., SVG not supported by Google Gemini)
2577
- // is handled by the readImage tool which provides explicit error messages.
2578
- // loadImageIfValid is a lower-level method that only checks general format support.
2579
-
2580
- // Determine MIME type (from shared config)
2581
- const mimeType = IMAGE_MIME_TYPES[extension];
2582
-
2583
- // Read and encode file asynchronously
2585
+ const mimeType = MEDIA_MIME_TYPES[extension];
2584
2586
  const fileBuffer = await readFile(absolutePath);
2585
2587
  const base64Data = fileBuffer.toString('base64');
2586
- const dataUrl = `data:${mimeType};base64,${base64Data}`;
2587
2588
 
2588
- // Cache the loaded image
2589
- this.pendingImages.set(imagePath, dataUrl);
2589
+ if (isDocumentExtension(extension)) {
2590
+ // Store documents as objects with metadata for the 'file' content part
2591
+ this.pendingImages.set(mediaPath, {
2592
+ type: 'document',
2593
+ mimeType,
2594
+ data: base64Data,
2595
+ filename: basename(mediaPath)
2596
+ });
2597
+ } else {
2598
+ // Store images as data URLs (backward compatible)
2599
+ const dataUrl = `data:${mimeType};base64,${base64Data}`;
2600
+ this.pendingImages.set(mediaPath, dataUrl);
2601
+ }
2590
2602
 
2591
2603
  if (this.debug) {
2592
- console.log(`[DEBUG] Successfully loaded image: ${imagePath} (${fileBuffer.length} bytes)`);
2604
+ console.log(`[DEBUG] Successfully loaded media: ${mediaPath} (${fileBuffer.length} bytes, ${mimeType})`);
2593
2605
  }
2594
2606
 
2595
2607
  return true;
2596
2608
  } catch (error) {
2597
2609
  if (this.debug) {
2598
- console.log(`[DEBUG] Failed to load image ${imagePath}: ${error.message}`);
2610
+ console.log(`[DEBUG] Failed to load media ${mediaPath}: ${error.message}`);
2599
2611
  }
2600
2612
  return false;
2601
2613
  }
2602
2614
  }
2603
2615
 
2616
+ /**
2617
+ * Backward-compatible alias for loadMediaIfValid
2618
+ * @param {string} imagePath - Path to the image file
2619
+ * @returns {Promise<boolean>}
2620
+ */
2621
+ async loadImageIfValid(imagePath) {
2622
+ return this.loadMediaIfValid(imagePath);
2623
+ }
2624
+
2604
2625
  /**
2605
2626
  * Get all currently loaded images as an array for AI model consumption
2606
- * @returns {Array<string>} - Array of base64 data URLs
2627
+ * @returns {Array<string>} - Array of base64 data URLs (images only, for backward compat)
2607
2628
  */
2608
2629
  getCurrentImages() {
2609
- return Array.from(this.pendingImages.values());
2630
+ return Array.from(this.pendingImages.values()).filter(v => typeof v === 'string');
2610
2631
  }
2611
2632
 
2612
2633
  /**
2613
- * Clear loaded images (useful for new conversations)
2634
+ * Get all currently loaded media as an array of content parts
2635
+ * @returns {Array<Object>} - Array of Vercel AI SDK content parts
2636
+ */
2637
+ getCurrentMedia() {
2638
+ const parts = [];
2639
+ for (const entry of this.pendingImages.values()) {
2640
+ if (typeof entry === 'string') {
2641
+ // Image data URL
2642
+ parts.push({ type: 'image', image: entry });
2643
+ } else if (entry && entry.type === 'document') {
2644
+ // Document (PDF) — use Vercel AI SDK 'file' content part
2645
+ parts.push({
2646
+ type: 'file',
2647
+ mediaType: entry.mimeType,
2648
+ data: entry.data,
2649
+ filename: entry.filename
2650
+ });
2651
+ }
2652
+ }
2653
+ return parts;
2654
+ }
2655
+
2656
+ /**
2657
+ * Clear loaded media (useful for new conversations)
2614
2658
  */
2615
2659
  clearLoadedImages() {
2616
2660
  this.pendingImages.clear();
2617
2661
  this.currentImages = [];
2618
2662
  if (this.debug) {
2619
- console.log('[DEBUG] Cleared all loaded images');
2663
+ console.log('[DEBUG] Cleared all loaded media');
2620
2664
  }
2621
2665
  }
2622
2666
 
2623
2667
  /**
2624
- * Prepare messages for AI consumption, adding images to the latest user message if available
2668
+ * Prepare messages for AI consumption, adding media to the latest user message if available
2625
2669
  * @param {Array} messages - Current conversation messages
2626
- * @returns {Array} - Messages formatted for AI SDK with potential image content
2670
+ * @returns {Array} - Messages formatted for AI SDK with potential media content
2627
2671
  */
2628
2672
  prepareMessagesWithImages(messages) {
2629
- const loadedImages = this.getCurrentImages();
2630
-
2631
- // If no images loaded, return messages as-is
2632
- if (loadedImages.length === 0) {
2673
+ const mediaParts = this.getCurrentMedia();
2674
+
2675
+ if (mediaParts.length === 0) {
2633
2676
  return messages;
2634
2677
  }
2635
2678
 
2636
- // Clone messages to avoid mutating the original
2637
- const messagesWithImages = [...messages];
2638
-
2639
- // Find the last user message to attach images to
2640
- const lastUserMessageIndex = messagesWithImages.map(m => m.role).lastIndexOf('user');
2641
-
2679
+ const messagesWithMedia = [...messages];
2680
+ const lastUserMessageIndex = messagesWithMedia.map(m => m.role).lastIndexOf('user');
2681
+
2642
2682
  if (lastUserMessageIndex === -1) {
2643
2683
  if (this.debug) {
2644
- console.log('[DEBUG] No user messages found to attach images to');
2684
+ console.log('[DEBUG] No user messages found to attach media to');
2645
2685
  }
2646
2686
  return messages;
2647
2687
  }
2648
2688
 
2649
- const lastUserMessage = messagesWithImages[lastUserMessageIndex];
2650
-
2651
- // Convert to multimodal format if we have images
2689
+ const lastUserMessage = messagesWithMedia[lastUserMessageIndex];
2690
+
2652
2691
  if (typeof lastUserMessage.content === 'string') {
2653
- messagesWithImages[lastUserMessageIndex] = {
2692
+ messagesWithMedia[lastUserMessageIndex] = {
2654
2693
  ...lastUserMessage,
2655
2694
  content: [
2656
2695
  { type: 'text', text: lastUserMessage.content },
2657
- ...loadedImages.map(imageData => ({
2658
- type: 'image',
2659
- image: imageData
2660
- }))
2696
+ ...mediaParts
2661
2697
  ]
2662
2698
  };
2663
2699
 
2664
2700
  if (this.debug) {
2665
- console.log(`[DEBUG] Added ${loadedImages.length} images to the latest user message`);
2701
+ console.log(`[DEBUG] Added ${mediaParts.length} media items to the latest user message`);
2666
2702
  }
2667
2703
  }
2668
2704
 
2669
- return messagesWithImages;
2705
+ return messagesWithMedia;
2670
2706
  }
2671
2707
 
2672
2708
  /**
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Shared media format configuration for Probe agent
3
+ *
4
+ * This module centralizes supported media formats (images + documents)
5
+ * and their MIME types to ensure consistency across all components.
6
+ *
7
+ * Supports:
8
+ * - Images: png, jpg, jpeg, webp, bmp, svg
9
+ * - Documents: pdf (native support in Claude, Gemini, OpenAI via Vercel AI SDK)
10
+ *
11
+ * Note: GIF support was intentionally removed for compatibility with
12
+ * AI models like Google Gemini that don't support animated images.
13
+ */
14
+
15
+ // Supported image file extensions (without leading dot)
16
+ export const SUPPORTED_IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'webp', 'bmp', 'svg'];
17
+
18
+ // Supported document file extensions (without leading dot)
19
+ export const SUPPORTED_DOCUMENT_EXTENSIONS = ['pdf'];
20
+
21
+ // All supported media extensions (images + documents)
22
+ export const SUPPORTED_MEDIA_EXTENSIONS = [...SUPPORTED_IMAGE_EXTENSIONS, ...SUPPORTED_DOCUMENT_EXTENSIONS];
23
+
24
+ // MIME type mapping for all supported media formats
25
+ export const MEDIA_MIME_TYPES = {
26
+ 'png': 'image/png',
27
+ 'jpg': 'image/jpeg',
28
+ 'jpeg': 'image/jpeg',
29
+ 'webp': 'image/webp',
30
+ 'bmp': 'image/bmp',
31
+ 'svg': 'image/svg+xml',
32
+ 'pdf': 'application/pdf'
33
+ };
34
+
35
+ // Legacy aliases for backward compatibility
36
+ export const IMAGE_MIME_TYPES = MEDIA_MIME_TYPES;
37
+
38
+ // Provider-specific unsupported media formats
39
+ export const PROVIDER_UNSUPPORTED_FORMATS = {
40
+ 'google': ['svg'], // Google Gemini doesn't support image/svg+xml
41
+ };
42
+
43
+ /**
44
+ * Check if a file extension is an image type
45
+ * @param {string} extension - File extension (without dot)
46
+ * @returns {boolean}
47
+ */
48
+ export function isImageExtension(extension) {
49
+ return SUPPORTED_IMAGE_EXTENSIONS.includes(extension?.toLowerCase());
50
+ }
51
+
52
+ /**
53
+ * Check if a file extension is a document type (PDF, etc.)
54
+ * @param {string} extension - File extension (without dot)
55
+ * @returns {boolean}
56
+ */
57
+ export function isDocumentExtension(extension) {
58
+ return SUPPORTED_DOCUMENT_EXTENSIONS.includes(extension?.toLowerCase());
59
+ }
60
+
61
+ /**
62
+ * Generate a regex pattern string for matching media file extensions
63
+ * @param {string[]} extensions - Array of extensions (without dots)
64
+ * @returns {string} Regex pattern string like "png|jpg|jpeg|webp|bmp|svg|pdf"
65
+ */
66
+ export function getExtensionPattern(extensions = SUPPORTED_MEDIA_EXTENSIONS) {
67
+ return extensions.join('|');
68
+ }
69
+
70
+ /**
71
+ * Get MIME type for a file extension
72
+ * @param {string} extension - File extension (without dot)
73
+ * @returns {string|undefined} MIME type or undefined if not supported
74
+ */
75
+ export function getMimeType(extension) {
76
+ return MEDIA_MIME_TYPES[extension?.toLowerCase()];
77
+ }
78
+
79
+ /**
80
+ * Check if a media extension is supported by a specific provider
81
+ * @param {string} extension - File extension (without dot)
82
+ * @param {string} provider - Provider name (e.g., 'google', 'anthropic', 'openai')
83
+ * @returns {boolean} True if the format is supported by the provider
84
+ */
85
+ export function isFormatSupportedByProvider(extension, provider) {
86
+ if (!extension || typeof extension !== 'string') {
87
+ return false;
88
+ }
89
+ if (extension.includes('/') || extension.includes('\\') || extension.includes('..')) {
90
+ return false;
91
+ }
92
+
93
+ const ext = extension.toLowerCase();
94
+
95
+ if (!SUPPORTED_MEDIA_EXTENSIONS.includes(ext)) {
96
+ return false;
97
+ }
98
+
99
+ if (!provider || typeof provider !== 'string') {
100
+ return true;
101
+ }
102
+
103
+ const unsupportedFormats = PROVIDER_UNSUPPORTED_FORMATS[provider];
104
+ if (unsupportedFormats && unsupportedFormats.includes(ext)) {
105
+ return false;
106
+ }
107
+
108
+ return true;
109
+ }
110
+
111
+ /**
112
+ * Get supported media extensions for a specific provider
113
+ * @param {string} provider - Provider name (e.g., 'google', 'anthropic', 'openai')
114
+ * @returns {string[]} Array of supported extensions for this provider
115
+ */
116
+ export function getSupportedExtensionsForProvider(provider) {
117
+ if (!provider || typeof provider !== 'string') {
118
+ return [...SUPPORTED_MEDIA_EXTENSIONS];
119
+ }
120
+ const unsupportedFormats = PROVIDER_UNSUPPORTED_FORMATS[provider] || [];
121
+ return SUPPORTED_MEDIA_EXTENSIONS.filter(ext => !unsupportedFormats.includes(ext));
122
+ }
@@ -26,6 +26,7 @@ import {
26
26
  listFilesSchema,
27
27
  searchFilesSchema,
28
28
  readImageSchema,
29
+ readMediaSchema,
29
30
  listSkillsSchema,
30
31
  useSkillSchema
31
32
  } from '../index.js';
@@ -109,6 +110,7 @@ export {
109
110
  listFilesSchema,
110
111
  searchFilesSchema,
111
112
  readImageSchema,
113
+ readMediaSchema,
112
114
  listSkillsSchema,
113
115
  useSkillSchema
114
116
  };
package/build/delegate.js CHANGED
@@ -387,7 +387,9 @@ export async function delegate({
387
387
  bashConfig = null,
388
388
  allowEdit = false,
389
389
  architectureFileName = null,
390
- promptType = 'code-researcher',
390
+ promptType = undefined,
391
+ customPrompt = null,
392
+ completionPrompt = null,
391
393
  allowedTools = null,
392
394
  disableTools = false,
393
395
  searchDelegate = undefined,
@@ -474,7 +476,9 @@ export async function delegate({
474
476
  // Tasks do not propagate back to the parent - each subagent has its own scope.
475
477
  const subagent = new ProbeAgent({
476
478
  sessionId,
477
- promptType, // Clean prompt, not inherited from parent
479
+ promptType, // Inherit from parent (or use parent's default)
480
+ customPrompt, // Inherit custom system prompt from parent
481
+ completionPrompt, // Inherit completion prompt from parent
478
482
  enableDelegate: false, // Explicitly disable delegation to prevent recursion
479
483
  disableMermaidValidation: true, // Faster processing
480
484
  disableJsonValidation: true, // Simpler responses
package/build/index.js CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  listFilesSchema,
33
33
  searchFilesSchema,
34
34
  readImageSchema,
35
+ readMediaSchema,
35
36
  listSkillsSchema,
36
37
  useSkillSchema
37
38
  } from './tools/common.js';
@@ -115,6 +116,7 @@ export {
115
116
  listFilesSchema,
116
117
  searchFilesSchema,
117
118
  readImageSchema,
119
+ readMediaSchema,
118
120
  listSkillsSchema,
119
121
  useSkillSchema,
120
122
  // Export task management
@@ -68,6 +68,10 @@ export const readImageSchema = z.object({
68
68
  path: z.string().describe('Path to the image file to read. Supports png, jpg, jpeg, webp, bmp, and svg formats.')
69
69
  });
70
70
 
71
+ export const readMediaSchema = z.object({
72
+ path: z.string().describe('Path to the media file to read. Supports images (png, jpg, jpeg, webp, bmp, svg) and documents (pdf).')
73
+ });
74
+
71
75
  export const bashSchema = z.object({
72
76
  command: z.string().describe('The bash command to execute'),
73
77
  workingDirectory: z.string().optional().describe('Directory to execute the command in (optional)'),
@@ -30,6 +30,7 @@ export {
30
30
  listFilesSchema,
31
31
  searchFilesSchema,
32
32
  readImageSchema,
33
+ readMediaSchema,
33
34
  listSkillsSchema,
34
35
  useSkillSchema
35
36
  } from './common.js';
@@ -1135,7 +1135,7 @@ export const extractTool = (options = {}) => {
1135
1135
  * @returns {Object} Configured delegate tool
1136
1136
  */
1137
1137
  export const delegateTool = (options = {}) => {
1138
- const { debug = false, timeout = 300, cwd, allowedFolders, workspaceRoot, enableBash = false, bashConfig, allowEdit = false, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null, delegationManager = null,
1138
+ const { debug = false, timeout = 300, cwd, allowedFolders, workspaceRoot, enableBash = false, bashConfig, allowEdit = false, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null, promptType: parentPromptType, customPrompt: parentCustomPrompt = null, completionPrompt: parentCompletionPrompt = null, delegationManager = null,
1139
1139
  // Timeout settings inherited from parent agent
1140
1140
  timeoutBehavior, maxOperationTimeout, requestTimeout, gracefulTimeoutBonusSteps,
1141
1141
  negotiatedTimeoutBudget, negotiatedTimeoutMaxRequests, negotiatedTimeoutMaxPerRequest,
@@ -1257,6 +1257,9 @@ export const delegateTool = (options = {}) => {
1257
1257
  allowEdit,
1258
1258
  bashConfig,
1259
1259
  architectureFileName,
1260
+ promptType: parentPromptType, // Inherit parent's prompt type
1261
+ customPrompt: parentCustomPrompt, // Inherit parent's custom system prompt
1262
+ completionPrompt: parentCompletionPrompt, // Inherit parent's completion prompt
1260
1263
  searchDelegate,
1261
1264
  enableMcp,
1262
1265
  mcpConfig,