@probelabs/probe-chat 0.6.0-rc102 → 0.6.0-rc103

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.
@@ -0,0 +1,195 @@
1
+ # Local Image Support in Probe Agent
2
+
3
+ The probe agent now supports reading local image files directly from file paths mentioned in user messages and **automatically loads images when mentioned during the agentic loop**.
4
+
5
+ ## Features Added
6
+
7
+ ### Automatic Local File Detection
8
+ - Detects local image file paths in user messages
9
+ - Supports both relative and absolute paths
10
+ - Recognizes common image extensions: `.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`, `.bmp`, `.svg`
11
+
12
+ ### 🚀 NEW: Agentic Loop Image Loading
13
+ - **Automatic detection**: Agent automatically detects when it mentions image files in its internal thinking
14
+ - **Smart loading**: Images are loaded and added to the AI context for subsequent iterations
15
+ - **Persistent context**: Loaded images remain available throughout the conversation
16
+ - **Tool result processing**: Images mentioned in tool outputs are also automatically loaded
17
+ - **Caching**: Prevents reloading the same images multiple times
18
+
19
+ ### Security Features
20
+ - Path validation to prevent directory traversal attacks
21
+ - Restricts file access to allowed directories (respects `ALLOWED_FOLDERS` environment variable)
22
+ - Validates file existence and readability before processing
23
+
24
+ ### Supported Path Formats
25
+ ```
26
+ ./image.png # Relative path from current directory
27
+ ../assets/screenshot.jpg # Relative path with directory traversal
28
+ /absolute/path/to/image.webp # Absolute path
29
+ image.gif # File in current directory
30
+ ```
31
+
32
+ ### Automatic Conversion
33
+ - Local files are automatically converted to base64 data URLs
34
+ - Maintains original MIME type based on file extension
35
+ - Seamlessly integrates with existing URL and base64 image support
36
+
37
+ ## Usage Examples
38
+
39
+ ### Basic Usage
40
+ ```javascript
41
+ import { ProbeChat } from './probeChat.js';
42
+
43
+ const chat = new ProbeChat({ debug: true });
44
+
45
+ // The agent will automatically detect and process the local image
46
+ const response = await chat.chat('Analyze this screenshot: ./screenshot.png');
47
+ ```
48
+
49
+ ### Mixed Content
50
+ ```javascript
51
+ // Mix local files with URLs
52
+ const message = `
53
+ Compare this local image ./local.png
54
+ with this remote image https://example.com/remote.jpg
55
+ `;
56
+
57
+ const response = await chat.chat(message);
58
+ ```
59
+
60
+ ### Direct Function Usage
61
+ ```javascript
62
+ import { extractImageUrls } from './probeChat.js';
63
+
64
+ const message = 'Please review this diagram: ./architecture.png';
65
+ const result = await extractImageUrls(message, true);
66
+
67
+ console.log(`Found ${result.urls.length} images`);
68
+ console.log(`Cleaned message: "${result.cleanedMessage}"`);
69
+ ```
70
+
71
+ ## 🤖 Agentic Loop Integration
72
+
73
+ The most powerful feature is automatic image loading during the agent's internal reasoning process.
74
+
75
+ ### How It Works
76
+
77
+ When the probe agent is working through a task, it can now:
78
+
79
+ 1. **Mention an image file** in its reasoning: "I need to check ./screenshot.png"
80
+ 2. **Automatically load the image** before the next AI iteration
81
+ 3. **Use visual context** for enhanced analysis and problem-solving
82
+
83
+ ### Agentic Flow Example
84
+
85
+ ```
86
+ 👤 USER: "Analyze the system architecture"
87
+
88
+ 🤖 AGENT: "Let me search for architecture documentation..."
89
+ 🔍 Tool: search "architecture design"
90
+ 📊 Result: "Found ./docs/system-diagram.png"
91
+
92
+ 🤖 AGENT: "I found a system diagram at ./docs/system-diagram.png. Let me analyze it."
93
+ 🖼️ AUTO: Image ./docs/system-diagram.png loaded into context
94
+
95
+ 🤖 AGENT: "Based on the diagram I can see..."
96
+ 💭 AI now has visual access to the diagram and can analyze it
97
+ ```
98
+
99
+ ### Trigger Patterns
100
+
101
+ The agent automatically loads images when it mentions:
102
+
103
+ - **Direct paths**: `./screenshot.png`, `/path/to/image.jpg`
104
+ - **Contextual references**: "the file diagram.png shows", "looking at chart.gif"
105
+ - **Tool results**: When tools return paths to image files
106
+ - **Generated content**: "saved visualization as ./output.png"
107
+
108
+ ### Benefits
109
+
110
+ - **🧠 Enhanced reasoning**: Agent gains visual understanding of referenced images
111
+ - **🔄 Seamless workflow**: No manual image loading required
112
+ - **⚡ Performance**: Intelligent caching prevents reloading
113
+ - **🔒 Security**: Same security validations as manual loading
114
+ - **📱 Persistence**: Images remain available throughout the conversation
115
+
116
+ ## Security Considerations
117
+
118
+ ### Path Restrictions
119
+ - Files must be within the allowed directory structure
120
+ - Prevents access to system files (e.g., `/etc/passwd`)
121
+ - Respects the `ALLOWED_FOLDERS` environment variable
122
+
123
+ ### File Validation
124
+ - Verifies file existence before attempting to read
125
+ - Validates file extensions against supported image formats
126
+ - Handles file reading errors gracefully
127
+
128
+ ### Error Handling
129
+ - Failed file reads are logged but don't interrupt processing
130
+ - Invalid paths are silently ignored
131
+ - Maintains functionality for valid images even if some fail
132
+
133
+ ## Implementation Details
134
+
135
+ ### Pattern Matching
136
+ The system uses an enhanced regex pattern to detect:
137
+ ```javascript
138
+ /(?:data:image\/[a-zA-Z]*;base64,[A-Za-z0-9+/=]+|https?:\/\/(?:(?:private-user-images\.githubusercontent\.com|github\.com\/user-attachments\/assets)\/[^\s"'<>]+|[^\s"'<>]+\.(?:png|jpg|jpeg|webp|gif)(?:\?[^\s"'<>]*)?)|(?:\.?\.?\/)?[^\s"'<>]*\.(?:png|jpg|jpeg|webp|gif))/gi
139
+ ```
140
+
141
+ ### Processing Pipeline
142
+ 1. **Pattern Detection** - Find all potential image references in text
143
+ 2. **Classification** - Distinguish between URLs, base64 data, and local paths
144
+ 3. **Validation** - Verify local file paths for security and existence
145
+ 4. **Conversion** - Read local files and convert to base64 data URLs
146
+ 5. **Integration** - Pass processed images to AI models
147
+
148
+ ### File Size Limitations
149
+ - No explicit file size limits implemented
150
+ - Memory usage scales with image size
151
+ - Large images may impact performance
152
+
153
+ ## Testing
154
+
155
+ Run the test suite to verify functionality:
156
+ ```bash
157
+ cd examples/chat
158
+ node test-local-image-reading.js
159
+ ```
160
+
161
+ The test covers:
162
+ - Basic local file detection and conversion
163
+ - Mixed URL and local file processing
164
+ - Relative path handling
165
+ - Security validation
166
+ - Error handling for missing files
167
+
168
+ ## Backward Compatibility
169
+
170
+ This enhancement is fully backward compatible:
171
+ - Existing URL-based image handling unchanged
172
+ - Base64 data URL support maintained
173
+ - No breaking changes to existing APIs
174
+
175
+ ## Environment Configuration
176
+
177
+ Set allowed folders to restrict file access:
178
+ ```bash
179
+ export ALLOWED_FOLDERS="/path/to/project,/path/to/assets"
180
+ ```
181
+
182
+ If no `ALLOWED_FOLDERS` is set, defaults to current working directory.
183
+
184
+ ## Error Handling
185
+
186
+ The system gracefully handles various error conditions:
187
+ - **File not found**: Logged and ignored
188
+ - **Permission denied**: Logged and ignored
189
+ - **Invalid format**: Logged and ignored
190
+ - **Path traversal attempts**: Blocked by security validation
191
+
192
+ Enable debug mode to see detailed logging:
193
+ ```javascript
194
+ const chat = new ProbeChat({ debug: true });
195
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe-chat",
3
- "version": "0.6.0-rc102",
3
+ "version": "0.6.0-rc103",
4
4
  "description": "CLI and web interface for Probe code search (formerly @probelabs/probe-web and @probelabs/probe-chat)",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -96,6 +96,7 @@
96
96
  "logo.png",
97
97
  "README.md",
98
98
  "TRACING.md",
99
+ "LOCAL_IMAGE_SUPPORT.md",
99
100
  "LICENSE"
100
101
  ]
101
102
  }
package/probeChat.js CHANGED
@@ -2,7 +2,8 @@ import 'dotenv/config';
2
2
  import { ProbeAgent } from '@probelabs/probe/agent';
3
3
  import { TokenUsageDisplay } from './tokenUsageDisplay.js';
4
4
  import { writeFileSync, existsSync } from 'fs';
5
- import { join } from 'path';
5
+ import { readFile, stat } from 'fs/promises';
6
+ import { join, resolve, isAbsolute } from 'path';
6
7
  import { TelemetryConfig } from './telemetry.js';
7
8
  import { trace } from '@opentelemetry/api';
8
9
  import { appTracer } from './appTracer.js';
@@ -39,24 +40,128 @@ if (typeof process !== 'undefined' && !process.env.PROBE_CHAT_SKIP_FOLDER_VALIDA
39
40
  validateFolders();
40
41
  }
41
42
 
43
+ // Maximum image file size (20MB) to prevent OOM attacks
44
+ const MAX_IMAGE_FILE_SIZE = 20 * 1024 * 1024;
45
+
46
+ /**
47
+ * Security validation for local file paths
48
+ * @param {string} filePath - The file path to validate
49
+ * @param {string} baseDir - The base directory to restrict access to
50
+ * @returns {boolean} - Whether the path is safe to access
51
+ */
52
+ function isSecureFilePath(filePath, baseDir = process.cwd()) {
53
+ try {
54
+ // Resolve the absolute path
55
+ const absolutePath = isAbsolute(filePath) ? filePath : resolve(baseDir, filePath);
56
+ const normalizedBase = resolve(baseDir);
57
+
58
+ // Ensure the resolved path is within the allowed directory
59
+ return absolutePath.startsWith(normalizedBase);
60
+ } catch (error) {
61
+ return false;
62
+ }
63
+ }
64
+
42
65
  /**
43
- * Extract image URLs from message text
66
+ * Convert local image file to base64 data URL
67
+ * @param {string} filePath - Path to the image file
68
+ * @param {boolean} debug - Whether to log debug information
69
+ * @returns {Promise<string|null>} - Base64 data URL or null if failed
70
+ */
71
+ async function convertImageFileToBase64(filePath, debug = false) {
72
+ try {
73
+ // Security check: validate the file path against all allowed directories
74
+ const allowedDirs = allowedFolders.length > 0 ? allowedFolders : [process.cwd()];
75
+ const isPathAllowed = allowedDirs.some(dir => isSecureFilePath(filePath, dir));
76
+
77
+ if (!isPathAllowed) {
78
+ if (debug) {
79
+ console.log(`[DEBUG] Security check failed for path: ${filePath}`);
80
+ }
81
+ return null;
82
+ }
83
+
84
+ // Resolve the path - for relative paths, use the first allowed directory as base
85
+ const baseDir = allowedDirs[0];
86
+ const absolutePath = isAbsolute(filePath) ? filePath : resolve(baseDir, filePath);
87
+
88
+ // Check if file exists and get file stats
89
+ let fileStats;
90
+ try {
91
+ fileStats = await stat(absolutePath);
92
+ } catch (error) {
93
+ if (debug) {
94
+ console.log(`[DEBUG] File not found: ${absolutePath}`);
95
+ }
96
+ return null;
97
+ }
98
+
99
+ // Validate file size to prevent OOM attacks
100
+ if (fileStats.size > MAX_IMAGE_FILE_SIZE) {
101
+ if (debug) {
102
+ console.log(`[DEBUG] Image file too large: ${absolutePath} (${fileStats.size} bytes, max: ${MAX_IMAGE_FILE_SIZE})`);
103
+ }
104
+ return null;
105
+ }
106
+
107
+ // Determine MIME type based on file extension
108
+ const extension = absolutePath.toLowerCase().split('.').pop();
109
+ const mimeTypes = {
110
+ 'png': 'image/png',
111
+ 'jpg': 'image/jpeg',
112
+ 'jpeg': 'image/jpeg',
113
+ 'webp': 'image/webp',
114
+ 'gif': 'image/gif'
115
+ };
116
+
117
+ const mimeType = mimeTypes[extension];
118
+ if (!mimeType) {
119
+ if (debug) {
120
+ console.log(`[DEBUG] Unsupported image format: ${extension}`);
121
+ }
122
+ return null;
123
+ }
124
+
125
+ // Read file and convert to base64 asynchronously
126
+ const fileBuffer = await readFile(absolutePath);
127
+ const base64Data = fileBuffer.toString('base64');
128
+ const dataUrl = `data:${mimeType};base64,${base64Data}`;
129
+
130
+ if (debug) {
131
+ console.log(`[DEBUG] Successfully converted ${absolutePath} to base64 (${fileBuffer.length} bytes)`);
132
+ }
133
+
134
+ return dataUrl;
135
+ } catch (error) {
136
+ if (debug) {
137
+ console.log(`[DEBUG] Error converting file to base64: ${error.message}`);
138
+ }
139
+ return null;
140
+ }
141
+ }
142
+
143
+ // Export the extractImageUrls function for testing
144
+ export { extractImageUrls };
145
+
146
+ /**
147
+ * Extract image URLs and local file paths from message text
44
148
  * @param {string} message - The message text to analyze
45
149
  * @param {boolean} debug - Whether to log debug information
46
- * @returns {Array} Array of { url: string, cleanedMessage: string }
150
+ * @returns {Promise<Object>} Promise resolving to { urls: Array, cleanedMessage: string }
47
151
  */
48
- function extractImageUrls(message, debug = false) {
152
+ async function extractImageUrls(message, debug = false) {
49
153
  // This function should be called within the session context, so it will inherit the trace ID
50
154
  const tracer = trace.getTracer('probe-chat', '1.0.0');
51
- return tracer.startActiveSpan('content.image.extract', (span) => {
155
+ return tracer.startActiveSpan('content.image.extract', async (span) => {
52
156
  try {
53
- // Pattern to match image URLs and base64 data:
157
+ // Pattern to match image URLs, base64 data, and local file paths:
54
158
  // 1. GitHub private-user-images URLs (always images, regardless of extension)
55
159
  // 2. GitHub user-attachments/assets URLs (always images, regardless of extension)
56
160
  // 3. URLs with common image extensions (PNG, JPG, JPEG, WebP, GIF)
57
161
  // 4. Base64 data URLs (data:image/...)
162
+ // 5. Local file paths with image extensions (relative and absolute)
58
163
  // Updated to stop at quotes, spaces, or common HTML/XML delimiters
59
- const imageUrlPattern = /(?:data:image\/[a-zA-Z]*;base64,[A-Za-z0-9+/=]+|https?:\/\/(?:(?:private-user-images\.githubusercontent\.com|github\.com\/user-attachments\/assets)\/[^\s"'<>]+|[^\s"'<>]+\.(?:png|jpg|jpeg|webp|gif)(?:\?[^\s"'<>]*)?))/gi;
164
+ const imageUrlPattern = /(?:data:image\/[a-zA-Z]*;base64,[A-Za-z0-9+/=]+|https?:\/\/(?:(?:private-user-images\.githubusercontent\.com|github\.com\/user-attachments\/assets)\/[^\s"'<>]+|[^\s"'<>]+\.(?:png|jpg|jpeg|webp|gif)(?:\?[^\s"'<>]*)?)|(?:\.?\.?\/)?[^\s"'<>]*\.(?:png|jpg|jpeg|webp|gif))/gi;
60
165
 
61
166
  span.setAttributes({
62
167
  'message.length': message.length,
@@ -69,31 +174,57 @@ function extractImageUrls(message, debug = false) {
69
174
  }
70
175
 
71
176
  const urls = [];
177
+ const foundPatterns = [];
72
178
  let match;
73
179
 
74
180
  while ((match = imageUrlPattern.exec(message)) !== null) {
75
- urls.push(match[0]);
181
+ foundPatterns.push(match[0]);
76
182
  if (debug) {
77
- console.log(`[DEBUG] Found image URL: ${match[0]}`);
183
+ console.log(`[DEBUG] Found image pattern: ${match[0]}`);
184
+ }
185
+ }
186
+
187
+ // Process each found pattern - convert local files to base64, keep URLs as-is
188
+ for (const pattern of foundPatterns) {
189
+ // Check if it's already a URL or base64 data
190
+ if (pattern.startsWith('http') || pattern.startsWith('data:image/')) {
191
+ urls.push(pattern);
192
+ if (debug) {
193
+ console.log(`[DEBUG] Using URL/base64 as-is: ${pattern.substring(0, 50)}...`);
194
+ }
195
+ } else {
196
+ // It's a local file path - convert to base64
197
+ const base64Data = await convertImageFileToBase64(pattern, debug);
198
+ if (base64Data) {
199
+ urls.push(base64Data);
200
+ if (debug) {
201
+ console.log(`[DEBUG] Converted local file ${pattern} to base64`);
202
+ }
203
+ } else {
204
+ if (debug) {
205
+ console.log(`[DEBUG] Failed to convert local file: ${pattern}`);
206
+ }
207
+ }
78
208
  }
79
209
  }
80
210
 
81
- // Clean the message by removing found URLs
211
+ // Clean the message by removing found patterns
82
212
  let cleanedMessage = message;
83
- urls.forEach(url => {
84
- cleanedMessage = cleanedMessage.replace(url, '').trim();
213
+ foundPatterns.forEach(pattern => {
214
+ cleanedMessage = cleanedMessage.replace(pattern, '').trim();
85
215
  });
86
216
 
87
217
  // Clean up any remaining extra whitespace
88
218
  cleanedMessage = cleanedMessage.replace(/\s+/g, ' ').trim();
89
219
 
90
220
  span.setAttributes({
91
- 'images.found': urls.length,
221
+ 'patterns.found': foundPatterns.length,
222
+ 'images.processed': urls.length,
92
223
  'message.cleaned_length': cleanedMessage.length
93
224
  });
94
225
 
95
226
  if (debug) {
96
- console.log(`[DEBUG] Extracted ${urls.length} image URLs`);
227
+ console.log(`[DEBUG] Found ${foundPatterns.length} patterns, processed ${urls.length} images`);
97
228
  console.log(`[DEBUG] Cleaned message length: ${cleanedMessage.length}`);
98
229
  }
99
230
 
@@ -163,7 +294,7 @@ export class ProbeChat {
163
294
  let cleanedMessage = message;
164
295
 
165
296
  if (!images.length) {
166
- const extracted = extractImageUrls(message, this.debug);
297
+ const extracted = await extractImageUrls(message, this.debug);
167
298
  images = extracted.urls;
168
299
  cleanedMessage = extracted.cleanedMessage;
169
300