@probelabs/probe 0.6.0-rc164 → 0.6.0-rc166

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc164",
3
+ "version": "0.6.0-rc166",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -27,6 +27,7 @@ import {
27
27
  bashToolDefinition,
28
28
  listFilesToolDefinition,
29
29
  searchFilesToolDefinition,
30
+ readImageToolDefinition,
30
31
  attemptCompletionToolDefinition,
31
32
  implementToolDefinition,
32
33
  editToolDefinition,
@@ -399,6 +400,23 @@ export class ProbeAgent {
399
400
  delegate: wrappedTools.delegateToolInstance,
400
401
  listFiles: listFilesToolInstance,
401
402
  searchFiles: searchFilesToolInstance,
403
+ readImage: {
404
+ execute: async (params) => {
405
+ const imagePath = params.path;
406
+ if (!imagePath) {
407
+ throw new Error('Image path is required');
408
+ }
409
+
410
+ // Load the image using the existing loadImageIfValid method
411
+ const loaded = await this.loadImageIfValid(imagePath);
412
+
413
+ if (!loaded) {
414
+ 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.`);
415
+ }
416
+
417
+ return `Image loaded successfully: ${imagePath}. The image is now available for analysis in the conversation.`;
418
+ }
419
+ }
402
420
  };
403
421
 
404
422
  // Add bash tool if enabled
@@ -1172,6 +1190,9 @@ export class ProbeAgent {
1172
1190
  if (isToolAllowed('searchFiles')) {
1173
1191
  toolDefinitions += `${searchFilesToolDefinition}\n`;
1174
1192
  }
1193
+ if (isToolAllowed('readImage')) {
1194
+ toolDefinitions += `${readImageToolDefinition}\n`;
1195
+ }
1175
1196
 
1176
1197
  // Edit tools (require both allowEdit flag AND allowedTools permission)
1177
1198
  if (this.allowEdit && isToolAllowed('implement')) {
@@ -1262,6 +1283,7 @@ Available Tools:
1262
1283
  - extract: Extract specific code blocks or lines from files.
1263
1284
  - listFiles: List files and directories in a specified location.
1264
1285
  - searchFiles: Find files matching a glob pattern with recursive search capability.
1286
+ - readImage: Read and load an image file for AI analysis.
1265
1287
  ${this.allowEdit ? '- implement: Implement a feature or fix a bug using aider.\n- edit: Edit files using exact string replacement.\n- create: Create new files with specified content.\n' : ''}${this.enableDelegate ? '- delegate: Delegate big distinct tasks to specialized probe subagents.\n' : ''}${this.enableBash ? '- bash: Execute bash commands for system operations.\n' : ''}
1266
1288
  - attempt_completion: Finalize the task and provide the result to the user.
1267
1289
  - attempt_complete: Quick completion using previous response (shorthand).
@@ -1700,22 +1722,30 @@ When troubleshooting:
1700
1722
  console.log(`[DEBUG] Assistant response (${assistantResponseContent.length} chars): ${assistantPreview}`);
1701
1723
  }
1702
1724
 
1703
- // Process image references in assistant response for next iteration
1704
- if (assistantResponseContent) {
1705
- await this.processImageReferences(assistantResponseContent);
1706
- }
1725
+ // Images in assistant responses are not automatically processed
1726
+ // AI can use the readImage tool to explicitly request reading an image
1707
1727
 
1708
1728
  // Parse tool call from response with valid tools list
1709
- const validTools = [
1710
- 'search', 'query', 'extract', 'listFiles', 'searchFiles', 'attempt_completion'
1711
- ];
1712
- if (this.allowEdit) {
1729
+ // Build validTools based on allowedTools configuration (same pattern as getSystemMessage)
1730
+ const validTools = [];
1731
+ if (this.allowedTools.isEnabled('search')) validTools.push('search');
1732
+ if (this.allowedTools.isEnabled('query')) validTools.push('query');
1733
+ if (this.allowedTools.isEnabled('extract')) validTools.push('extract');
1734
+ if (this.allowedTools.isEnabled('listFiles')) validTools.push('listFiles');
1735
+ if (this.allowedTools.isEnabled('searchFiles')) validTools.push('searchFiles');
1736
+ if (this.allowedTools.isEnabled('readImage')) validTools.push('readImage');
1737
+ if (this.allowedTools.isEnabled('attempt_completion')) validTools.push('attempt_completion');
1738
+
1739
+ // Edit tools (require both allowEdit flag AND allowedTools permission)
1740
+ if (this.allowEdit && this.allowedTools.isEnabled('implement')) {
1713
1741
  validTools.push('implement', 'edit', 'create');
1714
1742
  }
1715
- if (this.enableBash) {
1743
+ // Bash tool (require both enableBash flag AND allowedTools permission)
1744
+ if (this.enableBash && this.allowedTools.isEnabled('bash')) {
1716
1745
  validTools.push('bash');
1717
1746
  }
1718
- if (this.enableDelegate) {
1747
+ // Delegate tool (require both enableDelegate flag AND allowedTools permission)
1748
+ if (this.enableDelegate && this.allowedTools.isEnabled('delegate')) {
1719
1749
  validTools.push('delegate');
1720
1750
  }
1721
1751
 
@@ -165,6 +165,74 @@ export function decodeHtmlEntities(text) {
165
165
  return decoded;
166
166
  }
167
167
 
168
+ /**
169
+ * Normalize JavaScript syntax to valid JSON syntax
170
+ * Converts single quotes to double quotes for strings in JSON-like structures
171
+ *
172
+ * @param {string} str - String that might contain JavaScript array/object syntax
173
+ * @returns {string} - String with single quotes normalized to double quotes
174
+ */
175
+ function normalizeJsonQuotes(str) {
176
+ if (!str || typeof str !== 'string') {
177
+ return str;
178
+ }
179
+
180
+ // Quick check: if there are no single quotes, no need to normalize
181
+ if (!str.includes("'")) {
182
+ return str;
183
+ }
184
+
185
+ let result = '';
186
+ let inDoubleQuote = false;
187
+ let inSingleQuote = false;
188
+ let escaped = false;
189
+
190
+ for (let i = 0; i < str.length; i++) {
191
+ const char = str[i];
192
+ const prevChar = i > 0 ? str[i - 1] : '';
193
+
194
+ // Handle escape sequences
195
+ if (escaped) {
196
+ result += char;
197
+ escaped = false;
198
+ continue;
199
+ }
200
+
201
+ if (char === '\\') {
202
+ escaped = true;
203
+ result += char;
204
+ continue;
205
+ }
206
+
207
+ // Track when we're inside double-quoted strings
208
+ if (char === '"' && !inSingleQuote) {
209
+ inDoubleQuote = !inDoubleQuote;
210
+ result += char;
211
+ continue;
212
+ }
213
+
214
+ // Convert single quotes to double quotes (when not inside double quotes)
215
+ if (char === "'" && !inDoubleQuote) {
216
+ // Check if this is a single quote inside a string value (like "It's")
217
+ // If we're already in a single-quoted string, toggle the state
218
+ if (inSingleQuote) {
219
+ // Closing single quote - convert to double quote
220
+ result += '"';
221
+ inSingleQuote = false;
222
+ } else {
223
+ // Opening single quote - convert to double quote
224
+ result += '"';
225
+ inSingleQuote = true;
226
+ }
227
+ continue;
228
+ }
229
+
230
+ result += char;
231
+ }
232
+
233
+ return result;
234
+ }
235
+
168
236
  /**
169
237
  * Clean AI response by extracting JSON content when response contains JSON
170
238
  * Only processes responses that contain JSON structures { or [
@@ -189,29 +257,36 @@ export function cleanSchemaResponse(response) {
189
257
  // Try with json language specifier
190
258
  const jsonBlockMatch = trimmed.match(/```json\s*\n([\s\S]*?)\n```/);
191
259
  if (jsonBlockMatch) {
192
- return jsonBlockMatch[1].trim();
260
+ return normalizeJsonQuotes(jsonBlockMatch[1].trim());
193
261
  }
194
262
 
195
263
  // Try any code block with JSON content
196
264
  const anyBlockMatch = trimmed.match(/```\s*\n([{\[][\s\S]*?[}\]])\s*```/);
197
265
  if (anyBlockMatch) {
198
- return anyBlockMatch[1].trim();
266
+ return normalizeJsonQuotes(anyBlockMatch[1].trim());
199
267
  }
200
268
 
201
269
  // Legacy patterns for more specific matching
202
270
  const codeBlockPatterns = [
203
271
  /```json\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/,
204
- /```\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/,
205
- /`([{\[][\s\S]*?[}\]])`/
272
+ /```\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/
206
273
  ];
207
274
 
208
275
  for (const pattern of codeBlockPatterns) {
209
276
  const match = trimmed.match(pattern);
210
277
  if (match) {
211
- return match[1].trim();
278
+ return normalizeJsonQuotes(match[1].trim());
212
279
  }
213
280
  }
214
281
 
282
+ // Single backtick pattern - ONLY if the entire input is just the code block
283
+ // This prevents extracting inline code from within JSON objects (e.g., `['*']` from markdown text)
284
+ const singleBacktickPattern = /^`([{\[][\s\S]*?[}\]])`$/;
285
+ const singleBacktickMatch = trimmed.match(singleBacktickPattern);
286
+ if (singleBacktickMatch) {
287
+ return normalizeJsonQuotes(singleBacktickMatch[1].trim());
288
+ }
289
+
215
290
  // Look for code block start followed immediately by JSON
216
291
  const codeBlockStartPattern = /```(?:json)?\s*\n?\s*([{\[])/;
217
292
  const codeBlockMatch = trimmed.match(codeBlockStartPattern);
@@ -236,7 +311,7 @@ export function cleanSchemaResponse(response) {
236
311
  }
237
312
 
238
313
  if (bracketCount === 0) {
239
- return trimmed.substring(startIndex, endIndex);
314
+ return normalizeJsonQuotes(trimmed.substring(startIndex, endIndex));
240
315
  }
241
316
  }
242
317
 
@@ -261,7 +336,8 @@ export function cleanSchemaResponse(response) {
261
336
  const isJsonArray = firstChar === '[' && lastChar === ']';
262
337
 
263
338
  if (isJsonObject || isJsonArray) {
264
- return cleaned;
339
+ // Normalize JavaScript syntax (single quotes) to valid JSON syntax (double quotes)
340
+ return normalizeJsonQuotes(cleaned);
265
341
  }
266
342
 
267
343
  return response; // Return original if no extractable JSON found
@@ -826,6 +902,76 @@ export function isMermaidSchema(schema) {
826
902
  return mermaidIndicators.some(indicator => indicator);
827
903
  }
828
904
 
905
+ /**
906
+ * Extract Mermaid diagrams from JSON string values
907
+ * Handles escaped newlines and backticks within JSON strings
908
+ * @param {string} response - Response that may contain JSON with mermaid blocks in string values
909
+ * @returns {Object} - {diagrams: Array, jsonPaths: Array, parsedJson: Object|null}
910
+ */
911
+ export function extractMermaidFromJson(response) {
912
+ if (!response || typeof response !== 'string') {
913
+ return { diagrams: [], jsonPaths: [], parsedJson: null };
914
+ }
915
+
916
+ // Try to extract JSON from code blocks first
917
+ let jsonContent = response.trim();
918
+ const jsonBlockMatch = jsonContent.match(/```json\s*\n([\s\S]*?)\n```/);
919
+ if (jsonBlockMatch) {
920
+ jsonContent = jsonBlockMatch[1].trim();
921
+ } else {
922
+ const anyBlockMatch = jsonContent.match(/```\s*\n([{\[][\s\S]*?[}\]])\s*```/);
923
+ if (anyBlockMatch) {
924
+ jsonContent = anyBlockMatch[1].trim();
925
+ }
926
+ }
927
+
928
+ // Try to parse as JSON
929
+ let parsedJson;
930
+ try {
931
+ parsedJson = JSON.parse(jsonContent);
932
+ } catch (e) {
933
+ return { diagrams: [], jsonPaths: [], parsedJson: null };
934
+ }
935
+
936
+ const diagrams = [];
937
+ const jsonPaths = [];
938
+
939
+ // Recursively search for mermaid diagrams in JSON string values
940
+ function searchObject(obj, path = []) {
941
+ if (typeof obj === 'string') {
942
+ // Look for mermaid code blocks in the string value
943
+ // Handle both escaped (\n) and literal newlines
944
+ const mermaidPattern = /```mermaid([^\n`]*?)(?:\n|\\n)([\s\S]*?)```/gi;
945
+ let match;
946
+
947
+ while ((match = mermaidPattern.exec(obj)) !== null) {
948
+ const attributes = match[1] ? match[1].trim() : '';
949
+ // Unescape the content (replace \\n with actual newlines)
950
+ const content = match[2].replace(/\\n/g, '\n');
951
+
952
+ diagrams.push({
953
+ content: content,
954
+ fullMatch: match[0],
955
+ startIndex: match.index,
956
+ endIndex: match.index + match[0].length,
957
+ attributes: attributes,
958
+ isInJson: true,
959
+ jsonPath: path.join('.')
960
+ });
961
+ jsonPaths.push(path.join('.'));
962
+ }
963
+ } else if (Array.isArray(obj)) {
964
+ obj.forEach((item, index) => searchObject(item, [...path, `[${index}]`]));
965
+ } else if (obj && typeof obj === 'object') {
966
+ Object.entries(obj).forEach(([key, value]) => searchObject(value, [...path, key]));
967
+ }
968
+ }
969
+
970
+ searchObject(parsedJson);
971
+
972
+ return { diagrams, jsonPaths, parsedJson };
973
+ }
974
+
829
975
  /**
830
976
  * Extract Mermaid diagrams from markdown code blocks with position tracking
831
977
  * @param {string} response - Response that may contain markdown with mermaid blocks
@@ -836,6 +982,16 @@ export function extractMermaidFromMarkdown(response) {
836
982
  return { diagrams: [], cleanedResponse: response };
837
983
  }
838
984
 
985
+ // First check if this looks like a JSON response - if so, use JSON-aware extraction
986
+ const trimmed = response.trim();
987
+ if ((trimmed.startsWith('{') || trimmed.startsWith('[')) ||
988
+ trimmed.includes('```json')) {
989
+ const jsonResult = extractMermaidFromJson(response);
990
+ if (jsonResult.diagrams.length > 0) {
991
+ return { diagrams: jsonResult.diagrams, cleanedResponse: response };
992
+ }
993
+ }
994
+
839
995
  // Find all mermaid code blocks with enhanced regex to capture more variations
840
996
  // This regex captures optional attributes on same line as ```mermaid, and all diagram content
841
997
  const mermaidBlockRegex = /```mermaid([^\n]*)\n([\s\S]*?)```/gi;
@@ -854,7 +1010,8 @@ export function extractMermaidFromMarkdown(response) {
854
1010
  fullMatch: match[0],
855
1011
  startIndex: match.index,
856
1012
  endIndex: match.index + match[0].length,
857
- attributes: attributes
1013
+ attributes: attributes,
1014
+ isInJson: false
858
1015
  });
859
1016
  }
860
1017
 
@@ -862,9 +1019,84 @@ export function extractMermaidFromMarkdown(response) {
862
1019
  return { diagrams, cleanedResponse: response };
863
1020
  }
864
1021
 
1022
+ /**
1023
+ * Replace mermaid diagrams in JSON string values with corrected versions
1024
+ * @param {string} originalResponse - Original response with JSON
1025
+ * @param {Array} correctedDiagrams - Array of corrected diagram objects with jsonPath
1026
+ * @returns {string} - Response with corrected diagrams properly escaped in JSON
1027
+ */
1028
+ export function replaceMermaidDiagramsInJson(originalResponse, correctedDiagrams) {
1029
+ if (!originalResponse || typeof originalResponse !== 'string') {
1030
+ return originalResponse;
1031
+ }
1032
+
1033
+ if (!correctedDiagrams || correctedDiagrams.length === 0) {
1034
+ return originalResponse;
1035
+ }
1036
+
1037
+ // Extract and parse JSON
1038
+ const jsonResult = extractMermaidFromJson(originalResponse);
1039
+ if (!jsonResult.parsedJson) {
1040
+ return originalResponse;
1041
+ }
1042
+
1043
+ let modifiedJson = jsonResult.parsedJson;
1044
+
1045
+ // Replace diagrams in the JSON object
1046
+ for (const diagram of correctedDiagrams) {
1047
+ if (!diagram.jsonPath || !diagram.isInJson) {
1048
+ continue;
1049
+ }
1050
+
1051
+ // Navigate to the path and replace the content
1052
+ const pathParts = diagram.jsonPath.split('.').filter(p => p);
1053
+ let current = modifiedJson;
1054
+
1055
+ for (let i = 0; i < pathParts.length - 1; i++) {
1056
+ const part = pathParts[i];
1057
+ if (part.startsWith('[') && part.endsWith(']')) {
1058
+ const index = parseInt(part.slice(1, -1), 10);
1059
+ current = current[index];
1060
+ } else {
1061
+ current = current[part];
1062
+ }
1063
+ }
1064
+
1065
+ // Get the last key/index
1066
+ const lastPart = pathParts[pathParts.length - 1];
1067
+ const attributesStr = diagram.attributes ? ` ${diagram.attributes}` : '';
1068
+ const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${diagram.content}\n\`\`\``;
1069
+
1070
+ if (lastPart.startsWith('[') && lastPart.endsWith(']')) {
1071
+ const index = parseInt(lastPart.slice(1, -1), 10);
1072
+ const originalString = current[index];
1073
+ // The fullMatch from extraction has unescaped newlines, so we need to match that
1074
+ current[index] = originalString.replace(diagram.fullMatch, newCodeBlock);
1075
+ } else {
1076
+ const originalString = current[lastPart];
1077
+ // The fullMatch from extraction has unescaped newlines, so we need to match that
1078
+ current[lastPart] = originalString.replace(diagram.fullMatch, newCodeBlock);
1079
+ }
1080
+ }
1081
+
1082
+ // Reconstruct the response with modified JSON
1083
+ const modifiedJsonString = JSON.stringify(modifiedJson, null, 2);
1084
+
1085
+ // Check if original was in a code block
1086
+ const trimmed = originalResponse.trim();
1087
+ if (trimmed.match(/```json\s*\n([\s\S]*?)\n```/)) {
1088
+ return originalResponse.replace(/```json\s*\n([\s\S]*?)\n```/, `\`\`\`json\n${modifiedJsonString}\n\`\`\``);
1089
+ } else if (trimmed.match(/```\s*\n([{\[][\s\S]*?[}\]])\s*```/)) {
1090
+ return originalResponse.replace(/```\s*\n([{\[][\s\S]*?[}\]])\s*```/, `\`\`\`\n${modifiedJsonString}\n\`\`\``);
1091
+ }
1092
+
1093
+ return modifiedJsonString;
1094
+ }
1095
+
865
1096
  /**
866
1097
  * Replace mermaid diagrams in original markdown with corrected versions
867
- * @param {string} originalResponse - Original response with markdown
1098
+ * Automatically detects JSON vs markdown format and uses appropriate replacement
1099
+ * @param {string} originalResponse - Original response with markdown or JSON
868
1100
  * @param {Array} correctedDiagrams - Array of corrected diagram objects
869
1101
  * @returns {string} - Response with corrected diagrams in original format
870
1102
  */
@@ -877,22 +1109,28 @@ export function replaceMermaidDiagramsInMarkdown(originalResponse, correctedDiag
877
1109
  return originalResponse;
878
1110
  }
879
1111
 
1112
+ // Check if any diagrams are in JSON format
1113
+ const hasJsonDiagrams = correctedDiagrams.some(d => d.isInJson);
1114
+ if (hasJsonDiagrams) {
1115
+ return replaceMermaidDiagramsInJson(originalResponse, correctedDiagrams);
1116
+ }
1117
+
880
1118
  let modifiedResponse = originalResponse;
881
-
1119
+
882
1120
  // Sort diagrams by start index in reverse order to preserve indices during replacement
883
1121
  const sortedDiagrams = [...correctedDiagrams].sort((a, b) => b.startIndex - a.startIndex);
884
-
1122
+
885
1123
  for (const diagram of sortedDiagrams) {
886
1124
  // Reconstruct the code block with original attributes if they existed
887
1125
  const attributesStr = diagram.attributes ? ` ${diagram.attributes}` : '';
888
1126
  const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${diagram.content}\n\`\`\``;
889
-
1127
+
890
1128
  // Replace the original code block
891
- modifiedResponse = modifiedResponse.slice(0, diagram.startIndex) +
892
- newCodeBlock +
1129
+ modifiedResponse = modifiedResponse.slice(0, diagram.startIndex) +
1130
+ newCodeBlock +
893
1131
  modifiedResponse.slice(diagram.endIndex);
894
1132
  }
895
-
1133
+
896
1134
  return modifiedResponse;
897
1135
  }
898
1136
 
@@ -154,6 +154,31 @@ User: Find all markdown files in the docs directory, but only at the top level.
154
154
  </examples>
155
155
  `;
156
156
 
157
+ // Define the readImage tool XML definition
158
+ export const readImageToolDefinition = `
159
+ ## readImage
160
+ Description: Read and load an image file so it can be viewed by the AI. Use this when you need to analyze, describe, or work with image content. Images from user messages are automatically loaded, but use this tool to explicitly read images mentioned in tool outputs or when you need to examine specific image files.
161
+
162
+ Parameters:
163
+ - path: (required) The path to the image file to read. Supports png, jpg, jpeg, webp, bmp, and svg formats.
164
+
165
+ Usage Example:
166
+
167
+ <examples>
168
+
169
+ User: Can you describe what's in screenshot.png?
170
+ <readImage>
171
+ <path>screenshot.png</path>
172
+ </readImage>
173
+
174
+ User: Analyze the diagram in docs/architecture.svg
175
+ <readImage>
176
+ <path>docs/architecture.svg</path>
177
+ </readImage>
178
+
179
+ </examples>
180
+ `;
181
+
157
182
  /**
158
183
  * Enhanced XML parser that handles thinking tags and attempt_complete shorthand
159
184
  * This function removes any <thinking></thinking> tags from the input string