@probelabs/probe 0.6.0-rc306 → 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.
@@ -958,40 +961,43 @@ export class ProbeAgent {
958
961
  }
959
962
  }
960
963
 
961
- // Image loading tool
962
- if (isToolAllowed('readImage')) {
963
- this.toolImplementations.readImage = {
964
- execute: async (params) => {
965
- const imagePath = params.path;
966
- if (!imagePath) {
967
- throw new Error('Image path is required');
968
- }
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
+ }
969
970
 
970
- // Validate extension before attempting to load
971
- // Use basename to prevent path traversal attacks (e.g., 'malicious.jpg/../../../etc/passwd')
972
- const filename = basename(imagePath);
973
- 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();
974
975
 
975
- // Always validate extension is in allowed list (defense-in-depth)
976
- if (!extension || !SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
977
- throw new Error(`Invalid or unsupported image extension: ${extension}. Supported formats: ${SUPPORTED_IMAGE_EXTENSIONS.join(', ')}`);
978
- }
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
+ }
979
980
 
980
- // Check provider-specific format restrictions (e.g., SVG not supported by Google Gemini)
981
- if (this.apiType && !isFormatSupportedByProvider(extension, this.apiType)) {
982
- 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.`);
983
- }
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
+ }
984
985
 
985
- // Load the image using the existing loadImageIfValid method
986
- const loaded = await this.loadImageIfValid(imagePath);
986
+ // Load the media file
987
+ const loaded = await this.loadMediaIfValid(mediaPath);
987
988
 
988
- if (!loaded) {
989
- 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.`);
990
- }
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
+ }
991
992
 
992
- return `Image loaded successfully: ${imagePath}. The image is now available for analysis in the conversation.`;
993
- }
994
- };
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 };
995
1001
  }
996
1002
 
997
1003
  // Add bash tool if enabled and allowed
@@ -2219,9 +2225,9 @@ export class ProbeAgent {
2219
2225
  schema: searchFilesSchema,
2220
2226
  description: 'Find files matching a glob pattern with recursive search capability.'
2221
2227
  },
2222
- readImage: {
2223
- schema: readImageSchema,
2224
- 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.'
2225
2231
  },
2226
2232
  listSkills: {
2227
2233
  schema: listSkillsSchema,
@@ -2383,7 +2389,7 @@ export class ProbeAgent {
2383
2389
 
2384
2390
  // Enhanced pattern to detect image file mentions in various contexts
2385
2391
  // Looks for: "image", "file", "screenshot", etc. followed by path-like strings with image extensions
2386
- const extensionsPattern = `(?:${SUPPORTED_IMAGE_EXTENSIONS.join('|')})`;
2392
+ const extensionsPattern = `(?:${SUPPORTED_MEDIA_EXTENSIONS.join('|')})`;
2387
2393
  const imagePatterns = [
2388
2394
  // Direct file path mentions: "./screenshot.png", "/path/to/image.jpg", etc.
2389
2395
  new RegExp(`(?:\\.?\\.\\/)?[^\\s"'<>\\[\\]]+\\\.${extensionsPattern}(?!\\w)`, 'gi'),
@@ -2504,43 +2510,36 @@ export class ProbeAgent {
2504
2510
  }
2505
2511
 
2506
2512
  /**
2507
- * Load and cache an image if it's valid and accessible
2508
- * @param {string} imagePath - Path to the image file
2509
- * @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
2510
2516
  */
2511
- async loadImageIfValid(imagePath) {
2517
+ async loadMediaIfValid(mediaPath) {
2512
2518
  try {
2513
2519
  // Skip if already loaded
2514
- if (this.pendingImages.has(imagePath)) {
2520
+ if (this.pendingImages.has(mediaPath)) {
2515
2521
  if (this.debug) {
2516
- console.log(`[DEBUG] Image already loaded: ${imagePath}`);
2522
+ console.log(`[DEBUG] Media already loaded: ${mediaPath}`);
2517
2523
  }
2518
2524
  return true;
2519
2525
  }
2520
2526
 
2521
2527
  // Security validation: check if path is within any allowed directory
2522
- // Use safeRealpath() to resolve symlinks and handle path traversal attempts (e.g., '/allowed/../etc/passwd')
2523
- // This prevents symlink bypass attacks (e.g., /tmp -> /private/tmp on macOS)
2524
2528
  const allowedDirs = this.allowedFolders && this.allowedFolders.length > 0 ? this.allowedFolders : [process.cwd()];
2525
2529
 
2526
2530
  let absolutePath;
2527
2531
  let isPathAllowed = false;
2528
2532
 
2529
- // If absolute path, check if it's within any allowed directory
2530
- if (isAbsolute(imagePath)) {
2531
- // Use safeRealpath to resolve symlinks for security
2532
- absolutePath = safeRealpath(resolve(imagePath));
2533
+ if (isAbsolute(mediaPath)) {
2534
+ absolutePath = safeRealpath(resolve(mediaPath));
2533
2535
  isPathAllowed = allowedDirs.some(dir => {
2534
2536
  const resolvedDir = safeRealpath(dir);
2535
- // Ensure the path is within the allowed directory (add separator to prevent prefix attacks)
2536
2537
  return absolutePath === resolvedDir || absolutePath.startsWith(resolvedDir + sep);
2537
2538
  });
2538
2539
  } else {
2539
- // For relative paths, try resolving against each allowed directory
2540
2540
  for (const dir of allowedDirs) {
2541
2541
  const resolvedDir = safeRealpath(dir);
2542
- const resolvedPath = safeRealpath(resolve(dir, imagePath));
2543
- // Ensure the resolved path is within the allowed directory
2542
+ const resolvedPath = safeRealpath(resolve(dir, mediaPath));
2544
2543
  if (resolvedPath === resolvedDir || resolvedPath.startsWith(resolvedDir + sep)) {
2545
2544
  absolutePath = resolvedPath;
2546
2545
  isPathAllowed = true;
@@ -2548,137 +2547,162 @@ export class ProbeAgent {
2548
2547
  }
2549
2548
  }
2550
2549
  }
2551
-
2552
- // Security check: ensure path is within at least one allowed directory
2550
+
2553
2551
  if (!isPathAllowed) {
2554
2552
  if (this.debug) {
2555
- console.log(`[DEBUG] Image path outside allowed directories: ${imagePath}`);
2553
+ console.log(`[DEBUG] Media path outside allowed directories: ${mediaPath}`);
2556
2554
  }
2557
2555
  return false;
2558
2556
  }
2559
2557
 
2560
- // Check if file exists and get file stats
2561
2558
  let fileStats;
2562
2559
  try {
2563
2560
  fileStats = await stat(absolutePath);
2564
2561
  } catch (error) {
2565
2562
  if (this.debug) {
2566
- console.log(`[DEBUG] Image file not found: ${absolutePath}`);
2563
+ console.log(`[DEBUG] Media file not found: ${absolutePath}`);
2567
2564
  }
2568
2565
  return false;
2569
2566
  }
2570
2567
 
2571
- // Validate file size to prevent OOM attacks
2572
- if (fileStats.size > MAX_IMAGE_FILE_SIZE) {
2568
+ const extension = absolutePath.toLowerCase().split('.').pop();
2569
+ if (!SUPPORTED_MEDIA_EXTENSIONS.includes(extension)) {
2573
2570
  if (this.debug) {
2574
- console.log(`[DEBUG] Image file too large: ${absolutePath} (${fileStats.size} bytes, max: ${MAX_IMAGE_FILE_SIZE})`);
2571
+ console.log(`[DEBUG] Unsupported media format: ${extension}`);
2575
2572
  }
2576
2573
  return false;
2577
2574
  }
2578
2575
 
2579
- // Validate file extension
2580
- const extension = absolutePath.toLowerCase().split('.').pop();
2581
- 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) {
2582
2579
  if (this.debug) {
2583
- console.log(`[DEBUG] Unsupported image format: ${extension}`);
2580
+ console.log(`[DEBUG] Media file too large: ${absolutePath} (${fileStats.size} bytes, max: ${maxSize})`);
2584
2581
  }
2585
2582
  return false;
2586
2583
  }
2587
2584
 
2588
- // Note: Provider-specific format validation (e.g., SVG not supported by Google Gemini)
2589
- // is handled by the readImage tool which provides explicit error messages.
2590
- // loadImageIfValid is a lower-level method that only checks general format support.
2591
-
2592
- // Determine MIME type (from shared config)
2593
- const mimeType = IMAGE_MIME_TYPES[extension];
2594
-
2595
- // Read and encode file asynchronously
2585
+ const mimeType = MEDIA_MIME_TYPES[extension];
2596
2586
  const fileBuffer = await readFile(absolutePath);
2597
2587
  const base64Data = fileBuffer.toString('base64');
2598
- const dataUrl = `data:${mimeType};base64,${base64Data}`;
2599
2588
 
2600
- // Cache the loaded image
2601
- 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
+ }
2602
2602
 
2603
2603
  if (this.debug) {
2604
- console.log(`[DEBUG] Successfully loaded image: ${imagePath} (${fileBuffer.length} bytes)`);
2604
+ console.log(`[DEBUG] Successfully loaded media: ${mediaPath} (${fileBuffer.length} bytes, ${mimeType})`);
2605
2605
  }
2606
2606
 
2607
2607
  return true;
2608
2608
  } catch (error) {
2609
2609
  if (this.debug) {
2610
- console.log(`[DEBUG] Failed to load image ${imagePath}: ${error.message}`);
2610
+ console.log(`[DEBUG] Failed to load media ${mediaPath}: ${error.message}`);
2611
2611
  }
2612
2612
  return false;
2613
2613
  }
2614
2614
  }
2615
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
+
2616
2625
  /**
2617
2626
  * Get all currently loaded images as an array for AI model consumption
2618
- * @returns {Array<string>} - Array of base64 data URLs
2627
+ * @returns {Array<string>} - Array of base64 data URLs (images only, for backward compat)
2619
2628
  */
2620
2629
  getCurrentImages() {
2621
- return Array.from(this.pendingImages.values());
2630
+ return Array.from(this.pendingImages.values()).filter(v => typeof v === 'string');
2622
2631
  }
2623
2632
 
2624
2633
  /**
2625
- * 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)
2626
2658
  */
2627
2659
  clearLoadedImages() {
2628
2660
  this.pendingImages.clear();
2629
2661
  this.currentImages = [];
2630
2662
  if (this.debug) {
2631
- console.log('[DEBUG] Cleared all loaded images');
2663
+ console.log('[DEBUG] Cleared all loaded media');
2632
2664
  }
2633
2665
  }
2634
2666
 
2635
2667
  /**
2636
- * 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
2637
2669
  * @param {Array} messages - Current conversation messages
2638
- * @returns {Array} - Messages formatted for AI SDK with potential image content
2670
+ * @returns {Array} - Messages formatted for AI SDK with potential media content
2639
2671
  */
2640
2672
  prepareMessagesWithImages(messages) {
2641
- const loadedImages = this.getCurrentImages();
2642
-
2643
- // If no images loaded, return messages as-is
2644
- if (loadedImages.length === 0) {
2673
+ const mediaParts = this.getCurrentMedia();
2674
+
2675
+ if (mediaParts.length === 0) {
2645
2676
  return messages;
2646
2677
  }
2647
2678
 
2648
- // Clone messages to avoid mutating the original
2649
- const messagesWithImages = [...messages];
2650
-
2651
- // Find the last user message to attach images to
2652
- const lastUserMessageIndex = messagesWithImages.map(m => m.role).lastIndexOf('user');
2653
-
2679
+ const messagesWithMedia = [...messages];
2680
+ const lastUserMessageIndex = messagesWithMedia.map(m => m.role).lastIndexOf('user');
2681
+
2654
2682
  if (lastUserMessageIndex === -1) {
2655
2683
  if (this.debug) {
2656
- console.log('[DEBUG] No user messages found to attach images to');
2684
+ console.log('[DEBUG] No user messages found to attach media to');
2657
2685
  }
2658
2686
  return messages;
2659
2687
  }
2660
2688
 
2661
- const lastUserMessage = messagesWithImages[lastUserMessageIndex];
2662
-
2663
- // Convert to multimodal format if we have images
2689
+ const lastUserMessage = messagesWithMedia[lastUserMessageIndex];
2690
+
2664
2691
  if (typeof lastUserMessage.content === 'string') {
2665
- messagesWithImages[lastUserMessageIndex] = {
2692
+ messagesWithMedia[lastUserMessageIndex] = {
2666
2693
  ...lastUserMessage,
2667
2694
  content: [
2668
2695
  { type: 'text', text: lastUserMessage.content },
2669
- ...loadedImages.map(imageData => ({
2670
- type: 'image',
2671
- image: imageData
2672
- }))
2696
+ ...mediaParts
2673
2697
  ]
2674
2698
  };
2675
2699
 
2676
2700
  if (this.debug) {
2677
- 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`);
2678
2702
  }
2679
2703
  }
2680
2704
 
2681
- return messagesWithImages;
2705
+ return messagesWithMedia;
2682
2706
  }
2683
2707
 
2684
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/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';