@probelabs/probe 0.6.0-rc121 → 0.6.0-rc123

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.
@@ -128,14 +128,30 @@ export class MCPClientManager {
128
128
  this.config = config || loadMCPConfiguration();
129
129
  const servers = parseEnabledServers(this.config);
130
130
 
131
+ // Always log the number of servers found
132
+ console.error(`[MCP INFO] Found ${servers.length} enabled MCP server${servers.length !== 1 ? 's' : ''}`);
133
+
134
+ if (servers.length === 0) {
135
+ console.error('[MCP INFO] No MCP servers configured or enabled');
136
+ console.error('[MCP INFO] 0 MCP tools available');
137
+ return {
138
+ connected: 0,
139
+ total: 0,
140
+ tools: []
141
+ };
142
+ }
143
+
131
144
  if (this.debug) {
132
- console.error(`[MCP] Found ${servers.length} enabled servers`);
145
+ console.error('[MCP DEBUG] Server details:');
146
+ servers.forEach(server => {
147
+ console.error(`[MCP DEBUG] - ${server.name} (${server.transport})`);
148
+ });
133
149
  }
134
150
 
135
151
  // Connect to each enabled server
136
152
  const connectionPromises = servers.map(server =>
137
153
  this.connectToServer(server).catch(error => {
138
- console.error(`[MCP] Failed to connect to ${server.name}:`, error.message);
154
+ console.error(`[MCP ERROR] Failed to connect to ${server.name}:`, error.message);
139
155
  return null;
140
156
  })
141
157
  );
@@ -143,9 +159,23 @@ export class MCPClientManager {
143
159
  const results = await Promise.all(connectionPromises);
144
160
  const connectedCount = results.filter(Boolean).length;
145
161
 
146
- if (this.debug) {
147
- console.error(`[MCP] Successfully connected to ${connectedCount}/${servers.length} servers`);
148
- console.error(`[MCP] Total tools available: ${this.tools.size}`);
162
+ // Always log connection results
163
+ if (connectedCount === 0) {
164
+ console.error(`[MCP ERROR] Failed to connect to all ${servers.length} server${servers.length !== 1 ? 's' : ''}`);
165
+ console.error('[MCP INFO] 0 MCP tools available');
166
+ } else if (connectedCount < servers.length) {
167
+ console.error(`[MCP INFO] Successfully connected to ${connectedCount}/${servers.length} servers`);
168
+ console.error(`[MCP INFO] ${this.tools.size} MCP tool${this.tools.size !== 1 ? 's' : ''} available`);
169
+ } else {
170
+ console.error(`[MCP INFO] Successfully connected to all ${connectedCount} server${connectedCount !== 1 ? 's' : ''}`);
171
+ console.error(`[MCP INFO] ${this.tools.size} MCP tool${this.tools.size !== 1 ? 's' : ''} available`);
172
+ }
173
+
174
+ if (this.debug && this.tools.size > 0) {
175
+ console.error('[MCP DEBUG] Available tools:');
176
+ Array.from(this.tools.keys()).forEach(toolName => {
177
+ console.error(`[MCP DEBUG] - ${toolName}`);
178
+ });
149
179
  }
150
180
 
151
181
  return {
@@ -164,7 +194,7 @@ export class MCPClientManager {
164
194
 
165
195
  try {
166
196
  if (this.debug) {
167
- console.error(`[MCP] Connecting to ${name} via ${serverConfig.transport}...`);
197
+ console.error(`[MCP DEBUG] Connecting to ${name} via ${serverConfig.transport}...`);
168
198
  }
169
199
 
170
200
  // Create transport
@@ -193,6 +223,7 @@ export class MCPClientManager {
193
223
 
194
224
  // Fetch and register tools
195
225
  const toolsResponse = await client.listTools();
226
+ const toolCount = toolsResponse?.tools?.length || 0;
196
227
 
197
228
  if (toolsResponse && toolsResponse.tools) {
198
229
  for (const tool of toolsResponse.tools) {
@@ -205,18 +236,19 @@ export class MCPClientManager {
205
236
  });
206
237
 
207
238
  if (this.debug) {
208
- console.error(`[MCP] Registered tool: ${qualifiedName}`);
239
+ console.error(`[MCP DEBUG] Registered tool: ${qualifiedName}`);
209
240
  }
210
241
  }
211
242
  }
212
243
 
213
- if (this.debug) {
214
- console.error(`[MCP] Connected to ${name} with ${toolsResponse?.tools?.length || 0} tools`);
215
- }
244
+ console.error(`[MCP INFO] Connected to ${name}: ${toolCount} tool${toolCount !== 1 ? 's' : ''} loaded`);
216
245
 
217
246
  return true;
218
247
  } catch (error) {
219
- console.error(`[MCP] Error connecting to ${name}:`, error.message);
248
+ console.error(`[MCP ERROR] Error connecting to ${name}:`, error.message);
249
+ if (this.debug) {
250
+ console.error(`[MCP DEBUG] Full error details:`, error);
251
+ }
220
252
  return false;
221
253
  }
222
254
  }
@@ -239,7 +271,7 @@ export class MCPClientManager {
239
271
 
240
272
  try {
241
273
  if (this.debug) {
242
- console.error(`[MCP] Calling ${toolName} with args:`, args);
274
+ console.error(`[MCP DEBUG] Calling ${toolName} with args:`, JSON.stringify(args, null, 2));
243
275
  }
244
276
 
245
277
  // Get timeout from config (default 30 seconds)
@@ -261,9 +293,16 @@ export class MCPClientManager {
261
293
  timeoutPromise
262
294
  ]);
263
295
 
296
+ if (this.debug) {
297
+ console.error(`[MCP DEBUG] Tool ${toolName} executed successfully`);
298
+ }
299
+
264
300
  return result;
265
301
  } catch (error) {
266
- console.error(`[MCP] Error calling tool ${toolName}:`, error);
302
+ console.error(`[MCP ERROR] Error calling tool ${toolName}:`, error.message);
303
+ if (this.debug) {
304
+ console.error(`[MCP DEBUG] Full error details:`, error);
305
+ }
267
306
  throw error;
268
307
  }
269
308
  }
@@ -316,16 +355,27 @@ export class MCPClientManager {
316
355
  async disconnect() {
317
356
  const disconnectPromises = [];
318
357
 
358
+ if (this.clients.size === 0) {
359
+ if (this.debug) {
360
+ console.error('[MCP DEBUG] No MCP clients to disconnect');
361
+ }
362
+ return;
363
+ }
364
+
365
+ if (this.debug) {
366
+ console.error(`[MCP DEBUG] Disconnecting from ${this.clients.size} MCP server${this.clients.size !== 1 ? 's' : ''}...`);
367
+ }
368
+
319
369
  for (const [name, clientInfo] of this.clients.entries()) {
320
370
  disconnectPromises.push(
321
371
  clientInfo.client.close()
322
372
  .then(() => {
323
373
  if (this.debug) {
324
- console.error(`[MCP] Disconnected from ${name}`);
374
+ console.error(`[MCP DEBUG] Disconnected from ${name}`);
325
375
  }
326
376
  })
327
377
  .catch(error => {
328
- console.error(`[MCP] Error disconnecting from ${name}:`, error);
378
+ console.error(`[MCP ERROR] Error disconnecting from ${name}:`, error.message);
329
379
  })
330
380
  );
331
381
  }
@@ -333,6 +383,10 @@ export class MCPClientManager {
333
383
  await Promise.all(disconnectPromises);
334
384
  this.clients.clear();
335
385
  this.tools.clear();
386
+
387
+ if (this.debug) {
388
+ console.error('[MCP DEBUG] All MCP connections closed');
389
+ }
336
390
  }
337
391
  }
338
392
 
@@ -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,