@probelabs/probe 0.6.0-rc103 → 0.6.0-rc105

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.
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { MCPClientManager } from './client.js';
7
7
  import { loadMCPConfiguration } from './config.js';
8
+ import { processXmlWithThinkingAndRecovery } from '../xmlParsingUtils.js';
8
9
 
9
10
  /**
10
11
  * Convert MCP tool to XML definition format
@@ -254,18 +255,18 @@ export class MCPXmlBridge {
254
255
 
255
256
  /**
256
257
  * Enhanced XML parser that handles both native and MCP tools
258
+ * Uses the exact same logic as CLI/SDK mode to ensure consistency
257
259
  * @param {string} xmlString - XML string to parse
258
260
  * @param {Array<string>} nativeTools - List of native tool names
259
261
  * @param {MCPXmlBridge} mcpBridge - MCP bridge instance
260
262
  * @returns {Object|null} Parsed tool call
261
263
  */
262
264
  export function parseHybridXmlToolCall(xmlString, nativeTools = [], mcpBridge = null) {
263
- // First try native tools with standard XML parsing
264
- for (const toolName of nativeTools) {
265
- const nativeResult = parseNativeXmlTool(xmlString, toolName);
266
- if (nativeResult) {
267
- return { ...nativeResult, type: 'native' };
268
- }
265
+ // First try native tools with the same logic as CLI/SDK mode
266
+ // This includes thinking tag removal and attempt_complete recovery logic
267
+ const nativeResult = parseNativeXmlToolWithThinking(xmlString, nativeTools);
268
+ if (nativeResult) {
269
+ return { ...nativeResult, type: 'native' };
269
270
  }
270
271
 
271
272
  // Then try MCP tools if bridge is available
@@ -279,6 +280,33 @@ export function parseHybridXmlToolCall(xmlString, nativeTools = [], mcpBridge =
279
280
  return null;
280
281
  }
281
282
 
283
+ /**
284
+ * Parse native XML tools using the same logic as CLI/SDK mode
285
+ * Now uses shared utilities instead of duplicating code
286
+ * @param {string} xmlString - XML string to parse
287
+ * @param {Array<string>} validTools - List of valid tool names
288
+ * @returns {Object|null} Parsed tool call
289
+ */
290
+ function parseNativeXmlToolWithThinking(xmlString, validTools) {
291
+ // Use the shared processing logic
292
+ const { cleanedXmlString, recoveryResult } = processXmlWithThinkingAndRecovery(xmlString, validTools);
293
+
294
+ // If recovery found an attempt_complete pattern, return it
295
+ if (recoveryResult) {
296
+ return recoveryResult;
297
+ }
298
+
299
+ // Use the original parseNativeXmlTool function to parse the cleaned XML string
300
+ for (const toolName of validTools) {
301
+ const result = parseNativeXmlTool(cleanedXmlString, toolName);
302
+ if (result) {
303
+ return result;
304
+ }
305
+ }
306
+
307
+ return null;
308
+ }
309
+
282
310
  /**
283
311
  * Parse native XML tool (existing format)
284
312
  * @param {string} xmlString - XML string
@@ -207,14 +207,24 @@ export function createWrappedTools(baseTools) {
207
207
  // Simple file listing tool
208
208
  export const listFilesTool = {
209
209
  execute: async (params) => {
210
- const { directory = '.' } = params;
210
+ const { directory = '.', workingDirectory } = params;
211
+
212
+ // Use the provided working directory, or fall back to process.cwd()
213
+ const baseCwd = workingDirectory || process.cwd();
214
+
215
+ // Security: Validate path to prevent traversal attacks
216
+ const secureBaseDir = path.resolve(baseCwd);
217
+ const targetDir = path.resolve(secureBaseDir, directory);
218
+ if (!targetDir.startsWith(secureBaseDir + path.sep) && targetDir !== secureBaseDir) {
219
+ throw new Error('Path traversal attempt detected. Access denied.');
220
+ }
211
221
 
212
222
  try {
213
223
  const files = await listFilesByLevel({
214
- directory,
224
+ directory: targetDir,
215
225
  maxFiles: 100,
216
226
  respectGitignore: !process.env.PROBE_NO_GITIGNORE || process.env.PROBE_NO_GITIGNORE === '',
217
- cwd: process.cwd()
227
+ cwd: secureBaseDir
218
228
  });
219
229
 
220
230
  return files;
@@ -227,15 +237,23 @@ export const listFilesTool = {
227
237
  // Simple file search tool
228
238
  export const searchFilesTool = {
229
239
  execute: async (params) => {
230
- const { pattern, directory = '.', recursive = true } = params;
240
+ const { pattern, directory = '.', recursive = true, workingDirectory } = params;
231
241
 
232
242
  if (!pattern) {
233
243
  throw new Error('Pattern is required for file search');
234
244
  }
235
245
 
246
+ // Security: Validate path to prevent traversal attacks
247
+ const baseCwd = workingDirectory || process.cwd();
248
+ const secureBaseDir = path.resolve(baseCwd);
249
+ const targetDir = path.resolve(secureBaseDir, directory);
250
+ if (!targetDir.startsWith(secureBaseDir + path.sep) && targetDir !== secureBaseDir) {
251
+ throw new Error('Path traversal attempt detected. Access denied.');
252
+ }
253
+
236
254
  try {
237
255
  const options = {
238
- cwd: directory,
256
+ cwd: targetDir,
239
257
  ignore: ['node_modules/**', '.git/**'],
240
258
  absolute: false
241
259
  };
@@ -18,6 +18,7 @@ import {
18
18
  parseXmlToolCall
19
19
  } from '../index.js';
20
20
  import { randomUUID } from 'crypto';
21
+ import { processXmlWithThinkingAndRecovery } from './xmlParsingUtils.js';
21
22
 
22
23
  // Create configured tool instances
23
24
  export function createTools(configOptions) {
@@ -134,30 +135,15 @@ User: Find all markdown files in the docs directory, but only at the top level.
134
135
  * @returns {Object|null} - The parsed tool call or null if no valid tool call found
135
136
  */
136
137
  export function parseXmlToolCallWithThinking(xmlString, validTools) {
137
- // Extract thinking content if present (for potential logging or analysis)
138
- const thinkingMatch = xmlString.match(/<thinking>([\s\S]*?)<\/thinking>/);
139
- const thinkingContent = thinkingMatch ? thinkingMatch[1].trim() : null;
140
-
141
- // Remove thinking tags and their content from the XML string
142
- let cleanedXmlString = xmlString.replace(/<thinking>[\s\S]*?<\/thinking>/g, '').trim();
143
-
144
- // Check for attempt_complete shorthand (single tag with no closing tag and no parameters)
145
- const attemptCompleteMatch = cleanedXmlString.match(/^<attempt_complete>\s*$/);
146
- if (attemptCompleteMatch) {
147
- // Convert shorthand to full attempt_completion format with special marker
148
- return {
149
- toolName: 'attempt_completion',
150
- params: { result: '__PREVIOUS_RESPONSE__' }
151
- };
138
+ // Use the shared processing logic
139
+ const { cleanedXmlString, recoveryResult } = processXmlWithThinkingAndRecovery(xmlString, validTools);
140
+
141
+ // If recovery found an attempt_complete pattern, return it
142
+ if (recoveryResult) {
143
+ return recoveryResult;
152
144
  }
153
145
 
154
- // Use the original parseXmlToolCall function to parse the cleaned XML string
155
- const parsedTool = parseXmlToolCall(cleanedXmlString, validTools);
156
-
157
- // If debugging is enabled, log the thinking content
158
- if (process.env.DEBUG === '1' && thinkingContent) {
159
- console.log(`[DEBUG] AI Thinking Process:\n${thinkingContent}`);
160
- }
146
+ // Otherwise, use the original parseXmlToolCall function to parse the cleaned XML string
147
+ return parseXmlToolCall(cleanedXmlString, validTools);
148
+ }
161
149
 
162
- return parsedTool;
163
- }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Shared XML parsing utilities used by both CLI/SDK and MCP modes
3
+ * This module contains the core logic for thinking tag removal and attempt_complete recovery
4
+ */
5
+
6
+ /**
7
+ * Remove thinking tags and their content from XML string
8
+ * @param {string} xmlString - The XML string to clean
9
+ * @returns {string} - Cleaned XML string without thinking tags
10
+ */
11
+ export function removeThinkingTags(xmlString) {
12
+ return xmlString.replace(/<thinking>[\s\S]*?<\/thinking>/g, '').trim();
13
+ }
14
+
15
+ /**
16
+ * Extract thinking content for potential logging
17
+ * @param {string} xmlString - The XML string to extract from
18
+ * @returns {string|null} - Thinking content or null if not found
19
+ */
20
+ export function extractThinkingContent(xmlString) {
21
+ const thinkingMatch = xmlString.match(/<thinking>([\s\S]*?)<\/thinking>/);
22
+ return thinkingMatch ? thinkingMatch[1].trim() : null;
23
+ }
24
+
25
+ /**
26
+ * Check for attempt_complete recovery patterns and return standardized result
27
+ * @param {string} cleanedXmlString - XML string with thinking tags already removed
28
+ * @param {Array<string>} validTools - List of valid tool names
29
+ * @returns {Object|null} - Standardized attempt_completion result or null
30
+ */
31
+ export function checkAttemptCompleteRecovery(cleanedXmlString, validTools = []) {
32
+ // Enhanced recovery logic for attempt_complete shorthand
33
+ const attemptCompletePatterns = [
34
+ // Standard shorthand with optional whitespace
35
+ /^<attempt_complete>\s*$/,
36
+ // Empty with proper closing tag (common case from the logs)
37
+ /^<attempt_complete>\s*<\/attempt_complete>\s*$/,
38
+ // Self-closing variant
39
+ /^<attempt_complete\s*\/>\s*$/,
40
+ // Incomplete opening tag (missing closing bracket)
41
+ /^<attempt_complete\s*$/,
42
+ // With trailing content (extract just the tag part) - must come after empty tag pattern
43
+ /^<attempt_complete>(.*)$/s,
44
+ // Self-closing with trailing content
45
+ /^<attempt_complete\s*\/>(.*)$/s
46
+ ];
47
+
48
+ for (const pattern of attemptCompletePatterns) {
49
+ const match = cleanedXmlString.match(pattern);
50
+ if (match) {
51
+ // Convert any form of attempt_complete to the standard format
52
+ return {
53
+ toolName: 'attempt_completion',
54
+ params: { result: '__PREVIOUS_RESPONSE__' }
55
+ };
56
+ }
57
+ }
58
+
59
+ // Additional recovery: check if the string contains attempt_complete anywhere
60
+ // and treat the entire response as a completion signal if no other tool tags are found
61
+ if (cleanedXmlString.includes('<attempt_complete') && !hasOtherToolTags(cleanedXmlString, validTools)) {
62
+ // This handles malformed cases where attempt_complete appears but is broken
63
+ return {
64
+ toolName: 'attempt_completion',
65
+ params: { result: '__PREVIOUS_RESPONSE__' }
66
+ };
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Helper function to check if the XML string contains other tool tags
74
+ * @param {string} xmlString - The XML string to check
75
+ * @param {string[]} validTools - List of valid tool names
76
+ * @returns {boolean} - True if other tool tags are found
77
+ */
78
+ function hasOtherToolTags(xmlString, validTools = []) {
79
+ const defaultTools = ['search', 'query', 'extract', 'listFiles', 'searchFiles', 'implement', 'attempt_completion'];
80
+ const toolsToCheck = validTools.length > 0 ? validTools : defaultTools;
81
+
82
+ // Check for any tool tags other than attempt_complete variants
83
+ for (const tool of toolsToCheck) {
84
+ if (tool !== 'attempt_completion' && xmlString.includes(`<${tool}`)) {
85
+ return true;
86
+ }
87
+ }
88
+ return false;
89
+ }
90
+
91
+ /**
92
+ * Apply the full thinking tag removal and attempt_complete recovery logic
93
+ * This replicates the core logic from parseXmlToolCallWithThinking
94
+ * @param {string} xmlString - The XML string to process
95
+ * @param {Array<string>} validTools - List of valid tool names
96
+ * @returns {Object} - Processing result with cleanedXml and potentialRecovery
97
+ */
98
+ export function processXmlWithThinkingAndRecovery(xmlString, validTools = []) {
99
+ // Extract thinking content if present (for potential logging or analysis)
100
+ const thinkingContent = extractThinkingContent(xmlString);
101
+
102
+ // Remove thinking tags and their content from the XML string
103
+ const cleanedXmlString = removeThinkingTags(xmlString);
104
+
105
+ // Check for attempt_complete recovery patterns
106
+ const recoveryResult = checkAttemptCompleteRecovery(cleanedXmlString, validTools);
107
+
108
+ // If debugging is enabled, log the thinking content
109
+ if (process.env.DEBUG === '1' && thinkingContent) {
110
+ console.log(`[DEBUG] AI Thinking Process:\n${thinkingContent}`);
111
+ }
112
+
113
+ return {
114
+ cleanedXmlString,
115
+ thinkingContent,
116
+ recoveryResult
117
+ };
118
+ }
package/build/utils.js CHANGED
@@ -43,7 +43,7 @@ export async function getBinaryPath(options = {}) {
43
43
 
44
44
  // Check bin directory
45
45
  const isWindows = process.platform === 'win32';
46
- const binaryName = isWindows ? 'probe.exe' : 'probe';
46
+ const binaryName = isWindows ? 'probe.exe' : 'probe-binary';
47
47
  const binaryPath = path.join(binDir, binaryName);
48
48
 
49
49
  if (fs.existsSync(binaryPath) && !forceDownload) {