@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.
package/src/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/src/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,
package/src/mcp/index.ts CHANGED
@@ -14,7 +14,7 @@ import fs from 'fs-extra';
14
14
  import { fileURLToPath } from 'url';
15
15
 
16
16
  // Import from parent package
17
- import { search, query, extract, getBinaryPath, setBinaryPath } from '../index.js';
17
+ import { search, query, extract, grep, getBinaryPath, setBinaryPath } from '../index.js';
18
18
 
19
19
  // Parse command-line arguments
20
20
  function parseArgs(): { timeout?: number; format?: string } {
@@ -44,7 +44,7 @@ Usage:
44
44
 
45
45
  Options:
46
46
  --timeout, -t <seconds> Set timeout for search operations (default: 30)
47
- --format <format> Set output format (json, outline-xml, etc.)
47
+ --format <format> Set output format (default: outline)
48
48
  --help, -h Show this help message
49
49
  `);
50
50
  process.exit(0);
@@ -113,19 +113,19 @@ interface SearchCodeArgs {
113
113
  path: string;
114
114
  query: string | string[];
115
115
  exact?: boolean;
116
- maxResults?: number;
117
- maxTokens?: number;
118
- allowTests?: boolean;
119
- session?: string;
120
- noGitignore?: boolean;
121
116
  }
122
117
 
123
-
124
118
  interface ExtractCodeArgs {
125
119
  path: string;
126
120
  files: string[];
127
- allowTests?: boolean;
128
- noGitignore?: boolean;
121
+ }
122
+
123
+ interface GrepArgs {
124
+ pattern: string;
125
+ paths: string | string[];
126
+ ignoreCase?: boolean;
127
+ count?: boolean;
128
+ context?: number;
129
129
  }
130
130
 
131
131
  class ProbeServer {
@@ -151,7 +151,7 @@ class ProbeServer {
151
151
  this.setupToolHandlers();
152
152
 
153
153
  // Error handling
154
- this.server.onerror = (error) => console.error('[MCP Error]', error);
154
+ this.server.onerror = (error) => console.error('[MCP ERROR]', error);
155
155
  process.on('SIGINT', async () => {
156
156
  await this.server.close();
157
157
  process.exit(0);
@@ -165,35 +165,22 @@ class ProbeServer {
165
165
  tools: [
166
166
  {
167
167
  name: 'search_code',
168
- description: "Search code in the repository using ElasticSearch. Use this tool first for any code-related questions.",
168
+ description: "Semantic code search using AST parsing and ElasticSearch-style queries. Use this for finding code, not grep.",
169
169
  inputSchema: {
170
170
  type: 'object',
171
171
  properties: {
172
172
  path: {
173
173
  type: 'string',
174
- description: 'Absolute path to the directory to search in (e.g., "/Users/username/projects/myproject").',
174
+ description: 'Absolute path to the directory to search',
175
175
  },
176
176
  query: {
177
177
  type: 'string',
178
- description: 'Elastic search query. Supports logical operators (AND, OR, NOT), and grouping with parentheses. For exact matches of specific identifiers, use quotes: "MyFunction", "SpecificStruct", "exact_variable_name". Examples: "config", "(term1 OR term2) AND term3", "getUserData", "struct Config".',
178
+ description: 'Search query. Use quotes for exact matches: "functionName". Supports AND, OR, NOT operators.',
179
179
  },
180
180
  exact: {
181
181
  type: 'boolean',
182
- description: 'When you exactly know what you are looking for, like known function name, struct name, or variable name, set this flag for precise matching'
183
- },
184
- allowTests: {
185
- type: 'boolean',
186
- description: 'Allow test files and test code blocks in results (enabled by default)',
187
- default: true
188
- },
189
- session: {
190
- type: 'string',
191
- description: 'Session identifier for caching. Set to "new" if unknown, or want to reset cache. Re-use session ID returned from previous searches',
192
- default: "new",
193
- },
194
- noGitignore: {
195
- type: 'boolean',
196
- description: 'Skip .gitignore files (will use PROBE_NO_GITIGNORE environment variable if not set)',
182
+ description: 'Use when searching for exact function/class/variable names',
183
+ default: false
197
184
  }
198
185
  },
199
186
  required: ['path', 'query']
@@ -201,30 +188,56 @@ class ProbeServer {
201
188
  },
202
189
  {
203
190
  name: 'extract_code',
204
- description: "Extract code blocks from files based on line number, or symbol name. Fetch full file when line number is not provided.",
191
+ description: "Extract code from files. Formats: file.js (whole file), file.js:42 (from line), file.js#functionName (symbol).",
205
192
  inputSchema: {
206
193
  type: 'object',
207
194
  properties: {
208
195
  path: {
209
196
  type: 'string',
210
- description: 'Absolute path to the directory to search in (e.g., "/Users/username/projects/myproject").',
197
+ description: 'Absolute path to the project directory',
211
198
  },
212
199
  files: {
213
200
  type: 'array',
214
201
  items: { type: 'string' },
215
- description: 'Files and lines or sybmbols to extract from: /path/to/file.rs:10, /path/to/file.rs#func_name Path should be absolute.',
202
+ description: 'Array of file paths with optional line/symbol: ["file.rs:10", "file.rs#func_name"]',
203
+ }
204
+ },
205
+ required: ['path', 'files'],
206
+ },
207
+ },
208
+ {
209
+ name: 'grep',
210
+ description: "Standard grep-style search for non-code files (logs, config files, text files). Line numbers are shown by default. For code files, use search_code instead.",
211
+ inputSchema: {
212
+ type: 'object',
213
+ properties: {
214
+ pattern: {
215
+ type: 'string',
216
+ description: 'Regular expression pattern to search for',
217
+ },
218
+ paths: {
219
+ oneOf: [
220
+ { type: 'string' },
221
+ { type: 'array', items: { type: 'string' } }
222
+ ],
223
+ description: 'Path or array of paths to search in',
216
224
  },
217
- allowTests: {
225
+ ignoreCase: {
218
226
  type: 'boolean',
219
- description: 'Allow test files and test code blocks in results (enabled by default)',
220
- default: true
227
+ description: 'Case-insensitive search',
228
+ default: false
221
229
  },
222
- noGitignore: {
230
+ count: {
223
231
  type: 'boolean',
224
- description: 'Skip .gitignore files (will use PROBE_NO_GITIGNORE environment variable if not set)',
232
+ description: 'Only show count of matches per file instead of the matches',
233
+ default: false
234
+ },
235
+ context: {
236
+ type: 'number',
237
+ description: 'Number of lines of context to show before and after each match',
225
238
  }
226
239
  },
227
- required: ['path', 'files'],
240
+ required: ['pattern', 'paths'],
228
241
  },
229
242
  },
230
243
  ],
@@ -232,7 +245,7 @@ class ProbeServer {
232
245
 
233
246
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
234
247
  if (request.params.name !== 'search_code' && request.params.name !== 'extract_code' &&
235
- request.params.name !== 'probe' && request.params.name !== 'extract') {
248
+ request.params.name !== 'grep' && request.params.name !== 'probe' && request.params.name !== 'extract') {
236
249
  throw new McpError(
237
250
  ErrorCode.MethodNotFound,
238
251
  `Unknown tool: ${request.params.name}`
@@ -252,9 +265,9 @@ class ProbeServer {
252
265
  if (!request.params.arguments || typeof request.params.arguments !== 'object') {
253
266
  throw new Error("Arguments must be an object");
254
267
  }
255
-
268
+
256
269
  const args = request.params.arguments as unknown as SearchCodeArgs;
257
-
270
+
258
271
  // Validate required fields
259
272
  if (!args.path) {
260
273
  throw new Error("Path is required in arguments");
@@ -262,8 +275,11 @@ class ProbeServer {
262
275
  if (!args.query) {
263
276
  throw new Error("Query is required in arguments");
264
277
  }
265
-
278
+
266
279
  result = await this.executeCodeSearch(args);
280
+ } else if (request.params.name === 'grep') {
281
+ const args = request.params.arguments as unknown as GrepArgs;
282
+ result = await this.executeGrep(args);
267
283
  } else { // extract_code or extract
268
284
  const args = request.params.arguments as unknown as ExtractCodeArgs;
269
285
  result = await this.executeCodeExtract(args);
@@ -304,53 +320,29 @@ class ProbeServer {
304
320
  throw new Error("Query is required");
305
321
  }
306
322
 
307
- // Log the arguments we received for debugging
308
- console.error(`Received search arguments: path=${args.path}, query=${JSON.stringify(args.query)}`);
309
-
310
- // Create a clean options object with only the essential properties first
323
+ // Build options with smart defaults
311
324
  const options: any = {
312
- path: args.path.trim(), // Ensure path is trimmed
313
- query: args.query
325
+ path: args.path.trim(),
326
+ query: args.query,
327
+ // Smart defaults for MCP usage
328
+ allowTests: true, // Include test files by default
329
+ session: "new", // Fresh session each time
330
+ maxResults: 20, // Reasonable limit for context window
331
+ maxTokens: 8000, // Fits in most AI context windows
314
332
  };
315
-
316
- // Add optional parameters only if they exist
333
+
334
+ // Only override defaults if user explicitly set them
317
335
  if (args.exact !== undefined) options.exact = args.exact;
318
- if (args.maxResults !== undefined) options.maxResults = args.maxResults;
319
- if (args.maxTokens !== undefined) options.maxTokens = args.maxTokens;
320
- // Set allowTests to true by default if not specified
321
- if (args.allowTests !== undefined) {
322
- options.allowTests = args.allowTests;
323
- } else {
324
- options.allowTests = true;
325
- }
326
- // Use noGitignore from args, or fall back to PROBE_NO_GITIGNORE environment variable
327
- if (args.noGitignore !== undefined) {
328
- options.noGitignore = args.noGitignore;
329
- } else if (process.env.PROBE_NO_GITIGNORE) {
330
- options.noGitignore = process.env.PROBE_NO_GITIGNORE === 'true';
331
- }
332
- if (args.session !== undefined && args.session.trim() !== '') {
333
- options.session = args.session;
334
- } else {
335
- options.session = "new";
336
- }
337
336
 
338
- // Handle format options
339
- if (this.defaultFormat === 'outline-xml') {
340
- // For outline-xml format, we pass it as a format flag to the search command
341
- options.format = 'outline-xml';
337
+ // Handle format based on server default
338
+ if (this.defaultFormat === 'outline' || this.defaultFormat === 'outline-xml') {
339
+ options.format = this.defaultFormat;
342
340
  } else if (this.defaultFormat === 'json') {
343
341
  options.json = true;
344
342
  }
345
-
343
+
346
344
  console.error("Executing search with options:", JSON.stringify(options, null, 2));
347
345
 
348
- // Double-check that path is still in the options object
349
- if (!options.path) {
350
- console.error("Path is missing from options object after construction");
351
- throw new Error("Path is missing from options object");
352
- }
353
-
354
346
  try {
355
347
  // Call search with the options object
356
348
  const result = await search(options);
@@ -379,27 +371,14 @@ class ProbeServer {
379
371
  throw new Error("Files array is required and must not be empty");
380
372
  }
381
373
 
382
- // Create a single options object with files and other parameters
374
+ // Build options with smart defaults
383
375
  const options: any = {
384
376
  files: args.files,
385
377
  path: args.path,
386
- format: 'xml'
378
+ format: 'xml',
379
+ allowTests: true, // Include test files by default
387
380
  };
388
381
 
389
- // Set allowTests to true by default if not specified
390
- if (args.allowTests !== undefined) {
391
- options.allowTests = args.allowTests;
392
- } else {
393
- options.allowTests = true;
394
- }
395
-
396
- // Use noGitignore from args, or fall back to PROBE_NO_GITIGNORE environment variable
397
- if (args.noGitignore !== undefined) {
398
- options.noGitignore = args.noGitignore;
399
- } else if (process.env.PROBE_NO_GITIGNORE) {
400
- options.noGitignore = process.env.PROBE_NO_GITIGNORE === 'true';
401
- }
402
-
403
382
  // Call extract with the complete options object
404
383
  try {
405
384
  // Track request size for token usage
@@ -454,6 +433,50 @@ class ProbeServer {
454
433
  }
455
434
  }
456
435
 
436
+ private async executeGrep(args: GrepArgs): Promise<string> {
437
+ try {
438
+ // Validate required parameters
439
+ if (!args.pattern) {
440
+ throw new Error("Pattern is required");
441
+ }
442
+ if (!args.paths) {
443
+ throw new Error("Paths are required");
444
+ }
445
+
446
+ // Build options object with good defaults
447
+ const options: any = {
448
+ pattern: args.pattern,
449
+ paths: args.paths,
450
+ // Default: show line numbers (makes output more useful)
451
+ lineNumbers: true,
452
+ // Default: never use color in MCP context (better for parsing)
453
+ color: 'never'
454
+ };
455
+
456
+ // Only add user-specified optional parameters
457
+ if (args.ignoreCase !== undefined) options.ignoreCase = args.ignoreCase;
458
+ if (args.count !== undefined) options.count = args.count;
459
+ if (args.context !== undefined) options.context = args.context;
460
+
461
+ console.error("Executing grep with options:", JSON.stringify(options, null, 2));
462
+
463
+ try {
464
+ // Call grep with the options object
465
+ const result = await grep(options);
466
+ return result || 'No matches found';
467
+ } catch (grepError: any) {
468
+ console.error("Grep function error:", grepError);
469
+ throw new Error(`Grep function error: ${grepError.message || String(grepError)}`);
470
+ }
471
+ } catch (error: any) {
472
+ console.error('Error executing grep:', error);
473
+ throw new McpError(
474
+ 'MethodNotFound' as unknown as ErrorCode,
475
+ `Error executing grep: ${error.message || String(error)}`
476
+ );
477
+ }
478
+ }
479
+
457
480
  async run() {
458
481
  // The @probelabs/probe package now handles binary path management internally
459
482
  // We don't need to verify or download the binary in the MCP server anymore
@@ -465,5 +488,5 @@ class ProbeServer {
465
488
  }
466
489
  }
467
490
 
468
- const server = new ProbeServer(cliConfig.timeout, cliConfig.format || 'outline-xml');
491
+ const server = new ProbeServer(cliConfig.timeout, cliConfig.format || 'outline');
469
492
  server.run().catch(console.error);