centaurus-cli 2.7.0 → 2.7.2
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/dist/cli-adapter.d.ts +9 -0
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +352 -119
- package/dist/cli-adapter.js.map +1 -1
- package/dist/services/ai-service-client.d.ts.map +1 -1
- package/dist/services/ai-service-client.js +3 -0
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/services/environment-context-injector.d.ts +2 -2
- package/dist/services/environment-context-injector.d.ts.map +1 -1
- package/dist/services/environment-context-injector.js +4 -4
- package/dist/services/environment-context-injector.js.map +1 -1
- package/dist/tools/command.d.ts +1 -2
- package/dist/tools/command.d.ts.map +1 -1
- package/dist/tools/command.js +39 -131
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/file-ops.d.ts +3 -3
- package/dist/tools/file-ops.d.ts.map +1 -1
- package/dist/tools/file-ops.js +120 -92
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/tools/get-diff.d.ts +5 -0
- package/dist/tools/get-diff.d.ts.map +1 -1
- package/dist/tools/get-diff.js +67 -5
- package/dist/tools/get-diff.js.map +1 -1
- package/dist/tools/grep-search.d.ts +13 -21
- package/dist/tools/grep-search.d.ts.map +1 -1
- package/dist/tools/grep-search.js +309 -280
- package/dist/tools/grep-search.js.map +1 -1
- package/dist/tools/inspect-symbol.d.ts +5 -0
- package/dist/tools/inspect-symbol.d.ts.map +1 -1
- package/dist/tools/inspect-symbol.js +102 -20
- package/dist/tools/inspect-symbol.js.map +1 -1
- package/dist/tools/types.d.ts +2 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/validation.d.ts +8 -10
- package/dist/tools/validation.d.ts.map +1 -1
- package/dist/tools/validation.js +35 -37
- package/dist/tools/validation.js.map +1 -1
- package/dist/ui/components/App.d.ts +1 -0
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +24 -17
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/AuthWelcomeScreen.d.ts.map +1 -1
- package/dist/ui/components/AuthWelcomeScreen.js +1 -9
- package/dist/ui/components/AuthWelcomeScreen.js.map +1 -1
- package/dist/ui/components/CodeBlock.d.ts.map +1 -1
- package/dist/ui/components/CodeBlock.js +24 -13
- package/dist/ui/components/CodeBlock.js.map +1 -1
- package/dist/ui/components/DiffViewer.d.ts +1 -0
- package/dist/ui/components/DiffViewer.d.ts.map +1 -1
- package/dist/ui/components/DiffViewer.js +107 -70
- package/dist/ui/components/DiffViewer.js.map +1 -1
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +4 -15
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +118 -23
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/utils/input-classifier.d.ts.map +1 -1
- package/dist/utils/input-classifier.js +2 -1
- package/dist/utils/input-classifier.js.map +1 -1
- package/package.json +1 -1
- package/prompts/system-prompt-autonomous.md +22 -73
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
84
|
-
if (params.
|
|
85
|
-
args.push('--
|
|
76
|
+
// Fixed string vs Regex
|
|
77
|
+
if (!params.IsRegex) {
|
|
78
|
+
args.push('--fixed-strings');
|
|
86
79
|
}
|
|
87
|
-
//
|
|
88
|
-
if (params.
|
|
89
|
-
|
|
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
|
-
|
|
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
|
|
95
|
-
return this.parseRipgrepOutput(stdout, params.
|
|
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
|
|
100
|
-
return
|
|
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
|
-
//
|
|
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
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
//
|
|
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 = ['
|
|
198
|
+
const args = ['-rn'];
|
|
189
199
|
// Case sensitivity
|
|
190
|
-
if (
|
|
200
|
+
if (params.CaseInsensitive) {
|
|
191
201
|
args.push('-i');
|
|
192
202
|
}
|
|
193
203
|
// Context lines
|
|
194
204
|
args.push(`--context=${GrepSearchTool.CONTEXT_LINES}`);
|
|
195
|
-
//
|
|
196
|
-
if (params.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
+
// Pattern
|
|
216
|
+
args.push(params.Query);
|
|
217
|
+
// Path
|
|
218
|
+
args.push(params.SearchPath);
|
|
234
219
|
try {
|
|
235
|
-
const { stdout } = await
|
|
236
|
-
return this.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
264
|
+
pushMatch();
|
|
266
265
|
continue;
|
|
267
266
|
}
|
|
268
|
-
//
|
|
267
|
+
// Match: file:line:content
|
|
269
268
|
const matchRegex = /^([^:]+):(\d+):(.*)$/;
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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:
|
|
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
|
-
*
|
|
301
|
+
* Execute search using findstr (Windows)
|
|
302
|
+
* NOTE: Context is DISABLED for findstr to avoid performance/encoding issues.
|
|
326
303
|
*/
|
|
327
|
-
async
|
|
328
|
-
const
|
|
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 (
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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:
|
|
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
|
|
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}"
|
|
418
|
+
return `No matches found for pattern "${result.searchPattern}"`;
|
|
409
419
|
}
|
|
410
|
-
const
|
|
411
|
-
|
|
420
|
+
const output = [];
|
|
421
|
+
output.push(`Found ${result.totalMatches} match${result.totalMatches === 1 ? '' : 'es'} for pattern "${result.searchPattern}"`);
|
|
412
422
|
if (result.truncated) {
|
|
413
|
-
|
|
423
|
+
output.push(`(showing first ${result.matches.length} matches)`);
|
|
414
424
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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: `
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
479
|
+
reason_text: {
|
|
461
480
|
type: 'string',
|
|
462
|
-
description: '
|
|
481
|
+
description: 'REQUIRED: A brief explanation of what you are searching for and why.',
|
|
463
482
|
},
|
|
464
|
-
|
|
465
|
-
type: 'boolean',
|
|
466
|
-
description: 'Whether search should be case-sensitive. Default: false',
|
|
467
|
-
},
|
|
468
|
-
filePattern: {
|
|
483
|
+
Query: {
|
|
469
484
|
type: 'string',
|
|
470
|
-
description: '
|
|
485
|
+
description: 'The search term or pattern to look for within files.',
|
|
471
486
|
},
|
|
472
|
-
|
|
487
|
+
SearchPath: {
|
|
473
488
|
type: 'string',
|
|
474
|
-
description: '
|
|
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: ['
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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);
|