@probelabs/probe 0.6.0-rc121 → 0.6.0-rc122

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.
@@ -51,8 +51,8 @@ export function loadMCPConfigurationFromPath(configPath) {
51
51
  const content = readFileSync(configPath, 'utf8');
52
52
  const config = JSON.parse(content);
53
53
 
54
- if (process.env.DEBUG === '1') {
55
- console.error(`[MCP] Loaded configuration from: ${configPath}`);
54
+ if (process.env.DEBUG === '1' || process.env.DEBUG_MCP === '1') {
55
+ console.error(`[MCP DEBUG] Loaded configuration from: ${configPath}`);
56
56
  }
57
57
 
58
58
  // Merge with environment variable overrides
@@ -94,12 +94,12 @@ export function loadMCPConfiguration() {
94
94
  try {
95
95
  const content = readFileSync(configPath, 'utf8');
96
96
  config = JSON.parse(content);
97
- if (process.env.DEBUG === '1') {
98
- console.error(`[MCP] Loaded configuration from: ${configPath}`);
97
+ if (process.env.DEBUG === '1' || process.env.DEBUG_MCP === '1') {
98
+ console.error(`[MCP DEBUG] Loaded configuration from: ${configPath}`);
99
99
  }
100
100
  break;
101
101
  } catch (error) {
102
- console.error(`[MCP] Failed to parse config from ${configPath}:`, error.message);
102
+ console.error(`[MCP ERROR] Failed to parse config from ${configPath}:`, error.message);
103
103
  }
104
104
  }
105
105
  }
@@ -209,12 +209,12 @@ export function parseEnabledServers(config) {
209
209
  // Validate required fields based on transport
210
210
  if (server.transport === 'stdio') {
211
211
  if (!server.command) {
212
- console.error(`[MCP] Server ${name} missing required 'command' for stdio transport`);
212
+ console.error(`[MCP ERROR] Server ${name} missing required 'command' for stdio transport`);
213
213
  continue;
214
214
  }
215
215
  } else if (['websocket', 'sse', 'http'].includes(server.transport)) {
216
216
  if (!server.url) {
217
- console.error(`[MCP] Server ${name} missing required 'url' for ${server.transport} transport`);
217
+ console.error(`[MCP ERROR] Server ${name} missing required 'url' for ${server.transport} transport`);
218
218
  continue;
219
219
  }
220
220
  }
@@ -301,7 +301,7 @@ export function saveConfig(config, path) {
301
301
  }
302
302
 
303
303
  writeFileSync(path, JSON.stringify(config, null, 2), 'utf8');
304
- console.log(`[MCP] Configuration saved to: ${path}`);
304
+ console.error(`[MCP INFO] Configuration saved to: ${path}`);
305
305
  }
306
306
 
307
307
  export default {
@@ -141,23 +141,44 @@ export class MCPXmlBridge {
141
141
 
142
142
  if (!config) {
143
143
  // No config provided - fall back to auto-discovery for backward compatibility
144
+ if (this.debug) {
145
+ console.error('[MCP DEBUG] No config provided, attempting auto-discovery...');
146
+ }
144
147
  mcpConfigs = loadMCPConfiguration();
148
+
149
+ // Check if auto-discovery found anything
150
+ if (!mcpConfigs || !mcpConfigs.mcpServers || Object.keys(mcpConfigs.mcpServers).length === 0) {
151
+ console.error('[MCP WARNING] MCP enabled but no configuration found');
152
+ console.error('[MCP INFO] To use MCP, provide configuration via:');
153
+ console.error('[MCP INFO] - mcpConfig option when creating ProbeAgent');
154
+ console.error('[MCP INFO] - mcpConfigPath option pointing to a config file');
155
+ console.error('[MCP INFO] - Config file in standard locations (~/.mcp/config.json, etc.)');
156
+ console.error('[MCP INFO] - Environment variable MCP_CONFIG_PATH');
157
+ }
145
158
  } else if (Array.isArray(config)) {
146
159
  // Deprecated: Array of server configs (backward compatibility)
160
+ if (this.debug) {
161
+ console.error('[MCP DEBUG] Using deprecated array config format (consider using mcpConfig object)');
162
+ }
147
163
  mcpConfigs = { mcpServers: config };
148
164
  } else {
149
165
  // New: Full config object provided directly
166
+ if (this.debug) {
167
+ console.error('[MCP DEBUG] Using provided MCP config object');
168
+ }
150
169
  mcpConfigs = config;
151
170
  }
152
171
 
153
172
  if (!mcpConfigs || !mcpConfigs.mcpServers || Object.keys(mcpConfigs.mcpServers).length === 0) {
154
- if (this.debug) {
155
- console.error('[MCP] No MCP servers configured');
156
- }
173
+ console.error('[MCP INFO] 0 MCP tools available');
157
174
  return;
158
175
  }
159
176
 
160
177
  try {
178
+ if (this.debug) {
179
+ console.error('[MCP DEBUG] Initializing MCP client manager...');
180
+ }
181
+
161
182
  // Initialize the MCP client manager
162
183
  this.mcpManager = new MCPClientManager({ debug: this.debug });
163
184
  const result = await this.mcpManager.initialize(mcpConfigs);
@@ -165,17 +186,27 @@ export class MCPXmlBridge {
165
186
  // Get tools from the manager
166
187
  const vercelTools = this.mcpManager.getVercelTools();
167
188
  this.mcpTools = vercelTools;
189
+ const toolCount = Object.keys(vercelTools).length;
168
190
 
169
191
  // Generate XML definitions for all tools
170
192
  for (const [name, tool] of Object.entries(vercelTools)) {
171
193
  this.xmlDefinitions[name] = mcpToolToXmlDefinition(name, tool);
172
194
  }
173
195
 
174
- if (this.debug) {
175
- console.error(`[MCP] Loaded ${Object.keys(vercelTools).length} MCP tools from ${result.connected} server(s)`);
196
+ if (toolCount === 0) {
197
+ console.error('[MCP INFO] MCP initialization complete: 0 tools loaded');
198
+ } else {
199
+ console.error(`[MCP INFO] MCP initialization complete: ${toolCount} tool${toolCount !== 1 ? 's' : ''} loaded from ${result.connected} server${result.connected !== 1 ? 's' : ''}`);
200
+
201
+ if (this.debug) {
202
+ console.error('[MCP DEBUG] Tool definitions generated for XML bridge');
203
+ }
176
204
  }
177
205
  } catch (error) {
178
- console.error('[MCP] Failed to initialize MCP connections:', error);
206
+ console.error('[MCP ERROR] Failed to initialize MCP connections:', error.message);
207
+ if (this.debug) {
208
+ console.error('[MCP DEBUG] Full error details:', error);
209
+ }
179
210
  }
180
211
  }
181
212
 
@@ -204,28 +235,42 @@ export class MCPXmlBridge {
204
235
  const parsed = parseXmlMcpToolCall(xmlString, this.getToolNames());
205
236
 
206
237
  if (!parsed) {
238
+ console.error('[MCP ERROR] No valid MCP tool call found in XML');
207
239
  throw new Error('No valid MCP tool call found in XML');
208
240
  }
209
241
 
210
242
  const { toolName, params } = parsed;
211
243
 
212
244
  if (this.debug) {
213
- console.error(`[MCP] Executing MCP tool: ${toolName} with params:`, params);
245
+ console.error(`[MCP DEBUG] Executing MCP tool: ${toolName}`);
246
+ console.error(`[MCP DEBUG] Parameters:`, JSON.stringify(params, null, 2));
214
247
  }
215
248
 
216
249
  const tool = this.mcpTools[toolName];
217
250
  if (!tool) {
251
+ console.error(`[MCP ERROR] Unknown MCP tool: ${toolName}`);
252
+ console.error(`[MCP ERROR] Available tools: ${this.getToolNames().join(', ')}`);
218
253
  throw new Error(`Unknown MCP tool: ${toolName}`);
219
254
  }
220
255
 
221
256
  try {
222
257
  const result = await tool.execute(params);
258
+
259
+ if (this.debug) {
260
+ console.error(`[MCP DEBUG] Tool ${toolName} executed successfully`);
261
+ }
262
+
223
263
  return {
224
264
  success: true,
225
265
  toolName,
226
266
  result
227
267
  };
228
268
  } catch (error) {
269
+ console.error(`[MCP ERROR] Tool ${toolName} execution failed:`, error.message);
270
+ if (this.debug) {
271
+ console.error(`[MCP DEBUG] Full error details:`, error);
272
+ }
273
+
229
274
  return {
230
275
  success: false,
231
276
  toolName,
@@ -40,7 +40,7 @@ export function decodeHtmlEntities(text) {
40
40
 
41
41
  /**
42
42
  * Clean AI response by extracting JSON content when response contains JSON
43
- * Only processes responses that contain JSON structures { or [
43
+ * Only processes responses that contain JSON structures { or [
44
44
  * @param {string} response - Raw AI response
45
45
  * @returns {string} - Cleaned response with JSON boundaries extracted if applicable
46
46
  */
@@ -50,34 +50,47 @@ export function cleanSchemaResponse(response) {
50
50
  }
51
51
 
52
52
  const trimmed = response.trim();
53
-
54
- // First, look for JSON after code block markers
53
+
54
+ // First, look for JSON after code block markers - similar to mermaid extraction
55
+ // Try with json language specifier
56
+ const jsonBlockMatch = trimmed.match(/```json\s*\n([\s\S]*?)\n```/);
57
+ if (jsonBlockMatch) {
58
+ return jsonBlockMatch[1].trim();
59
+ }
60
+
61
+ // Try any code block with JSON content
62
+ const anyBlockMatch = trimmed.match(/```\s*\n([{\[][\s\S]*?[}\]])\s*```/);
63
+ if (anyBlockMatch) {
64
+ return anyBlockMatch[1].trim();
65
+ }
66
+
67
+ // Legacy patterns for more specific matching
55
68
  const codeBlockPatterns = [
56
69
  /```json\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/,
57
70
  /```\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/,
58
71
  /`([{\[][\s\S]*?[}\]])`/
59
72
  ];
60
-
73
+
61
74
  for (const pattern of codeBlockPatterns) {
62
75
  const match = trimmed.match(pattern);
63
76
  if (match) {
64
77
  return match[1].trim();
65
78
  }
66
79
  }
67
-
80
+
68
81
  // Look for code block start followed immediately by JSON
69
82
  const codeBlockStartPattern = /```(?:json)?\s*\n?\s*([{\[])/;
70
83
  const codeBlockMatch = trimmed.match(codeBlockStartPattern);
71
-
84
+
72
85
  if (codeBlockMatch) {
73
86
  const startIndex = codeBlockMatch.index + codeBlockMatch[0].length - 1; // Position of the bracket
74
-
87
+
75
88
  // Find the matching closing bracket
76
89
  const openChar = codeBlockMatch[1];
77
90
  const closeChar = openChar === '{' ? '}' : ']';
78
91
  let bracketCount = 1;
79
92
  let endIndex = startIndex + 1;
80
-
93
+
81
94
  while (endIndex < trimmed.length && bracketCount > 0) {
82
95
  const char = trimmed[endIndex];
83
96
  if (char === openChar) {
@@ -87,36 +100,36 @@ export function cleanSchemaResponse(response) {
87
100
  }
88
101
  endIndex++;
89
102
  }
90
-
103
+
91
104
  if (bracketCount === 0) {
92
105
  return trimmed.substring(startIndex, endIndex);
93
106
  }
94
107
  }
95
-
108
+
96
109
  // Fallback: Find JSON boundaries anywhere in the text
97
110
  const firstBracket = Math.min(
98
111
  trimmed.indexOf('{') >= 0 ? trimmed.indexOf('{') : Infinity,
99
112
  trimmed.indexOf('[') >= 0 ? trimmed.indexOf('[') : Infinity
100
113
  );
101
-
114
+
102
115
  const lastBracket = Math.max(
103
116
  trimmed.lastIndexOf('}'),
104
117
  trimmed.lastIndexOf(']')
105
118
  );
106
-
119
+
107
120
  // Only extract if we found valid JSON boundaries
108
121
  if (firstBracket < Infinity && lastBracket >= 0 && firstBracket < lastBracket) {
109
122
  // Check if the response likely starts with JSON (directly or after minimal content)
110
123
  const beforeFirstBracket = trimmed.substring(0, firstBracket).trim();
111
-
124
+
112
125
  // If there's minimal content before the first bracket, extract the JSON
113
- if (beforeFirstBracket === '' ||
126
+ if (beforeFirstBracket === '' ||
114
127
  beforeFirstBracket.match(/^```\w*$/) ||
115
128
  beforeFirstBracket.split('\n').length <= 2) {
116
129
  return trimmed.substring(firstBracket, lastBracket + 1);
117
130
  }
118
131
  }
119
-
132
+
120
133
  return response; // Return original if no extractable JSON found
121
134
  }
122
135
 
package/build/grep.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Grep functionality for the probe package
3
+ * @module grep
4
+ */
5
+
6
+ import { execFile } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import { getBinaryPath } from './utils.js';
9
+
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ /**
13
+ * Flag mapping for grep options
14
+ * Maps option keys to command-line flags
15
+ */
16
+ const GREP_FLAG_MAP = {
17
+ ignoreCase: '-i',
18
+ lineNumbers: '-n',
19
+ count: '-c',
20
+ filesWithMatches: '-l',
21
+ filesWithoutMatches: '-L',
22
+ invertMatch: '-v',
23
+ beforeContext: '-B',
24
+ afterContext: '-A',
25
+ context: '-C',
26
+ noGitignore: '--no-gitignore',
27
+ color: '--color',
28
+ maxCount: '-m'
29
+ };
30
+
31
+ /**
32
+ * Standard grep-style search across files (works with any file type, not just code)
33
+ *
34
+ * This provides a cross-platform grep interface that works on any OS and file type.
35
+ * Use this for searching non-code files (logs, config files, text files, etc.) that
36
+ * are not supported by probe's semantic search.
37
+ *
38
+ * For code files, prefer using the `search()` function which provides semantic,
39
+ * AST-aware search capabilities.
40
+ *
41
+ * @param {Object} options - Grep options
42
+ * @param {string} options.pattern - Pattern to search for (regex)
43
+ * @param {string|string[]} options.paths - Path(s) to search in
44
+ * @param {boolean} [options.ignoreCase] - Case-insensitive search (-i)
45
+ * @param {boolean} [options.lineNumbers] - Show line numbers (-n)
46
+ * @param {boolean} [options.count] - Only show count of matches (-c)
47
+ * @param {boolean} [options.filesWithMatches] - Only show filenames with matches (-l)
48
+ * @param {boolean} [options.filesWithoutMatches] - Only show filenames without matches (-L)
49
+ * @param {boolean} [options.invertMatch] - Invert match, show non-matching lines (-v)
50
+ * @param {number} [options.beforeContext] - Lines of context before match (-B)
51
+ * @param {number} [options.afterContext] - Lines of context after match (-A)
52
+ * @param {number} [options.context] - Lines of context before and after match (-C)
53
+ * @param {boolean} [options.noGitignore] - Don't respect .gitignore files (--no-gitignore)
54
+ * @param {string} [options.color] - Colorize output: 'always', 'never', 'auto' (--color)
55
+ * @param {number} [options.maxCount] - Stop after N matches per file (-m)
56
+ * @param {Object} [options.binaryOptions] - Options for getting the binary
57
+ * @param {boolean} [options.binaryOptions.forceDownload] - Force download even if binary exists
58
+ * @param {string} [options.binaryOptions.version] - Specific version to download
59
+ * @returns {Promise<string>} - Grep results as string
60
+ * @throws {Error} If the grep operation fails
61
+ *
62
+ * @example
63
+ * // Search for "error" in log files (case-insensitive)
64
+ * const results = await grep({
65
+ * pattern: 'error',
66
+ * paths: '/var/log',
67
+ * ignoreCase: true,
68
+ * lineNumbers: true
69
+ * });
70
+ *
71
+ * @example
72
+ * // Count occurrences of "TODO" in project
73
+ * const count = await grep({
74
+ * pattern: 'TODO',
75
+ * paths: '.',
76
+ * count: true
77
+ * });
78
+ *
79
+ * @example
80
+ * // Find files containing "config" with context
81
+ * const matches = await grep({
82
+ * pattern: 'config',
83
+ * paths: '/etc',
84
+ * context: 2,
85
+ * filesWithMatches: true
86
+ * });
87
+ */
88
+ export async function grep(options) {
89
+ if (!options || !options.pattern) {
90
+ throw new Error('Pattern is required');
91
+ }
92
+
93
+ if (!options.paths) {
94
+ throw new Error('Path(s) are required');
95
+ }
96
+
97
+ // Get the binary path
98
+ const binaryPath = await getBinaryPath(options.binaryOptions || {});
99
+
100
+ // Build CLI arguments array for grep subcommand
101
+ // Using an array prevents command injection vulnerabilities
102
+ const cliArgs = ['grep'];
103
+
104
+ // Add flags from GREP_FLAG_MAP
105
+ for (const [key, flag] of Object.entries(GREP_FLAG_MAP)) {
106
+ const value = options[key];
107
+ if (value === undefined || value === null) continue;
108
+
109
+ if (typeof value === 'boolean' && value) {
110
+ // Boolean flag
111
+ cliArgs.push(flag);
112
+ } else if (typeof value === 'number') {
113
+ // Numeric option
114
+ cliArgs.push(flag, String(value));
115
+ } else if (typeof value === 'string') {
116
+ // String option (like color)
117
+ cliArgs.push(flag, value);
118
+ }
119
+ }
120
+
121
+ // Add pattern (no need to escape - execFile handles it securely)
122
+ cliArgs.push(options.pattern);
123
+
124
+ // Add paths (can be single string or array)
125
+ const paths = Array.isArray(options.paths) ? options.paths : [options.paths];
126
+ cliArgs.push(...paths);
127
+
128
+ try {
129
+ // Use execFile instead of exec to prevent command injection
130
+ // execFile does not spawn a shell and passes arguments as an array
131
+ const { stdout, stderr } = await execFileAsync(binaryPath, cliArgs, {
132
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
133
+ env: {
134
+ ...process.env,
135
+ // Disable colors in stderr for cleaner output
136
+ NO_COLOR: '1'
137
+ }
138
+ });
139
+
140
+ // Return stdout (grep results)
141
+ return stdout;
142
+ } catch (error) {
143
+ // Grep exit code 1 means "no matches found", which is not an error
144
+ if (error.code === 1 && !error.stderr) {
145
+ return error.stdout || '';
146
+ }
147
+
148
+ // Other errors are real failures
149
+ const errorMessage = error.stderr || error.message || 'Unknown error';
150
+ throw new Error(`Grep failed: ${errorMessage}`);
151
+ }
152
+ }
package/build/index.js CHANGED
@@ -10,6 +10,7 @@
10
10
  import { search } from './search.js';
11
11
  import { query } from './query.js';
12
12
  import { extract } from './extract.js';
13
+ import { grep } from './grep.js';
13
14
  import { delegate } from './delegate.js';
14
15
  import { getBinaryPath, setBinaryPath } from './utils.js';
15
16
  import * as tools from './tools/index.js';
@@ -40,6 +41,7 @@ export {
40
41
  search,
41
42
  query,
42
43
  extract,
44
+ grep,
43
45
  delegate,
44
46
  getBinaryPath,
45
47
  setBinaryPath,