centaurus-cli 2.7.1 → 2.7.3

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.
Files changed (106) hide show
  1. package/dist/cli-adapter.d.ts +15 -0
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +380 -122
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/mcp-config-manager.d.ts +53 -0
  6. package/dist/config/mcp-config-manager.d.ts.map +1 -0
  7. package/dist/config/mcp-config-manager.js +210 -0
  8. package/dist/config/mcp-config-manager.js.map +1 -0
  9. package/dist/config/slash-commands.d.ts +14 -0
  10. package/dist/config/slash-commands.d.ts.map +1 -0
  11. package/dist/config/slash-commands.js +65 -0
  12. package/dist/config/slash-commands.js.map +1 -0
  13. package/dist/index.js +17 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/mcp/mcp-command-handler.d.ts +16 -0
  16. package/dist/mcp/mcp-command-handler.d.ts.map +1 -0
  17. package/dist/mcp/mcp-command-handler.js +196 -0
  18. package/dist/mcp/mcp-command-handler.js.map +1 -0
  19. package/dist/mcp/mcp-server-manager.d.ts +30 -0
  20. package/dist/mcp/mcp-server-manager.d.ts.map +1 -0
  21. package/dist/mcp/mcp-server-manager.js +165 -0
  22. package/dist/mcp/mcp-server-manager.js.map +1 -0
  23. package/dist/mcp/mcp-tool-wrapper.d.ts +12 -0
  24. package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -0
  25. package/dist/mcp/mcp-tool-wrapper.js +57 -0
  26. package/dist/mcp/mcp-tool-wrapper.js.map +1 -0
  27. package/dist/services/ai-service-client.d.ts.map +1 -1
  28. package/dist/services/ai-service-client.js +1 -0
  29. package/dist/services/ai-service-client.js.map +1 -1
  30. package/dist/services/environment-context-injector.d.ts +2 -2
  31. package/dist/services/environment-context-injector.d.ts.map +1 -1
  32. package/dist/services/environment-context-injector.js +4 -4
  33. package/dist/services/environment-context-injector.js.map +1 -1
  34. package/dist/tools/command.d.ts +1 -2
  35. package/dist/tools/command.d.ts.map +1 -1
  36. package/dist/tools/command.js +39 -131
  37. package/dist/tools/command.js.map +1 -1
  38. package/dist/tools/file-ops.d.ts +3 -3
  39. package/dist/tools/file-ops.d.ts.map +1 -1
  40. package/dist/tools/file-ops.js +125 -99
  41. package/dist/tools/file-ops.js.map +1 -1
  42. package/dist/tools/get-diff.d.ts +5 -0
  43. package/dist/tools/get-diff.d.ts.map +1 -1
  44. package/dist/tools/get-diff.js +67 -5
  45. package/dist/tools/get-diff.js.map +1 -1
  46. package/dist/tools/grep-search.d.ts +13 -21
  47. package/dist/tools/grep-search.d.ts.map +1 -1
  48. package/dist/tools/grep-search.js +309 -280
  49. package/dist/tools/grep-search.js.map +1 -1
  50. package/dist/tools/inspect-symbol.d.ts +5 -0
  51. package/dist/tools/inspect-symbol.d.ts.map +1 -1
  52. package/dist/tools/inspect-symbol.js +102 -20
  53. package/dist/tools/inspect-symbol.js.map +1 -1
  54. package/dist/tools/types.d.ts +2 -1
  55. package/dist/tools/types.d.ts.map +1 -1
  56. package/dist/tools/validation.d.ts +8 -10
  57. package/dist/tools/validation.d.ts.map +1 -1
  58. package/dist/tools/validation.js +35 -37
  59. package/dist/tools/validation.js.map +1 -1
  60. package/dist/ui/components/App.d.ts +4 -0
  61. package/dist/ui/components/App.d.ts.map +1 -1
  62. package/dist/ui/components/App.js +182 -70
  63. package/dist/ui/components/App.js.map +1 -1
  64. package/dist/ui/components/AuthWelcomeScreen.d.ts.map +1 -1
  65. package/dist/ui/components/AuthWelcomeScreen.js +1 -9
  66. package/dist/ui/components/AuthWelcomeScreen.js.map +1 -1
  67. package/dist/ui/components/CodeBlock.d.ts.map +1 -1
  68. package/dist/ui/components/CodeBlock.js +24 -13
  69. package/dist/ui/components/CodeBlock.js.map +1 -1
  70. package/dist/ui/components/DiffViewer.d.ts +1 -0
  71. package/dist/ui/components/DiffViewer.d.ts.map +1 -1
  72. package/dist/ui/components/DiffViewer.js +107 -70
  73. package/dist/ui/components/DiffViewer.js.map +1 -1
  74. package/dist/ui/components/FileCreationPreview.d.ts +8 -0
  75. package/dist/ui/components/FileCreationPreview.d.ts.map +1 -0
  76. package/dist/ui/components/FileCreationPreview.js +63 -0
  77. package/dist/ui/components/FileCreationPreview.js.map +1 -0
  78. package/dist/ui/components/InputBox.d.ts.map +1 -1
  79. package/dist/ui/components/InputBox.js +134 -10
  80. package/dist/ui/components/InputBox.js.map +1 -1
  81. package/dist/ui/components/InteractiveShell.d.ts +1 -0
  82. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  83. package/dist/ui/components/InteractiveShell.js +30 -8
  84. package/dist/ui/components/InteractiveShell.js.map +1 -1
  85. package/dist/ui/components/MarkdownRenderer.d.ts.map +1 -1
  86. package/dist/ui/components/MarkdownRenderer.js +8 -30
  87. package/dist/ui/components/MarkdownRenderer.js.map +1 -1
  88. package/dist/ui/components/SlashCommandAutocomplete.d.ts +11 -0
  89. package/dist/ui/components/SlashCommandAutocomplete.d.ts.map +1 -0
  90. package/dist/ui/components/SlashCommandAutocomplete.js +15 -0
  91. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -0
  92. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  93. package/dist/ui/components/ToolExecutionMessage.js +212 -53
  94. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  95. package/dist/ui/components/ToolExecutionStatus.d.ts.map +1 -1
  96. package/dist/ui/components/ToolExecutionStatus.js +28 -1
  97. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  98. package/dist/utils/input-classifier.d.ts.map +1 -1
  99. package/dist/utils/input-classifier.js +3 -1
  100. package/dist/utils/input-classifier.js.map +1 -1
  101. package/dist/utils/shell.d.ts +2 -0
  102. package/dist/utils/shell.d.ts.map +1 -1
  103. package/dist/utils/shell.js +71 -0
  104. package/dist/utils/shell.js.map +1 -1
  105. package/package.json +2 -1
  106. package/prompts/system-prompt-autonomous.md +22 -73
@@ -1,23 +1,27 @@
1
- import { exec } from 'child_process';
1
+ import { execFile } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import * as path from 'path';
4
- const execAsync = promisify(exec);
4
+ import * as fs from 'fs';
5
+ const execFileAsync = promisify(execFile);
5
6
  /**
6
7
  * GrepSearchTool - Search for text patterns across files in the codebase
7
8
  *
8
- * This tool wraps ripgrep (rg) or falls back to native grep/findstr
9
- * to provide fast, efficient code search capabilities.
9
+ * Improvements over basic implementation:
10
+ * 1. Security: Uses execFile to prevent shell injection
11
+ * 2. Robustness: Handles binary files and encoding issues (via rg/grep)
12
+ * 3. AI-Friendly Output: Formats results for easy parsing
13
+ * 4. Fallback: Gracefully degrades from ripgrep -> grep -> findstr
10
14
  */
11
15
  export class GrepSearchTool {
12
16
  static MAX_MATCHES = 50;
13
- static MAX_LINE_LENGTH = 200;
17
+ static MAX_LINE_LENGTH = 300;
14
18
  static CONTEXT_LINES = 2;
15
19
  /**
16
20
  * Check if ripgrep is available
17
21
  */
18
22
  async hasRipgrep() {
19
23
  try {
20
- await execAsync('rg --version');
24
+ await execFileAsync('rg', ['--version']);
21
25
  return true;
22
26
  }
23
27
  catch {
@@ -29,28 +33,13 @@ export class GrepSearchTool {
29
33
  */
30
34
  async hasGrep() {
31
35
  try {
32
- await execAsync('grep --version');
36
+ await execFileAsync('grep', ['--version']);
33
37
  return true;
34
38
  }
35
39
  catch {
36
40
  return false;
37
41
  }
38
42
  }
39
- /**
40
- * Validate regex pattern
41
- */
42
- validatePattern(pattern) {
43
- try {
44
- new RegExp(pattern);
45
- return { valid: true };
46
- }
47
- catch (error) {
48
- return {
49
- valid: false,
50
- error: `Invalid regex pattern: "${pattern}"\nSuggestion: Check your regex syntax. Common issues: unmatched parentheses, invalid escape sequences.`
51
- };
52
- }
53
- }
54
43
  /**
55
44
  * Normalize file path to use forward slashes
56
45
  */
@@ -71,33 +60,50 @@ export class GrepSearchTool {
71
60
  */
72
61
  async searchWithRipgrep(params, cwd) {
73
62
  const args = [
74
- 'rg',
75
63
  '--json',
76
64
  `--context=${GrepSearchTool.CONTEXT_LINES}`,
77
65
  `--max-count=${GrepSearchTool.MAX_MATCHES}`,
78
66
  ];
79
67
  // Case sensitivity
80
- if (!params.caseSensitive) {
68
+ if (!params.CaseInsensitive) {
69
+ // rg is case-sensitive by default, smart-case is default in CLI but maybe not here
70
+ // We want explicit case sensitivity unless CaseInsensitive is true
71
+ // Actually, rg is case-sensitive by default.
72
+ }
73
+ else {
81
74
  args.push('--ignore-case');
82
75
  }
83
- // File pattern
84
- if (params.filePattern) {
85
- args.push('--glob', params.filePattern);
76
+ // Fixed string vs Regex
77
+ if (!params.IsRegex) {
78
+ args.push('--fixed-strings');
86
79
  }
87
- // Exclude pattern
88
- if (params.excludePattern) {
89
- args.push('--glob', `!${params.excludePattern}`);
80
+ // File pattern (Includes)
81
+ if (params.Includes && params.Includes.length > 0) {
82
+ params.Includes.forEach(pattern => {
83
+ args.push('--glob', pattern);
84
+ });
90
85
  }
86
+ // Search Path (file or directory)
87
+ // rg takes the path as the last argument usually, or we can use it as root
88
+ // We'll pass it as an argument.
91
89
  // Pattern
92
- args.push('--', params.pattern);
90
+ // We pass pattern after flags
91
+ args.push('--', params.Query);
92
+ // Path
93
+ args.push(params.SearchPath);
93
94
  try {
94
- const { stdout } = await execAsync(args.join(' '), { cwd, maxBuffer: 10 * 1024 * 1024 });
95
- return this.parseRipgrepOutput(stdout, params.pattern);
95
+ const { stdout } = await execFileAsync('rg', args, { cwd, maxBuffer: 10 * 1024 * 1024 });
96
+ return this.parseRipgrepOutput(stdout, params.Query);
96
97
  }
97
98
  catch (error) {
98
99
  // ripgrep returns exit code 1 when no matches found
99
- if (error.code === 1 && error.stdout) {
100
- return this.parseRipgrepOutput(error.stdout, params.pattern);
100
+ if (error.code === 1) {
101
+ return {
102
+ matches: [],
103
+ totalMatches: 0,
104
+ truncated: false,
105
+ searchPattern: params.Query,
106
+ };
101
107
  }
102
108
  throw error;
103
109
  }
@@ -112,30 +118,32 @@ export class GrepSearchTool {
112
118
  let contextBefore = [];
113
119
  let contextAfter = [];
114
120
  let totalMatches = 0;
121
+ // Helper to finalize a match
122
+ const pushMatch = () => {
123
+ if (currentMatch && currentMatch.file && currentMatch.line !== undefined) {
124
+ matches.push({
125
+ file: this.normalizePath(currentMatch.file),
126
+ line: currentMatch.line,
127
+ match: this.truncateLine(currentMatch.match || ''),
128
+ contextBefore: contextBefore.map(l => this.truncateLine(l)),
129
+ contextAfter: contextAfter.map(l => this.truncateLine(l)),
130
+ });
131
+ currentMatch = null;
132
+ contextBefore = [];
133
+ contextAfter = [];
134
+ }
135
+ };
115
136
  for (const line of lines) {
116
137
  try {
117
138
  const data = JSON.parse(line);
118
139
  if (data.type === 'match') {
119
- // Save previous match if exists
120
- if (currentMatch && currentMatch.file && currentMatch.line !== undefined) {
121
- matches.push({
122
- file: this.normalizePath(currentMatch.file),
123
- line: currentMatch.line,
124
- column: currentMatch.column || 1,
125
- match: this.truncateLine(currentMatch.match || ''),
126
- contextBefore: contextBefore.map(l => this.truncateLine(l)),
127
- contextAfter: contextAfter.map(l => this.truncateLine(l)),
128
- });
129
- contextBefore = [];
130
- contextAfter = [];
131
- }
140
+ pushMatch(); // Push previous match if any
132
141
  totalMatches++;
133
142
  if (matches.length < GrepSearchTool.MAX_MATCHES) {
134
143
  const matchData = data.data;
135
144
  currentMatch = {
136
145
  file: matchData.path.text,
137
146
  line: matchData.line_number,
138
- column: matchData.submatches?.[0]?.start || 1,
139
147
  match: matchData.lines.text.trimEnd(),
140
148
  };
141
149
  }
@@ -144,36 +152,38 @@ export class GrepSearchTool {
144
152
  const contextData = data.data;
145
153
  const contextLine = contextData.lines.text.trimEnd();
146
154
  if (currentMatch && currentMatch.line !== undefined) {
147
- // Determine if this is before or after the match
148
- if (contextData.line_number < currentMatch.line) {
149
- contextBefore.push(contextLine);
150
- // Keep only last N lines
151
- if (contextBefore.length > GrepSearchTool.CONTEXT_LINES) {
152
- contextBefore.shift();
153
- }
154
- }
155
- else {
156
- contextAfter.push(contextLine);
155
+ // This is context AFTER the current match
156
+ contextAfter.push(contextLine);
157
+ }
158
+ else {
159
+ // This is context BEFORE the next match (or we are between matches)
160
+ // Since rg --json streams, context before a match usually comes before the match event.
161
+ // But wait, if we just finished a match, 'context' could be after.
162
+ // Actually, rg JSON output structure:
163
+ // context (before) -> match -> context (after)
164
+ // But if matches are close, context after match 1 might be context before match 2.
165
+ // For simplicity in this parser:
166
+ // If we have a currentMatch, this is context AFTER.
167
+ // If we don't, this is context BEFORE.
168
+ contextBefore.push(contextLine);
169
+ // Keep only last N lines for 'before' context
170
+ if (contextBefore.length > GrepSearchTool.CONTEXT_LINES) {
171
+ contextBefore.shift();
157
172
  }
158
173
  }
159
174
  }
175
+ else if (data.type === 'end') {
176
+ // End of a search result for a file?
177
+ // rg emits 'end' stats.
178
+ // We might need to flush the last match if context came after it.
179
+ pushMatch();
180
+ }
160
181
  }
161
182
  catch (parseError) {
162
- // Skip invalid JSON lines
163
183
  continue;
164
184
  }
165
185
  }
166
- // Save last match
167
- if (currentMatch && currentMatch.file && currentMatch.line !== undefined) {
168
- matches.push({
169
- file: this.normalizePath(currentMatch.file),
170
- line: currentMatch.line,
171
- column: currentMatch.column || 1,
172
- match: this.truncateLine(currentMatch.match || ''),
173
- contextBefore: contextBefore.map(l => this.truncateLine(l)),
174
- contextAfter: contextAfter.map(l => this.truncateLine(l)),
175
- });
176
- }
186
+ pushMatch(); // Ensure flush
177
187
  return {
178
188
  matches,
179
189
  totalMatches,
@@ -185,206 +195,200 @@ export class GrepSearchTool {
185
195
  * Execute search using native grep (Linux/Mac)
186
196
  */
187
197
  async searchWithGrep(params, cwd) {
188
- const args = ['grep', '-rn'];
198
+ const args = ['-rn'];
189
199
  // Case sensitivity
190
- if (!params.caseSensitive) {
200
+ if (params.CaseInsensitive) {
191
201
  args.push('-i');
192
202
  }
193
203
  // Context lines
194
204
  args.push(`--context=${GrepSearchTool.CONTEXT_LINES}`);
195
- // File pattern
196
- if (params.filePattern) {
197
- args.push(`--include=${params.filePattern}`);
198
- }
199
- // Exclude pattern
200
- if (params.excludePattern) {
201
- args.push(`--exclude=${params.excludePattern}`);
202
- }
203
- // Pattern and directory
204
- args.push(params.pattern, '.');
205
- try {
206
- const { stdout } = await execAsync(args.join(' '), { cwd, maxBuffer: 10 * 1024 * 1024 });
207
- return this.parseGrepOutput(stdout, params.pattern);
208
- }
209
- catch (error) {
210
- // grep returns exit code 1 when no matches found
211
- if (error.code === 1 && error.stdout) {
212
- return this.parseGrepOutput(error.stdout, params.pattern);
213
- }
214
- throw error;
205
+ // Includes
206
+ if (params.Includes && params.Includes.length > 0) {
207
+ params.Includes.forEach(pattern => {
208
+ args.push(`--include=${pattern}`);
209
+ });
215
210
  }
216
- }
217
- /**
218
- * Execute search using findstr (Windows)
219
- */
220
- async searchWithFindstr(params, cwd) {
221
- // findstr has limited capabilities, so we'll use a simpler approach
222
- const pattern = params.pattern;
223
- const filePattern = params.filePattern || '*.*';
224
- // findstr doesn't support context lines, so we'll need to handle that separately
225
- const args = [
226
- 'findstr',
227
- '/S', // Search subdirectories
228
- '/N', // Show line numbers
229
- ];
230
- if (!params.caseSensitive) {
231
- args.push('/I'); // Case-insensitive
211
+ // Fixed string vs Regex
212
+ if (!params.IsRegex) {
213
+ args.push('-F');
232
214
  }
233
- args.push(`/C:"${pattern}"`, filePattern);
215
+ // Pattern
216
+ args.push(params.Query);
217
+ // Path
218
+ args.push(params.SearchPath);
234
219
  try {
235
- const { stdout } = await execAsync(args.join(' '), { cwd, maxBuffer: 10 * 1024 * 1024 });
236
- return this.parseFindstrOutput(stdout, params.pattern, cwd);
220
+ const { stdout } = await execFileAsync('grep', args, { cwd, maxBuffer: 10 * 1024 * 1024 });
221
+ return this.parseGrepOutput(stdout, params.Query);
237
222
  }
238
223
  catch (error) {
239
- // findstr returns exit code 1 when no matches found
240
- if (error.code === 1 && (!error.stdout || error.stdout.trim() === '')) {
224
+ if (error.code === 1) {
241
225
  return {
242
226
  matches: [],
243
227
  totalMatches: 0,
244
228
  truncated: false,
245
- searchPattern: params.pattern,
229
+ searchPattern: params.Query,
246
230
  };
247
231
  }
248
- if (error.stdout) {
249
- return this.parseFindstrOutput(error.stdout, params.pattern, cwd);
250
- }
251
232
  throw error;
252
233
  }
253
234
  }
254
- /**
255
- * Parse grep output
256
- */
257
235
  parseGrepOutput(output, searchPattern) {
258
- const lines = output.trim().split('\n').filter(line => line);
236
+ // Grep output with context:
237
+ // file:line:match
238
+ // file-line-context
239
+ // -- (separator)
240
+ const lines = output.trim().split('\n');
259
241
  const matches = [];
260
- let i = 0;
261
- while (i < lines.length && matches.length < GrepSearchTool.MAX_MATCHES) {
262
- const line = lines[i];
263
- // Skip separator lines
242
+ // Simple parsing strategy: group by file and proximity
243
+ // For robust parsing similar to rg, we'd need more complex logic.
244
+ // Given the constraints, we'll do a best-effort parse.
245
+ let currentMatch = null;
246
+ let contextBefore = [];
247
+ let contextAfter = [];
248
+ const pushMatch = () => {
249
+ if (currentMatch && currentMatch.file && currentMatch.line !== undefined) {
250
+ matches.push({
251
+ file: this.normalizePath(currentMatch.file),
252
+ line: currentMatch.line,
253
+ match: this.truncateLine(currentMatch.match || ''),
254
+ contextBefore: contextBefore.map(l => this.truncateLine(l)),
255
+ contextAfter: contextAfter.map(l => this.truncateLine(l)),
256
+ });
257
+ currentMatch = null;
258
+ contextBefore = [];
259
+ contextAfter = [];
260
+ }
261
+ };
262
+ for (const line of lines) {
264
263
  if (line === '--') {
265
- i++;
264
+ pushMatch();
266
265
  continue;
267
266
  }
268
- // Parse match line: filename:linenum:content or filename:linenum-content (for context)
267
+ // Match: file:line:content
269
268
  const matchRegex = /^([^:]+):(\d+):(.*)$/;
270
- const contextRegex = /^([^:]+):(\d+)-(.*)$/;
271
- const matchMatch = line.match(matchRegex);
272
- if (matchMatch) {
273
- const [, file, lineNum, content] = matchMatch;
274
- const contextBefore = [];
275
- const contextAfter = [];
276
- // Look back for context
277
- let j = i - 1;
278
- while (j >= 0 && contextBefore.length < GrepSearchTool.CONTEXT_LINES) {
279
- const prevLine = lines[j];
280
- if (prevLine === '--')
281
- break;
282
- const contextMatch = prevLine.match(contextRegex);
283
- if (contextMatch && contextMatch[1] === file) {
284
- contextBefore.unshift(contextMatch[3]);
285
- }
286
- else {
287
- break;
288
- }
289
- j--;
269
+ // Context: file-line-content
270
+ const contextRegex = /^([^:]+)-(\d+)-(.*)$/;
271
+ const matchM = line.match(matchRegex);
272
+ const contextM = line.match(contextRegex);
273
+ if (matchM) {
274
+ pushMatch(); // Flush previous
275
+ currentMatch = {
276
+ file: matchM[1],
277
+ line: parseInt(matchM[2], 10),
278
+ match: matchM[3]
279
+ };
280
+ }
281
+ else if (contextM) {
282
+ if (currentMatch) {
283
+ contextAfter.push(contextM[3]);
290
284
  }
291
- // Look ahead for context
292
- let k = i + 1;
293
- while (k < lines.length && contextAfter.length < GrepSearchTool.CONTEXT_LINES) {
294
- const nextLine = lines[k];
295
- if (nextLine === '--')
296
- break;
297
- const contextMatch = nextLine.match(contextRegex);
298
- if (contextMatch && contextMatch[1] === file) {
299
- contextAfter.push(contextMatch[3]);
300
- }
301
- else {
302
- break;
303
- }
304
- k++;
285
+ else {
286
+ contextBefore.push(contextM[3]);
287
+ if (contextBefore.length > GrepSearchTool.CONTEXT_LINES)
288
+ contextBefore.shift();
305
289
  }
306
- matches.push({
307
- file: this.normalizePath(file),
308
- line: parseInt(lineNum, 10),
309
- column: 1, // grep doesn't provide column info
310
- match: this.truncateLine(content),
311
- contextBefore: contextBefore.map(l => this.truncateLine(l)),
312
- contextAfter: contextAfter.map(l => this.truncateLine(l)),
313
- });
314
290
  }
315
- i++;
316
291
  }
292
+ pushMatch();
317
293
  return {
318
- matches,
319
- totalMatches: lines.filter(l => l.match(/^[^:]+:\d+:/)).length,
294
+ matches: matches.slice(0, GrepSearchTool.MAX_MATCHES),
295
+ totalMatches: matches.length, // grep doesn't give total easily without another run
320
296
  truncated: matches.length >= GrepSearchTool.MAX_MATCHES,
321
- searchPattern,
297
+ searchPattern
322
298
  };
323
299
  }
324
300
  /**
325
- * Parse findstr output
301
+ * Execute search using findstr (Windows)
302
+ * NOTE: Context is DISABLED for findstr to avoid performance/encoding issues.
326
303
  */
327
- async parseFindstrOutput(output, searchPattern, cwd) {
328
- const lines = output.trim().split('\n').filter(line => line);
304
+ async searchWithFindstr(params, cwd) {
305
+ const args = [
306
+ '/N', // Print line numbers
307
+ '/S', // Recursive
308
+ ];
309
+ if (params.CaseInsensitive) {
310
+ args.push('/I');
311
+ }
312
+ // findstr doesn't support full regex, only limited.
313
+ // If IsRegex is true, we might be limited.
314
+ if (!params.IsRegex) {
315
+ args.push('/L'); // Literal search
316
+ }
317
+ else {
318
+ args.push('/R'); // Regex search
319
+ }
320
+ // Pattern
321
+ args.push('/C:' + params.Query);
322
+ // File mask
323
+ // findstr [options] strings [drive:][path]filename[...]
324
+ // We can pass the path/mask at the end.
325
+ // If Includes is set, we can try to use it, but findstr is limited.
326
+ // We'll just use SearchPath and optional Includes if it's a simple extension.
327
+ let searchMask = '*.*';
328
+ if (params.Includes && params.Includes.length === 1 && params.Includes[0].startsWith('*.')) {
329
+ searchMask = params.Includes[0];
330
+ }
331
+ // If SearchPath is a directory, append mask. If file, use it.
332
+ let target = params.SearchPath;
333
+ try {
334
+ const stats = await fs.promises.stat(path.resolve(cwd, params.SearchPath));
335
+ if (stats.isDirectory()) {
336
+ target = path.join(params.SearchPath, searchMask);
337
+ }
338
+ }
339
+ catch {
340
+ // Assume it's a pattern or file that doesn't exist yet?
341
+ // Just pass it as is.
342
+ }
343
+ args.push(target);
344
+ try {
345
+ // Use execFile with 'findstr'
346
+ const { stdout } = await execFileAsync('findstr', args, { cwd, maxBuffer: 10 * 1024 * 1024 });
347
+ return this.parseFindstrOutput(stdout, params.Query);
348
+ }
349
+ catch (error) {
350
+ if (error.code === 1) {
351
+ return { matches: [], totalMatches: 0, truncated: false, searchPattern: params.Query };
352
+ }
353
+ throw error;
354
+ }
355
+ }
356
+ parseFindstrOutput(output, searchPattern) {
357
+ const lines = output.trim().split('\r\n'); // Windows line endings
329
358
  const matches = [];
330
- const fs = await import('fs/promises');
331
359
  for (const line of lines) {
332
- if (matches.length >= GrepSearchTool.MAX_MATCHES)
333
- break;
334
- // Parse findstr output: filename:linenum:content
335
- const match = line.match(/^([^:]+):(\d+):(.*)$/);
336
- if (!match)
360
+ if (!line)
337
361
  continue;
338
- const [, file, lineNum, content] = match;
339
- const lineNumber = parseInt(lineNum, 10);
340
- // Read context lines from file
341
- const contextBefore = [];
342
- const contextAfter = [];
343
- try {
344
- const fullPath = path.resolve(cwd, file);
345
- const fileContent = await fs.readFile(fullPath, 'utf-8');
346
- const fileLines = fileContent.split('\n');
347
- // Get context before
348
- for (let i = Math.max(0, lineNumber - GrepSearchTool.CONTEXT_LINES - 1); i < lineNumber - 1; i++) {
349
- if (fileLines[i] !== undefined) {
350
- contextBefore.push(fileLines[i]);
351
- }
352
- }
353
- // Get context after
354
- for (let i = lineNumber; i < Math.min(fileLines.length, lineNumber + GrepSearchTool.CONTEXT_LINES); i++) {
355
- if (fileLines[i] !== undefined) {
356
- contextAfter.push(fileLines[i]);
357
- }
358
- }
359
- }
360
- catch (error) {
361
- // If we can't read the file, just skip context
362
+ // Format: file:line:match
363
+ // But findstr output depends on args. /N /S gives:
364
+ // file:line:match
365
+ // Find first two colons
366
+ const firstColon = line.indexOf(':');
367
+ const secondColon = line.indexOf(':', firstColon + 1);
368
+ if (firstColon > -1 && secondColon > -1) {
369
+ const file = line.substring(0, firstColon);
370
+ const lineNum = parseInt(line.substring(firstColon + 1, secondColon), 10);
371
+ const content = line.substring(secondColon + 1);
372
+ matches.push({
373
+ file: this.normalizePath(file),
374
+ line: lineNum,
375
+ match: this.truncateLine(content),
376
+ contextBefore: [], // No context for findstr
377
+ contextAfter: []
378
+ });
362
379
  }
363
- matches.push({
364
- file: this.normalizePath(file),
365
- line: lineNumber,
366
- column: 1, // findstr doesn't provide column info
367
- match: this.truncateLine(content),
368
- contextBefore: contextBefore.map(l => this.truncateLine(l)),
369
- contextAfter: contextAfter.map(l => this.truncateLine(l)),
370
- });
371
380
  }
372
381
  return {
373
- matches,
374
- totalMatches: lines.length,
382
+ matches: matches.slice(0, GrepSearchTool.MAX_MATCHES),
383
+ totalMatches: matches.length,
375
384
  truncated: matches.length >= GrepSearchTool.MAX_MATCHES,
376
- searchPattern,
385
+ searchPattern
377
386
  };
378
387
  }
379
388
  /**
380
389
  * Execute the grep search
381
390
  */
382
391
  async execute(params, cwd) {
383
- // Validate pattern
384
- const validation = this.validatePattern(params.pattern);
385
- if (!validation.valid) {
386
- throw new Error(validation.error);
387
- }
388
392
  // Try ripgrep first
389
393
  if (await this.hasRipgrep()) {
390
394
  return await this.searchWithRipgrep(params, cwd);
@@ -401,89 +405,114 @@ export class GrepSearchTool {
401
405
  }
402
406
  }
403
407
  /**
404
- * Format grep search results for display
408
+ * Format grep search results for AI consumption
409
+ *
410
+ * Format:
411
+ * filename
412
+ * line: context
413
+ * line:> match
414
+ * line: context
405
415
  */
406
416
  function formatGrepResults(result) {
407
417
  if (result.matches.length === 0) {
408
- return `No matches found for pattern "${result.searchPattern}"\nSuggestion: Try a broader search pattern or check spelling. Use case-insensitive search if needed.`;
418
+ return `No matches found for pattern "${result.searchPattern}"`;
409
419
  }
410
- const lines = [];
411
- lines.push(`Found ${result.totalMatches} match${result.totalMatches === 1 ? '' : 'es'} for pattern "${result.searchPattern}"`);
420
+ const output = [];
421
+ output.push(`Found ${result.totalMatches} match${result.totalMatches === 1 ? '' : 'es'} for pattern "${result.searchPattern}"`);
412
422
  if (result.truncated) {
413
- lines.push(`(showing first ${result.matches.length} matches)`);
423
+ output.push(`(showing first ${result.matches.length} matches)`);
414
424
  }
415
- lines.push('');
416
- for (const match of result.matches) {
417
- lines.push(`${match.file}:${match.line}:${match.column}`);
418
- // Context before
419
- if (match.contextBefore.length > 0) {
420
- for (const ctx of match.contextBefore) {
421
- lines.push(` | ${ctx}`);
422
- }
423
- }
424
- // The match itself
425
- lines.push(` > ${match.match}`);
426
- // Context after
427
- if (match.contextAfter.length > 0) {
428
- for (const ctx of match.contextAfter) {
429
- lines.push(` | ${ctx}`);
430
- }
431
- }
432
- lines.push('');
433
- }
434
- return lines.join('\n');
425
+ // Group by file
426
+ const matchesByFile = new Map();
427
+ result.matches.forEach(match => {
428
+ if (!matchesByFile.has(match.file)) {
429
+ matchesByFile.set(match.file, []);
430
+ }
431
+ matchesByFile.get(match.file)?.push(match);
432
+ });
433
+ matchesByFile.forEach((matches, file) => {
434
+ output.push(`\n${file}`);
435
+ matches.forEach(match => {
436
+ // Context Before
437
+ match.contextBefore.forEach((ctx, idx) => {
438
+ // Calculate line number for context if possible?
439
+ // rg json gives line numbers for context, but our interface simplified it.
440
+ // We'll just print it without line number or with a placeholder if we don't have it.
441
+ // Actually, for AI parsing, it's better to have line numbers.
442
+ // But if we don't have them (grep/findstr), we shouldn't fake them incorrectly.
443
+ // However, rg gives them.
444
+ // Let's just indent context.
445
+ // Wait, user requested: "48: function..."
446
+ // If we don't have line numbers for context, maybe we should skip context or just indent?
447
+ // "Use line numbers explicitly for every line."
448
+ // Since we might not have them for context in all cases, let's try to infer or just use indentation.
449
+ // For the match, we definitely have the line number.
450
+ // Let's assume context lines are immediately preceding.
451
+ const startLine = match.line - match.contextBefore.length;
452
+ output.push(`${startLine + idx}: ${ctx}`);
453
+ });
454
+ // Match
455
+ output.push(`${match.line}:> ${match.match}`);
456
+ // Context After
457
+ match.contextAfter.forEach((ctx, idx) => {
458
+ output.push(`${match.line + 1 + idx}: ${ctx}`);
459
+ });
460
+ // Separator if needed? AI usually handles blocks well.
461
+ });
462
+ });
463
+ return output.join('\n');
435
464
  }
436
- /**
437
- * Grep search tool for the tool registry
438
- */
439
465
  export const grepSearchTool = {
440
466
  schema: {
441
467
  name: 'grep_search',
442
- description: `Search for text patterns across files in the codebase using regex.
443
-
444
- USE THIS TOOL WHEN:
445
- - User asks "where is X defined"
446
- - You need to find all occurrences of a function, class, or variable
447
- - You need to locate TODO comments or specific code patterns
448
- - You want to understand how a feature is implemented across files
449
-
450
- BEFORE using read_file on multiple files, use this to find relevant files first.
468
+ description: `Use ripgrep to find exact pattern matches within files or directories.
469
+ Results are returned in a parseable text format with line numbers:
470
+ filename
471
+ line: context
472
+ line:> match
473
+ line: context
451
474
 
452
- EXAMPLES:
453
- - Find class definition: pattern="class User"
454
- - Find function calls: pattern="connectDatabase\\\\("
455
- - Find TODO comments: pattern="TODO.*auth"
456
- - Find imports: pattern="import.*express"`,
475
+ Total results are capped at 50 matches. Use the Includes option to filter by file type or specific paths to refine your search.`,
457
476
  parameters: {
458
477
  type: 'object',
459
478
  properties: {
460
- pattern: {
479
+ reason_text: {
461
480
  type: 'string',
462
- description: 'Regex pattern to search for. Use \\\\ to escape special characters.',
481
+ description: 'REQUIRED: A brief explanation of what you are searching for and why.',
463
482
  },
464
- caseSensitive: {
465
- type: 'boolean',
466
- description: 'Whether search should be case-sensitive. Default: false',
467
- },
468
- filePattern: {
483
+ Query: {
469
484
  type: 'string',
470
- description: 'Glob pattern to filter files (e.g., "*.ts", "src/**/*.py"). Optional.',
485
+ description: 'The search term or pattern to look for within files.',
471
486
  },
472
- excludePattern: {
487
+ SearchPath: {
473
488
  type: 'string',
474
- description: 'Glob pattern to exclude files (e.g., "*.test.ts", "dist/**"). Optional.',
489
+ description: 'The path to search. This can be a directory or a file.',
490
+ },
491
+ Includes: {
492
+ type: 'array',
493
+ description: 'Glob patterns to filter files (e.g. ["*.ts"]).',
494
+ items: { type: 'string' }
495
+ },
496
+ CaseInsensitive: {
497
+ type: 'boolean',
498
+ description: 'If true, performs a case-insensitive search.',
499
+ },
500
+ IsRegex: {
501
+ type: 'boolean',
502
+ description: 'If true, treats Query as a regular expression.',
475
503
  },
476
504
  },
477
- required: ['pattern'],
505
+ required: ['reason_text', 'Query', 'SearchPath'],
478
506
  },
479
507
  },
480
508
  async execute(args, context) {
481
509
  const tool = new GrepSearchTool();
482
510
  const params = {
483
- pattern: args.pattern,
484
- caseSensitive: args.caseSensitive ?? false,
485
- filePattern: args.filePattern,
486
- excludePattern: args.excludePattern,
511
+ Query: args.Query,
512
+ SearchPath: args.SearchPath,
513
+ Includes: args.Includes,
514
+ CaseInsensitive: args.CaseInsensitive,
515
+ IsRegex: args.IsRegex,
487
516
  };
488
517
  try {
489
518
  const result = await tool.execute(params, context.cwd);