@probelabs/probe 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.
package/cjs/index.cjs CHANGED
@@ -1543,7 +1543,7 @@ async function delegate({ task, timeout = 300, debug = false, currentIteration =
1543
1543
  console.error(`[DELEGATE] Using binary at: ${binaryPath}`);
1544
1544
  console.error(`[DELEGATE] Command args: ${args.join(" ")}`);
1545
1545
  }
1546
- return new Promise((resolve, reject) => {
1546
+ return new Promise((resolve2, reject) => {
1547
1547
  const delegationSpan = tracer ? tracer.createDelegationSpan(sessionId, task) : null;
1548
1548
  const process2 = (0, import_child_process5.spawn)(binaryPath, args, {
1549
1549
  stdio: ["pipe", "pipe", "pipe"],
@@ -1608,7 +1608,7 @@ async function delegate({ task, timeout = 300, debug = false, currentIteration =
1608
1608
  delegationSpan.end();
1609
1609
  }
1610
1610
  }
1611
- resolve(response);
1611
+ resolve2(response);
1612
1612
  } else {
1613
1613
  const errorMessage = stderr.trim() || `Delegate process failed with exit code ${code}`;
1614
1614
  if (debug) {
@@ -2868,7 +2868,7 @@ function createMockProvider() {
2868
2868
  provider: "mock",
2869
2869
  // Mock the doGenerate method used by Vercel AI SDK
2870
2870
  doGenerate: async ({ messages, tools: tools2 }) => {
2871
- await new Promise((resolve) => setTimeout(resolve, 10));
2871
+ await new Promise((resolve2) => setTimeout(resolve2, 10));
2872
2872
  return {
2873
2873
  text: "This is a mock response for testing",
2874
2874
  toolCalls: [],
@@ -4693,7 +4693,7 @@ var ProbeAgent_exports = {};
4693
4693
  __export(ProbeAgent_exports, {
4694
4694
  ProbeAgent: () => ProbeAgent
4695
4695
  });
4696
- var import_anthropic, import_openai, import_google, import_ai2, import_crypto5, import_events2, MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, ProbeAgent;
4696
+ var import_anthropic, import_openai, import_google, import_ai2, import_crypto5, import_events2, import_fs5, import_promises, import_path7, MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, SUPPORTED_IMAGE_EXTENSIONS, MAX_IMAGE_FILE_SIZE, ProbeAgent;
4697
4697
  var init_ProbeAgent = __esm({
4698
4698
  "src/agent/ProbeAgent.js"() {
4699
4699
  "use strict";
@@ -4703,6 +4703,9 @@ var init_ProbeAgent = __esm({
4703
4703
  import_ai2 = require("ai");
4704
4704
  import_crypto5 = require("crypto");
4705
4705
  import_events2 = require("events");
4706
+ import_fs5 = require("fs");
4707
+ import_promises = require("fs/promises");
4708
+ import_path7 = require("path");
4706
4709
  init_tokenCounter();
4707
4710
  init_tools2();
4708
4711
  init_common();
@@ -4713,6 +4716,8 @@ var init_ProbeAgent = __esm({
4713
4716
  init_mcp();
4714
4717
  MAX_TOOL_ITERATIONS = parseInt(process.env.MAX_TOOL_ITERATIONS || "30", 10);
4715
4718
  MAX_HISTORY_MESSAGES = 100;
4719
+ SUPPORTED_IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "gif", "bmp", "svg"];
4720
+ MAX_IMAGE_FILE_SIZE = 20 * 1024 * 1024;
4716
4721
  ProbeAgent = class {
4717
4722
  /**
4718
4723
  * Create a new ProbeAgent instance
@@ -4756,6 +4761,8 @@ var init_ProbeAgent = __esm({
4756
4761
  }
4757
4762
  this.initializeTools();
4758
4763
  this.history = [];
4764
+ this.pendingImages = /* @__PURE__ */ new Map();
4765
+ this.currentImages = [];
4759
4766
  this.events = new import_events2.EventEmitter();
4760
4767
  this.enableMcp = !!options.enableMcp || process.env.ENABLE_MCP === "1";
4761
4768
  this.mcpConfigPath = options.mcpConfigPath || null;
@@ -4881,6 +4888,175 @@ var init_ProbeAgent = __esm({
4881
4888
  console.log(`Using Google API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl})` : ""}`);
4882
4889
  }
4883
4890
  }
4891
+ /**
4892
+ * Process assistant response content and detect/load image references
4893
+ * @param {string} content - The assistant's response content
4894
+ * @returns {Promise<void>}
4895
+ */
4896
+ async processImageReferences(content) {
4897
+ if (!content) return;
4898
+ const extensionsPattern = `(?:${SUPPORTED_IMAGE_EXTENSIONS.join("|")})`;
4899
+ const imagePatterns = [
4900
+ // Direct file path mentions: "./screenshot.png", "/path/to/image.jpg", etc.
4901
+ new RegExp(`(?:\\.?\\.\\/)?[^\\s"'<>\\[\\]]+\\.${extensionsPattern}(?!\\w)`, "gi"),
4902
+ // Contextual mentions: "look at image.png", "the file screenshot.jpg shows"
4903
+ new RegExp(`(?:image|file|screenshot|diagram|photo|picture|graphic)\\s*:?\\s*([^\\s"'<>\\[\\]]+\\.${extensionsPattern})(?!\\w)`, "gi"),
4904
+ // Tool result mentions: often contain file paths
4905
+ new RegExp(`(?:found|saved|created|generated).*?([^\\s"'<>\\[\\]]+\\.${extensionsPattern})(?!\\w)`, "gi")
4906
+ ];
4907
+ const foundPaths = /* @__PURE__ */ new Set();
4908
+ for (const pattern of imagePatterns) {
4909
+ let match;
4910
+ while ((match = pattern.exec(content)) !== null) {
4911
+ const imagePath = match[1] || match[0];
4912
+ if (imagePath && imagePath.length > 0) {
4913
+ foundPaths.add(imagePath.trim());
4914
+ }
4915
+ }
4916
+ }
4917
+ if (foundPaths.size === 0) return;
4918
+ if (this.debug) {
4919
+ console.log(`[DEBUG] Found ${foundPaths.size} potential image references:`, Array.from(foundPaths));
4920
+ }
4921
+ for (const imagePath of foundPaths) {
4922
+ await this.loadImageIfValid(imagePath);
4923
+ }
4924
+ }
4925
+ /**
4926
+ * Load and cache an image if it's valid and accessible
4927
+ * @param {string} imagePath - Path to the image file
4928
+ * @returns {Promise<boolean>} - True if image was loaded successfully
4929
+ */
4930
+ async loadImageIfValid(imagePath) {
4931
+ try {
4932
+ if (this.pendingImages.has(imagePath)) {
4933
+ if (this.debug) {
4934
+ console.log(`[DEBUG] Image already loaded: ${imagePath}`);
4935
+ }
4936
+ return true;
4937
+ }
4938
+ const allowedDirs = this.allowedFolders && this.allowedFolders.length > 0 ? this.allowedFolders : [process.cwd()];
4939
+ let absolutePath;
4940
+ let isPathAllowed = false;
4941
+ if ((0, import_path7.isAbsolute)(imagePath)) {
4942
+ absolutePath = imagePath;
4943
+ isPathAllowed = allowedDirs.some((dir) => absolutePath.startsWith((0, import_path7.resolve)(dir)));
4944
+ } else {
4945
+ for (const dir of allowedDirs) {
4946
+ const resolvedPath = (0, import_path7.resolve)(dir, imagePath);
4947
+ if (resolvedPath.startsWith((0, import_path7.resolve)(dir))) {
4948
+ absolutePath = resolvedPath;
4949
+ isPathAllowed = true;
4950
+ break;
4951
+ }
4952
+ }
4953
+ }
4954
+ if (!isPathAllowed) {
4955
+ if (this.debug) {
4956
+ console.log(`[DEBUG] Image path outside allowed directories: ${imagePath}`);
4957
+ }
4958
+ return false;
4959
+ }
4960
+ let fileStats;
4961
+ try {
4962
+ fileStats = await (0, import_promises.stat)(absolutePath);
4963
+ } catch (error) {
4964
+ if (this.debug) {
4965
+ console.log(`[DEBUG] Image file not found: ${absolutePath}`);
4966
+ }
4967
+ return false;
4968
+ }
4969
+ if (fileStats.size > MAX_IMAGE_FILE_SIZE) {
4970
+ if (this.debug) {
4971
+ console.log(`[DEBUG] Image file too large: ${absolutePath} (${fileStats.size} bytes, max: ${MAX_IMAGE_FILE_SIZE})`);
4972
+ }
4973
+ return false;
4974
+ }
4975
+ const extension = absolutePath.toLowerCase().split(".").pop();
4976
+ if (!SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
4977
+ if (this.debug) {
4978
+ console.log(`[DEBUG] Unsupported image format: ${extension}`);
4979
+ }
4980
+ return false;
4981
+ }
4982
+ const mimeTypes = {
4983
+ "png": "image/png",
4984
+ "jpg": "image/jpeg",
4985
+ "jpeg": "image/jpeg",
4986
+ "webp": "image/webp",
4987
+ "gif": "image/gif",
4988
+ "bmp": "image/bmp",
4989
+ "svg": "image/svg+xml"
4990
+ };
4991
+ const mimeType = mimeTypes[extension];
4992
+ const fileBuffer = await (0, import_promises.readFile)(absolutePath);
4993
+ const base64Data = fileBuffer.toString("base64");
4994
+ const dataUrl = `data:${mimeType};base64,${base64Data}`;
4995
+ this.pendingImages.set(imagePath, dataUrl);
4996
+ if (this.debug) {
4997
+ console.log(`[DEBUG] Successfully loaded image: ${imagePath} (${fileBuffer.length} bytes)`);
4998
+ }
4999
+ return true;
5000
+ } catch (error) {
5001
+ if (this.debug) {
5002
+ console.log(`[DEBUG] Failed to load image ${imagePath}: ${error.message}`);
5003
+ }
5004
+ return false;
5005
+ }
5006
+ }
5007
+ /**
5008
+ * Get all currently loaded images as an array for AI model consumption
5009
+ * @returns {Array<string>} - Array of base64 data URLs
5010
+ */
5011
+ getCurrentImages() {
5012
+ return Array.from(this.pendingImages.values());
5013
+ }
5014
+ /**
5015
+ * Clear loaded images (useful for new conversations)
5016
+ */
5017
+ clearLoadedImages() {
5018
+ this.pendingImages.clear();
5019
+ this.currentImages = [];
5020
+ if (this.debug) {
5021
+ console.log("[DEBUG] Cleared all loaded images");
5022
+ }
5023
+ }
5024
+ /**
5025
+ * Prepare messages for AI consumption, adding images to the latest user message if available
5026
+ * @param {Array} messages - Current conversation messages
5027
+ * @returns {Array} - Messages formatted for AI SDK with potential image content
5028
+ */
5029
+ prepareMessagesWithImages(messages) {
5030
+ const loadedImages = this.getCurrentImages();
5031
+ if (loadedImages.length === 0) {
5032
+ return messages;
5033
+ }
5034
+ const messagesWithImages = [...messages];
5035
+ const lastUserMessageIndex = messagesWithImages.map((m) => m.role).lastIndexOf("user");
5036
+ if (lastUserMessageIndex === -1) {
5037
+ if (this.debug) {
5038
+ console.log("[DEBUG] No user messages found to attach images to");
5039
+ }
5040
+ return messages;
5041
+ }
5042
+ const lastUserMessage = messagesWithImages[lastUserMessageIndex];
5043
+ if (typeof lastUserMessage.content === "string") {
5044
+ messagesWithImages[lastUserMessageIndex] = {
5045
+ ...lastUserMessage,
5046
+ content: [
5047
+ { type: "text", text: lastUserMessage.content },
5048
+ ...loadedImages.map((imageData) => ({
5049
+ type: "image",
5050
+ image: imageData
5051
+ }))
5052
+ ]
5053
+ };
5054
+ if (this.debug) {
5055
+ console.log(`[DEBUG] Added ${loadedImages.length} images to the latest user message`);
5056
+ }
5057
+ }
5058
+ return messagesWithImages;
5059
+ }
4884
5060
  /**
4885
5061
  * Initialize mock model for testing
4886
5062
  */
@@ -5262,9 +5438,10 @@ You are working with a repository located at: ${searchDirectory}
5262
5438
  let assistantResponseContent = "";
5263
5439
  try {
5264
5440
  const executeAIRequest = async () => {
5441
+ const messagesForAI = this.prepareMessagesWithImages(currentMessages);
5265
5442
  const result = await (0, import_ai2.streamText)({
5266
5443
  model: this.provider(this.model),
5267
- messages: currentMessages,
5444
+ messages: messagesForAI,
5268
5445
  maxTokens: maxResponseTokens,
5269
5446
  temperature: 0.3
5270
5447
  });
@@ -5298,6 +5475,9 @@ You are working with a repository located at: ${searchDirectory}
5298
5475
  const assistantPreview = createMessagePreview(assistantResponseContent);
5299
5476
  console.log(`[DEBUG] Assistant response (${assistantResponseContent.length} chars): ${assistantPreview}`);
5300
5477
  }
5478
+ if (assistantResponseContent) {
5479
+ await this.processImageReferences(assistantResponseContent);
5480
+ }
5301
5481
  const validTools = [
5302
5482
  "search",
5303
5483
  "query",
@@ -5422,12 +5602,17 @@ ${toolResultContent}
5422
5602
  throw toolError;
5423
5603
  }
5424
5604
  currentMessages.push({ role: "assistant", content: assistantResponseContent });
5605
+ const toolResultContent = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult, null, 2);
5606
+ const toolResultMessage = `<tool_result>
5607
+ ${toolResultContent}
5608
+ </tool_result>`;
5425
5609
  currentMessages.push({
5426
5610
  role: "user",
5427
- content: `<tool_result>
5428
- ${typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult, null, 2)}
5429
- </tool_result>`
5611
+ content: toolResultMessage
5430
5612
  });
5613
+ if (toolResultContent) {
5614
+ await this.processImageReferences(toolResultContent);
5615
+ }
5431
5616
  if (this.debug) {
5432
5617
  console.log(`[DEBUG] Tool ${toolName} executed successfully. Result length: ${typeof toolResult === "string" ? toolResult.length : JSON.stringify(toolResult).length}`);
5433
5618
  }
@@ -5925,12 +6110,12 @@ function initializeSimpleTelemetryFromOptions(options) {
5925
6110
  });
5926
6111
  return telemetry;
5927
6112
  }
5928
- var import_fs5, import_path7, SimpleTelemetry, SimpleAppTracer;
6113
+ var import_fs6, import_path8, SimpleTelemetry, SimpleAppTracer;
5929
6114
  var init_simpleTelemetry = __esm({
5930
6115
  "src/agent/simpleTelemetry.js"() {
5931
6116
  "use strict";
5932
- import_fs5 = require("fs");
5933
- import_path7 = require("path");
6117
+ import_fs6 = require("fs");
6118
+ import_path8 = require("path");
5934
6119
  SimpleTelemetry = class {
5935
6120
  constructor(options = {}) {
5936
6121
  this.serviceName = options.serviceName || "probe-agent";
@@ -5944,11 +6129,11 @@ var init_simpleTelemetry = __esm({
5944
6129
  }
5945
6130
  initializeFileExporter() {
5946
6131
  try {
5947
- const dir = (0, import_path7.dirname)(this.filePath);
5948
- if (!(0, import_fs5.existsSync)(dir)) {
5949
- (0, import_fs5.mkdirSync)(dir, { recursive: true });
6132
+ const dir = (0, import_path8.dirname)(this.filePath);
6133
+ if (!(0, import_fs6.existsSync)(dir)) {
6134
+ (0, import_fs6.mkdirSync)(dir, { recursive: true });
5950
6135
  }
5951
- this.stream = (0, import_fs5.createWriteStream)(this.filePath, { flags: "a" });
6136
+ this.stream = (0, import_fs6.createWriteStream)(this.filePath, { flags: "a" });
5952
6137
  this.stream.on("error", (error) => {
5953
6138
  console.error(`[SimpleTelemetry] Stream error: ${error.message}`);
5954
6139
  });
@@ -6009,20 +6194,20 @@ var init_simpleTelemetry = __esm({
6009
6194
  }
6010
6195
  async flush() {
6011
6196
  if (this.stream) {
6012
- return new Promise((resolve) => {
6013
- this.stream.once("drain", resolve);
6197
+ return new Promise((resolve2) => {
6198
+ this.stream.once("drain", resolve2);
6014
6199
  if (!this.stream.writableNeedDrain) {
6015
- resolve();
6200
+ resolve2();
6016
6201
  }
6017
6202
  });
6018
6203
  }
6019
6204
  }
6020
6205
  async shutdown() {
6021
6206
  if (this.stream) {
6022
- return new Promise((resolve) => {
6207
+ return new Promise((resolve2) => {
6023
6208
  this.stream.end(() => {
6024
6209
  console.log(`[SimpleTelemetry] File stream closed: ${this.filePath}`);
6025
- resolve();
6210
+ resolve2();
6026
6211
  });
6027
6212
  });
6028
6213
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc102",
3
+ "version": "0.6.0-rc103",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -5,6 +5,9 @@ import { createGoogleGenerativeAI } from '@ai-sdk/google';
5
5
  import { streamText } from 'ai';
6
6
  import { randomUUID } from 'crypto';
7
7
  import { EventEmitter } from 'events';
8
+ import { existsSync } from 'fs';
9
+ import { readFile, stat } from 'fs/promises';
10
+ import { resolve, isAbsolute } from 'path';
8
11
  import { TokenCounter } from './tokenCounter.js';
9
12
  import {
10
13
  createTools,
@@ -46,6 +49,12 @@ import {
46
49
  const MAX_TOOL_ITERATIONS = parseInt(process.env.MAX_TOOL_ITERATIONS || '30', 10);
47
50
  const MAX_HISTORY_MESSAGES = 100;
48
51
 
52
+ // Supported image file extensions
53
+ const SUPPORTED_IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'svg'];
54
+
55
+ // Maximum image file size (20MB) to prevent OOM attacks
56
+ const MAX_IMAGE_FILE_SIZE = 20 * 1024 * 1024;
57
+
49
58
  /**
50
59
  * ProbeAgent class to handle AI interactions with code search capabilities
51
60
  */
@@ -105,6 +114,10 @@ export class ProbeAgent {
105
114
  // Initialize chat history
106
115
  this.history = [];
107
116
 
117
+ // Initialize image tracking for agentic loop
118
+ this.pendingImages = new Map(); // Map<imagePath, base64Data> to avoid reloading
119
+ this.currentImages = []; // Currently active images for AI calls
120
+
108
121
  // Initialize event emitter for tool execution updates
109
122
  this.events = new EventEmitter();
110
123
 
@@ -268,6 +281,226 @@ export class ProbeAgent {
268
281
  }
269
282
  }
270
283
 
284
+ /**
285
+ * Process assistant response content and detect/load image references
286
+ * @param {string} content - The assistant's response content
287
+ * @returns {Promise<void>}
288
+ */
289
+ async processImageReferences(content) {
290
+ if (!content) return;
291
+
292
+ // Enhanced pattern to detect image file mentions in various contexts
293
+ // Looks for: "image", "file", "screenshot", etc. followed by path-like strings with image extensions
294
+ const extensionsPattern = `(?:${SUPPORTED_IMAGE_EXTENSIONS.join('|')})`;
295
+ const imagePatterns = [
296
+ // Direct file path mentions: "./screenshot.png", "/path/to/image.jpg", etc.
297
+ new RegExp(`(?:\\.?\\.\\/)?[^\\s"'<>\\[\\]]+\\\.${extensionsPattern}(?!\\w)`, 'gi'),
298
+ // Contextual mentions: "look at image.png", "the file screenshot.jpg shows"
299
+ new RegExp(`(?:image|file|screenshot|diagram|photo|picture|graphic)\\s*:?\\s*([^\\s"'<>\\[\\]]+\\.${extensionsPattern})(?!\\w)`, 'gi'),
300
+ // Tool result mentions: often contain file paths
301
+ new RegExp(`(?:found|saved|created|generated).*?([^\\s"'<>\\[\\]]+\\.${extensionsPattern})(?!\\w)`, 'gi')
302
+ ];
303
+
304
+ const foundPaths = new Set();
305
+
306
+ // Extract potential image paths using all patterns
307
+ for (const pattern of imagePatterns) {
308
+ let match;
309
+ while ((match = pattern.exec(content)) !== null) {
310
+ // For patterns with capture groups, use the captured path; otherwise use the full match
311
+ const imagePath = match[1] || match[0];
312
+ if (imagePath && imagePath.length > 0) {
313
+ foundPaths.add(imagePath.trim());
314
+ }
315
+ }
316
+ }
317
+
318
+ if (foundPaths.size === 0) return;
319
+
320
+ if (this.debug) {
321
+ console.log(`[DEBUG] Found ${foundPaths.size} potential image references:`, Array.from(foundPaths));
322
+ }
323
+
324
+ // Process each found path
325
+ for (const imagePath of foundPaths) {
326
+ await this.loadImageIfValid(imagePath);
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Load and cache an image if it's valid and accessible
332
+ * @param {string} imagePath - Path to the image file
333
+ * @returns {Promise<boolean>} - True if image was loaded successfully
334
+ */
335
+ async loadImageIfValid(imagePath) {
336
+ try {
337
+ // Skip if already loaded
338
+ if (this.pendingImages.has(imagePath)) {
339
+ if (this.debug) {
340
+ console.log(`[DEBUG] Image already loaded: ${imagePath}`);
341
+ }
342
+ return true;
343
+ }
344
+
345
+ // Security validation: check if path is within any allowed directory
346
+ const allowedDirs = this.allowedFolders && this.allowedFolders.length > 0 ? this.allowedFolders : [process.cwd()];
347
+
348
+ let absolutePath;
349
+ let isPathAllowed = false;
350
+
351
+ // If absolute path, check if it's within any allowed directory
352
+ if (isAbsolute(imagePath)) {
353
+ absolutePath = imagePath;
354
+ isPathAllowed = allowedDirs.some(dir => absolutePath.startsWith(resolve(dir)));
355
+ } else {
356
+ // For relative paths, try resolving against each allowed directory
357
+ for (const dir of allowedDirs) {
358
+ const resolvedPath = resolve(dir, imagePath);
359
+ if (resolvedPath.startsWith(resolve(dir))) {
360
+ absolutePath = resolvedPath;
361
+ isPathAllowed = true;
362
+ break;
363
+ }
364
+ }
365
+ }
366
+
367
+ // Security check: ensure path is within at least one allowed directory
368
+ if (!isPathAllowed) {
369
+ if (this.debug) {
370
+ console.log(`[DEBUG] Image path outside allowed directories: ${imagePath}`);
371
+ }
372
+ return false;
373
+ }
374
+
375
+ // Check if file exists and get file stats
376
+ let fileStats;
377
+ try {
378
+ fileStats = await stat(absolutePath);
379
+ } catch (error) {
380
+ if (this.debug) {
381
+ console.log(`[DEBUG] Image file not found: ${absolutePath}`);
382
+ }
383
+ return false;
384
+ }
385
+
386
+ // Validate file size to prevent OOM attacks
387
+ if (fileStats.size > MAX_IMAGE_FILE_SIZE) {
388
+ if (this.debug) {
389
+ console.log(`[DEBUG] Image file too large: ${absolutePath} (${fileStats.size} bytes, max: ${MAX_IMAGE_FILE_SIZE})`);
390
+ }
391
+ return false;
392
+ }
393
+
394
+ // Validate file extension
395
+ const extension = absolutePath.toLowerCase().split('.').pop();
396
+ if (!SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
397
+ if (this.debug) {
398
+ console.log(`[DEBUG] Unsupported image format: ${extension}`);
399
+ }
400
+ return false;
401
+ }
402
+
403
+ // Determine MIME type
404
+ const mimeTypes = {
405
+ 'png': 'image/png',
406
+ 'jpg': 'image/jpeg',
407
+ 'jpeg': 'image/jpeg',
408
+ 'webp': 'image/webp',
409
+ 'gif': 'image/gif',
410
+ 'bmp': 'image/bmp',
411
+ 'svg': 'image/svg+xml'
412
+ };
413
+ const mimeType = mimeTypes[extension];
414
+
415
+ // Read and encode file asynchronously
416
+ const fileBuffer = await readFile(absolutePath);
417
+ const base64Data = fileBuffer.toString('base64');
418
+ const dataUrl = `data:${mimeType};base64,${base64Data}`;
419
+
420
+ // Cache the loaded image
421
+ this.pendingImages.set(imagePath, dataUrl);
422
+
423
+ if (this.debug) {
424
+ console.log(`[DEBUG] Successfully loaded image: ${imagePath} (${fileBuffer.length} bytes)`);
425
+ }
426
+
427
+ return true;
428
+ } catch (error) {
429
+ if (this.debug) {
430
+ console.log(`[DEBUG] Failed to load image ${imagePath}: ${error.message}`);
431
+ }
432
+ return false;
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Get all currently loaded images as an array for AI model consumption
438
+ * @returns {Array<string>} - Array of base64 data URLs
439
+ */
440
+ getCurrentImages() {
441
+ return Array.from(this.pendingImages.values());
442
+ }
443
+
444
+ /**
445
+ * Clear loaded images (useful for new conversations)
446
+ */
447
+ clearLoadedImages() {
448
+ this.pendingImages.clear();
449
+ this.currentImages = [];
450
+ if (this.debug) {
451
+ console.log('[DEBUG] Cleared all loaded images');
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Prepare messages for AI consumption, adding images to the latest user message if available
457
+ * @param {Array} messages - Current conversation messages
458
+ * @returns {Array} - Messages formatted for AI SDK with potential image content
459
+ */
460
+ prepareMessagesWithImages(messages) {
461
+ const loadedImages = this.getCurrentImages();
462
+
463
+ // If no images loaded, return messages as-is
464
+ if (loadedImages.length === 0) {
465
+ return messages;
466
+ }
467
+
468
+ // Clone messages to avoid mutating the original
469
+ const messagesWithImages = [...messages];
470
+
471
+ // Find the last user message to attach images to
472
+ const lastUserMessageIndex = messagesWithImages.map(m => m.role).lastIndexOf('user');
473
+
474
+ if (lastUserMessageIndex === -1) {
475
+ if (this.debug) {
476
+ console.log('[DEBUG] No user messages found to attach images to');
477
+ }
478
+ return messages;
479
+ }
480
+
481
+ const lastUserMessage = messagesWithImages[lastUserMessageIndex];
482
+
483
+ // Convert to multimodal format if we have images
484
+ if (typeof lastUserMessage.content === 'string') {
485
+ messagesWithImages[lastUserMessageIndex] = {
486
+ ...lastUserMessage,
487
+ content: [
488
+ { type: 'text', text: lastUserMessage.content },
489
+ ...loadedImages.map(imageData => ({
490
+ type: 'image',
491
+ image: imageData
492
+ }))
493
+ ]
494
+ };
495
+
496
+ if (this.debug) {
497
+ console.log(`[DEBUG] Added ${loadedImages.length} images to the latest user message`);
498
+ }
499
+ }
500
+
501
+ return messagesWithImages;
502
+ }
503
+
271
504
  /**
272
505
  * Initialize mock model for testing
273
506
  */
@@ -695,9 +928,12 @@ When troubleshooting:
695
928
  try {
696
929
  // Wrap AI request with tracing if available
697
930
  const executeAIRequest = async () => {
931
+ // Prepare messages with potential image content
932
+ const messagesForAI = this.prepareMessagesWithImages(currentMessages);
933
+
698
934
  const result = await streamText({
699
935
  model: this.provider(this.model),
700
- messages: currentMessages,
936
+ messages: messagesForAI,
701
937
  maxTokens: maxResponseTokens,
702
938
  temperature: 0.3,
703
939
  });
@@ -741,6 +977,11 @@ When troubleshooting:
741
977
  console.log(`[DEBUG] Assistant response (${assistantResponseContent.length} chars): ${assistantPreview}`);
742
978
  }
743
979
 
980
+ // Process image references in assistant response for next iteration
981
+ if (assistantResponseContent) {
982
+ await this.processImageReferences(assistantResponseContent);
983
+ }
984
+
744
985
  // Parse tool call from response with valid tools list
745
986
  const validTools = [
746
987
  'search', 'query', 'extract', 'listFiles', 'searchFiles', 'attempt_completion'
@@ -898,11 +1139,20 @@ When troubleshooting:
898
1139
 
899
1140
  // Add assistant response and tool result to conversation
900
1141
  currentMessages.push({ role: 'assistant', content: assistantResponseContent });
1142
+
1143
+ const toolResultContent = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult, null, 2);
1144
+ const toolResultMessage = `<tool_result>\n${toolResultContent}\n</tool_result>`;
1145
+
901
1146
  currentMessages.push({
902
1147
  role: 'user',
903
- content: `<tool_result>\n${typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult, null, 2)}\n</tool_result>`
1148
+ content: toolResultMessage
904
1149
  });
905
1150
 
1151
+ // Process tool result for image references
1152
+ if (toolResultContent) {
1153
+ await this.processImageReferences(toolResultContent);
1154
+ }
1155
+
906
1156
  if (this.debug) {
907
1157
  console.log(`[DEBUG] Tool ${toolName} executed successfully. Result length: ${typeof toolResult === 'string' ? toolResult.length : JSON.stringify(toolResult).length}`);
908
1158
  }