@yeaft/webchat-agent 1.0.299 → 1.0.301
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/context.js +1 -0
- package/index.js +9 -0
- package/local-runtime/server/database.js +1 -0
- package/local-runtime/server/db/connection.js +86 -0
- package/local-runtime/server/db/yeaft-project-db.js +225 -0
- package/local-runtime/server/handlers/agent-output.js +41 -5
- package/local-runtime/server/handlers/client-conversation.js +73 -0
- package/local-runtime/server/ws-utils.js +5 -1
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +73 -78
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/cli-session-runner.js +3 -2
- package/yeaft/cli.js +20 -1
- package/yeaft/conversation/search.js +13 -7
- package/yeaft/engine.js +29 -5
- package/yeaft/managed-cli.js +618 -0
- package/yeaft/session.js +7 -0
- package/yeaft/sub-agent/runner.js +6 -0
- package/yeaft/tools/disk-usage.js +243 -0
- package/yeaft/tools/glob.js +84 -28
- package/yeaft/tools/grep.js +616 -152
- package/yeaft/tools/history-search.js +12 -3
- package/yeaft/tools/index.js +2 -0
- package/yeaft/tools/process-runner.js +211 -0
- package/yeaft/tools/search-paths.js +108 -0
- package/yeaft/tools/types.js +2 -0
- package/yeaft/web-bridge.js +93 -6
package/yeaft/tools/grep.js
CHANGED
|
@@ -7,10 +7,20 @@
|
|
|
7
7
|
|
|
8
8
|
import { defineTool } from './types.js';
|
|
9
9
|
import { spawn } from 'child_process';
|
|
10
|
-
import { readdir, readFile
|
|
10
|
+
import { lstat, readdir, readFile } from 'fs/promises';
|
|
11
11
|
import { StringDecoder } from 'string_decoder';
|
|
12
12
|
import { existsSync } from 'fs';
|
|
13
|
-
import { resolve, join, relative, extname } from 'path';
|
|
13
|
+
import { basename, dirname, resolve, join, relative, extname } from 'path';
|
|
14
|
+
import { managedCliToolReady, resolveManagedCliCommand } from '../managed-cli.js';
|
|
15
|
+
import { runProcess } from './process-runner.js';
|
|
16
|
+
import {
|
|
17
|
+
createSearchPathMatcher,
|
|
18
|
+
SEARCH_SKIP_GLOBS,
|
|
19
|
+
isAbortError,
|
|
20
|
+
isSkippedSearchDirectory,
|
|
21
|
+
throwIfAborted,
|
|
22
|
+
waitForAbortable,
|
|
23
|
+
} from './search-paths.js';
|
|
14
24
|
|
|
15
25
|
/** Max output lines. */
|
|
16
26
|
const MAX_LINES = 250;
|
|
@@ -23,9 +33,9 @@ const MAX_CAPTURE_BYTES = MAX_OUTPUT_BYTES - Buffer.byteLength(OUTPUT_TRUNCATED_
|
|
|
23
33
|
|
|
24
34
|
/** Keep one pathological source line from consuming the whole output budget. */
|
|
25
35
|
const MAX_LINE_BYTES = 16 * 1024;
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
36
|
+
const MAX_TEXT_FILE_BYTES = 16 * 1024 * 1024;
|
|
37
|
+
const MAX_CANDIDATE_BYTES = 16 * 1024 * 1024;
|
|
38
|
+
const FALLBACK_CONCURRENCY = 4;
|
|
29
39
|
|
|
30
40
|
/** Binary extensions to skip. */
|
|
31
41
|
const BINARY_EXTS = new Set([
|
|
@@ -59,6 +69,169 @@ function boundToolOutput(text) {
|
|
|
59
69
|
return truncateUtf8(text, MAX_CAPTURE_BYTES) + OUTPUT_TRUNCATED_MARKER;
|
|
60
70
|
}
|
|
61
71
|
|
|
72
|
+
function normalizeRipgrepPath(searchPath, path) {
|
|
73
|
+
const normalized = String(path || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
74
|
+
const normalizedSearchPath = String(searchPath || '').replace(/\\/g, '/');
|
|
75
|
+
const isAbsolute = normalized.startsWith('/')
|
|
76
|
+
|| (/^[A-Za-z]:\//.test(normalized) && /^[A-Za-z]:\//.test(normalizedSearchPath));
|
|
77
|
+
if (!isAbsolute) return normalized;
|
|
78
|
+
return relative(searchPath, normalized).replace(/\\/g, '/');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createGrepRecord(path, suffix = '', kind = 'match', metadata = {}) {
|
|
82
|
+
return { path, suffix, kind, ...metadata };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createRipgrepEnv() {
|
|
86
|
+
const env = { ...process.env };
|
|
87
|
+
delete env.RIPGREP_CONFIG_PATH;
|
|
88
|
+
return env;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderGrepRecord(record, options) {
|
|
92
|
+
if (options.filesOnly) return record.path;
|
|
93
|
+
const separatorAfterPath = record.kind === 'context' ? '-' : ':';
|
|
94
|
+
return record.path + separatorAfterPath + record.suffix;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function prepareOutputValue(value) {
|
|
98
|
+
const normalized = String(value).replace(/\r/g, '');
|
|
99
|
+
const text = truncateUtf8(normalized, MAX_LINE_BYTES);
|
|
100
|
+
return {
|
|
101
|
+
text,
|
|
102
|
+
truncated: Buffer.byteLength(text, 'utf8') < Buffer.byteLength(normalized, 'utf8'),
|
|
103
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function measureOutputValues(values) {
|
|
108
|
+
return values.reduce((total, value, index) => total + value.bytes + (index > 0 ? 1 : 0), 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderOutputValues(values, truncated) {
|
|
112
|
+
if (!truncated) return values.map(value => value.text).join('\n');
|
|
113
|
+
const markerBytes = Buffer.byteLength(OUTPUT_TRUNCATED_MARKER, 'utf8');
|
|
114
|
+
let remaining = Math.max(0, SEARCH_RESULT_BYTES - markerBytes);
|
|
115
|
+
const parts = [];
|
|
116
|
+
for (const value of values) {
|
|
117
|
+
const separator = parts.length > 0 ? '\n' : '';
|
|
118
|
+
const separatorBytes = Buffer.byteLength(separator, 'utf8');
|
|
119
|
+
if (remaining <= separatorBytes) break;
|
|
120
|
+
const text = truncateUtf8(value.text, remaining - separatorBytes);
|
|
121
|
+
if (!text && value.text) break;
|
|
122
|
+
parts.push(separator + text);
|
|
123
|
+
remaining -= separatorBytes + Buffer.byteLength(text, 'utf8');
|
|
124
|
+
if (text !== value.text) break;
|
|
125
|
+
}
|
|
126
|
+
return parts.join('') + truncateUtf8(OUTPUT_TRUNCATED_MARKER, SEARCH_RESULT_BYTES);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderSelectedRecords(records, visibleMatches, options, moreResults) {
|
|
130
|
+
const visibleMatchRecords = new Set(visibleMatches);
|
|
131
|
+
const visibleMatchKeys = new Set(visibleMatches.map(record => record.matchKey).filter(Boolean));
|
|
132
|
+
const eligible = records.filter(record => (
|
|
133
|
+
record.kind !== 'context'
|
|
134
|
+
? visibleMatchRecords.has(record)
|
|
135
|
+
: record.matchKeys?.some(matchKey => visibleMatchKeys.has(matchKey))
|
|
136
|
+
));
|
|
137
|
+
const prepared = new Map(eligible.map(record => [
|
|
138
|
+
record,
|
|
139
|
+
prepareOutputValue(renderGrepRecord(record, options)),
|
|
140
|
+
]));
|
|
141
|
+
const notice = moreResults ? prepareOutputValue('\n... (more results omitted)') : null;
|
|
142
|
+
const completeValues = eligible.map(record => prepared.get(record));
|
|
143
|
+
if (notice) completeValues.push(notice);
|
|
144
|
+
if (completeValues.every(value => !value.truncated)
|
|
145
|
+
&& measureOutputValues(completeValues) <= SEARCH_RESULT_BYTES) {
|
|
146
|
+
return renderOutputValues(completeValues, false);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const markerBytes = Buffer.byteLength(OUTPUT_TRUNCATED_MARKER, 'utf8');
|
|
150
|
+
const noticeBytes = notice ? notice.bytes + 1 : 0;
|
|
151
|
+
const selectedMatches = new Set(visibleMatches);
|
|
152
|
+
const selectedMatchKeys = new Set(
|
|
153
|
+
visibleMatches.map(record => record.matchKey).filter(Boolean),
|
|
154
|
+
);
|
|
155
|
+
const matchValues = visibleMatches.map(record => prepared.get(record));
|
|
156
|
+
const selectedContexts = new Set();
|
|
157
|
+
let usedBytes = measureOutputValues(matchValues) + markerBytes + noticeBytes;
|
|
158
|
+
if (usedBytes <= SEARCH_RESULT_BYTES) {
|
|
159
|
+
for (const record of eligible) {
|
|
160
|
+
if (record.kind !== 'context'
|
|
161
|
+
|| !record.matchKeys?.some(matchKey => selectedMatchKeys.has(matchKey))) continue;
|
|
162
|
+
const value = prepared.get(record);
|
|
163
|
+
if (value.truncated || usedBytes + value.bytes + 1 > SEARCH_RESULT_BYTES) continue;
|
|
164
|
+
selectedContexts.add(record);
|
|
165
|
+
usedBytes += value.bytes + 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const selectedValues = eligible
|
|
170
|
+
.filter(record => selectedMatches.has(record) || selectedContexts.has(record))
|
|
171
|
+
.map(record => prepared.get(record));
|
|
172
|
+
if (notice) selectedValues.push(notice);
|
|
173
|
+
return renderOutputValues(selectedValues, true);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function compareSearchPaths(left, right) {
|
|
177
|
+
return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function compareGrepRecords(left, right) {
|
|
181
|
+
if (left.path !== right.path) return compareSearchPaths(left.path, right.path);
|
|
182
|
+
const leftLine = Number.parseInt(left.suffix, 10);
|
|
183
|
+
const rightLine = Number.parseInt(right.suffix, 10);
|
|
184
|
+
if (Number.isFinite(leftLine) && Number.isFinite(rightLine) && leftLine !== rightLine) {
|
|
185
|
+
return leftLine - rightLine;
|
|
186
|
+
}
|
|
187
|
+
if (left.suffix === right.suffix) return 0;
|
|
188
|
+
return left.suffix < right.suffix ? -1 : 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function createNodeRegexPlan(pattern, options) {
|
|
192
|
+
let source = options.fixedStrings
|
|
193
|
+
? pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
194
|
+
: pattern;
|
|
195
|
+
const flags = new Set(['g']);
|
|
196
|
+
if (options.caseInsensitive) flags.add('i');
|
|
197
|
+
if (options.multiline) {
|
|
198
|
+
flags.add('m');
|
|
199
|
+
flags.add('s');
|
|
200
|
+
}
|
|
201
|
+
if (!options.fixedStrings) {
|
|
202
|
+
while (true) {
|
|
203
|
+
const inlineFlags = source.match(/^\(\?([ims]*)(?:-([ims]*))?\)/);
|
|
204
|
+
if (!inlineFlags || (!inlineFlags[1] && !inlineFlags[2])) break;
|
|
205
|
+
for (const flag of inlineFlags[1]) flags.add(flag);
|
|
206
|
+
for (const flag of inlineFlags[2] || '') flags.delete(flag);
|
|
207
|
+
source = source.slice(inlineFlags[0].length);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const flagString = [...flags].join('');
|
|
211
|
+
new RegExp(source, flagString);
|
|
212
|
+
return { source, flags: flagString };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function canUseRipgrep(pattern, options) {
|
|
216
|
+
const utf8RoundTrip = Buffer.from(pattern, 'utf8').toString('utf8') === pattern;
|
|
217
|
+
if (options.caseInsensitive || options.multiline || !utf8RoundTrip) return false;
|
|
218
|
+
if (/[\0\r\n\u2028\u2029]/.test(pattern)) return false;
|
|
219
|
+
if (options.fixedStrings) return true;
|
|
220
|
+
|
|
221
|
+
// JavaScript RegExp is the public contract. Rust regex only finds candidate
|
|
222
|
+
// files for plain printable ASCII text; Node.js validates every result.
|
|
223
|
+
return /^[\x20-\x7e]+$/.test(pattern)
|
|
224
|
+
&& !/[\\^$.*+?()[\]{}|]/.test(pattern);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function prepareGrepPattern(pattern, options) {
|
|
228
|
+
const regexPlan = createNodeRegexPlan(pattern, options);
|
|
229
|
+
return {
|
|
230
|
+
...regexPlan,
|
|
231
|
+
useRipgrep: canUseRipgrep(pattern, options),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
62
235
|
function formatGrepError(message) {
|
|
63
236
|
const errorMessage = `Grep failed: ${message}`;
|
|
64
237
|
const serialized = JSON.stringify({ error: errorMessage });
|
|
@@ -85,81 +258,122 @@ function formatGrepError(message) {
|
|
|
85
258
|
return result;
|
|
86
259
|
}
|
|
87
260
|
|
|
88
|
-
function createOutputCollector(maxBytes = MAX_OUTPUT_BYTES) {
|
|
261
|
+
export function createOutputCollector(maxBytes = MAX_OUTPUT_BYTES) {
|
|
89
262
|
const parts = [];
|
|
90
|
-
const
|
|
263
|
+
const marker = truncateUtf8(OUTPUT_TRUNCATED_MARKER, maxBytes);
|
|
264
|
+
const markerBytes = Buffer.byteLength(marker, 'utf8');
|
|
91
265
|
let bytes = 0;
|
|
92
266
|
let truncated = false;
|
|
267
|
+
|
|
268
|
+
function truncateExistingParts(contentBytes) {
|
|
269
|
+
let remaining = contentBytes;
|
|
270
|
+
const bounded = [];
|
|
271
|
+
for (const part of parts) {
|
|
272
|
+
if (remaining <= 0) break;
|
|
273
|
+
const value = truncateUtf8(part, remaining);
|
|
274
|
+
if (value) bounded.push(value);
|
|
275
|
+
remaining -= Buffer.byteLength(value, 'utf8');
|
|
276
|
+
}
|
|
277
|
+
parts.length = 0;
|
|
278
|
+
parts.push(...bounded);
|
|
279
|
+
bytes = contentBytes - remaining;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function markTruncated(part = '') {
|
|
283
|
+
if (truncated) return;
|
|
284
|
+
truncated = true;
|
|
285
|
+
const contentBytes = Math.max(0, maxBytes - markerBytes);
|
|
286
|
+
truncateExistingParts(contentBytes);
|
|
287
|
+
const remaining = contentBytes - bytes;
|
|
288
|
+
if (remaining > 0 && part) {
|
|
289
|
+
const bounded = truncateUtf8(part, remaining);
|
|
290
|
+
if (bounded) {
|
|
291
|
+
parts.push(bounded);
|
|
292
|
+
bytes += Buffer.byteLength(bounded, 'utf8');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
93
297
|
return {
|
|
94
298
|
add(value) {
|
|
95
299
|
if (truncated) return false;
|
|
96
300
|
const normalized = String(value).replace(/\r/g, '');
|
|
97
301
|
const line = truncateUtf8(normalized, MAX_LINE_BYTES);
|
|
98
302
|
const lineWasTruncated = Buffer.byteLength(normalized, 'utf8') > Buffer.byteLength(line, 'utf8');
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
303
|
+
const part = (parts.length > 0 ? '\n' : '') + line;
|
|
304
|
+
const partBytes = Buffer.byteLength(part, 'utf8');
|
|
305
|
+
if (lineWasTruncated || bytes + partBytes > maxBytes) {
|
|
306
|
+
markTruncated(part);
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
parts.push(part);
|
|
310
|
+
bytes += partBytes;
|
|
311
|
+
return true;
|
|
107
312
|
},
|
|
108
|
-
|
|
313
|
+
get truncated() { return truncated; },
|
|
314
|
+
toString() { return parts.join('') + (truncated ? marker : ''); },
|
|
109
315
|
};
|
|
110
316
|
}
|
|
111
317
|
|
|
112
318
|
/**
|
|
113
319
|
* Check if ripgrep is available.
|
|
114
320
|
*/
|
|
115
|
-
export function setRipgrepAvailabilityForTests(value) {
|
|
116
|
-
ripgrepAvailability = value;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function hasRipgrep() {
|
|
120
|
-
if (typeof ripgrepAvailability === 'boolean') return Promise.resolve(ripgrepAvailability);
|
|
121
|
-
if (ripgrepAvailability) return ripgrepAvailability;
|
|
122
|
-
ripgrepAvailability = new Promise((resolve) => {
|
|
123
|
-
const proc = spawn('rg', ['--version'], { stdio: 'pipe', windowsHide: true });
|
|
124
|
-
proc.on('close', (code) => resolve(code === 0));
|
|
125
|
-
proc.on('error', () => resolve(false));
|
|
126
|
-
}).then((available) => {
|
|
127
|
-
ripgrepAvailability = available;
|
|
128
|
-
return available;
|
|
129
|
-
});
|
|
130
|
-
return ripgrepAvailability;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
321
|
/**
|
|
134
322
|
* Run ripgrep and return results.
|
|
135
323
|
*/
|
|
136
|
-
export function runRipgrep(pattern, searchPath, options, spawnProcess = spawn) {
|
|
324
|
+
export function runRipgrep(pattern, searchPath, options, spawnProcess = spawn, command = 'rg') {
|
|
325
|
+
throwIfAborted(options.signal);
|
|
137
326
|
return new Promise((resolve, reject) => {
|
|
138
|
-
const
|
|
327
|
+
const relativeTarget = options.cwd ? relative(options.cwd, searchPath) : null;
|
|
328
|
+
const searchTarget = relativeTarget === '' ? null : (relativeTarget ?? searchPath);
|
|
329
|
+
const args = [
|
|
330
|
+
'--no-config',
|
|
331
|
+
'--no-heading',
|
|
332
|
+
'--line-number',
|
|
333
|
+
'--color', 'never',
|
|
334
|
+
'--hidden',
|
|
335
|
+
'--no-ignore',
|
|
336
|
+
'--null',
|
|
337
|
+
];
|
|
139
338
|
if (options.caseInsensitive) args.push('-i');
|
|
140
339
|
if (options.fixedStrings) args.push('-F');
|
|
141
|
-
|
|
142
|
-
|
|
340
|
+
// User glob and type semantics are defined by the shared matcher below.
|
|
341
|
+
// Do not pass either positive filter to rg: rg's glob dialect and type
|
|
342
|
+
// registry are broader, so either filter could discard a fallback match.
|
|
343
|
+
for (const skipGlob of SEARCH_SKIP_GLOBS) args.push('--glob', skipGlob);
|
|
143
344
|
if (options.filesOnly) args.push('-l');
|
|
144
345
|
if (options.count) args.push('-c');
|
|
145
346
|
if (options.context) args.push('-C', String(options.context));
|
|
146
347
|
if (options.before) args.push('-B', String(options.before));
|
|
147
348
|
if (options.after) args.push('-A', String(options.after));
|
|
349
|
+
if (options.context || options.before || options.after) args.push('--no-context-separator');
|
|
148
350
|
if (options.multiline) args.push('-U', '--multiline-dotall');
|
|
149
|
-
|
|
150
|
-
|
|
351
|
+
args.push('--sort', 'path');
|
|
352
|
+
args.push('--', pattern);
|
|
353
|
+
if (searchTarget) args.push(searchTarget);
|
|
354
|
+
|
|
355
|
+
const proc = spawnProcess(command, args, {
|
|
356
|
+
cwd: options.cwd,
|
|
357
|
+
env: createRipgrepEnv(),
|
|
358
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
359
|
+
windowsHide: true,
|
|
360
|
+
});
|
|
151
361
|
const requestedBudget = Number(options.byteBudget);
|
|
152
362
|
const stdoutBudget = Number.isFinite(requestedBudget) && requestedBudget >= 0
|
|
153
363
|
? Math.min(requestedBudget, MAX_OUTPUT_BYTES)
|
|
154
364
|
: MAX_OUTPUT_BYTES;
|
|
155
|
-
const
|
|
156
|
-
const
|
|
365
|
+
const stdout = createOutputCollector(stdoutBudget);
|
|
366
|
+
const records = [];
|
|
367
|
+
const matchesPath = createSearchPathMatcher(options);
|
|
368
|
+
const stdoutDecoder = new StringDecoder('utf8');
|
|
157
369
|
const stderrChunks = [];
|
|
158
|
-
let
|
|
370
|
+
let pendingStdout = '';
|
|
159
371
|
let stderrBytes = 0;
|
|
160
372
|
let stdoutTruncated = false;
|
|
161
373
|
let stderrTruncated = false;
|
|
162
|
-
let
|
|
374
|
+
let resultCount = 0;
|
|
375
|
+
let pendingPath = null;
|
|
376
|
+
let discardSuffix = false;
|
|
163
377
|
let stoppedForLimit = false;
|
|
164
378
|
let stopRequested = false;
|
|
165
379
|
let settled = false;
|
|
@@ -170,32 +384,75 @@ export function runRipgrep(pattern, searchPath, options, spawnProcess = spawn) {
|
|
|
170
384
|
try { proc.kill(); } catch {}
|
|
171
385
|
}
|
|
172
386
|
|
|
173
|
-
function
|
|
174
|
-
if (stdoutTruncated ||
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
387
|
+
function captureRecord(record) {
|
|
388
|
+
if (stoppedForLimit || stdoutTruncated || !matchesPath(record.path)) return;
|
|
389
|
+
resultCount += 1;
|
|
390
|
+
records.push(record);
|
|
391
|
+
if (!stdout.add(renderGrepRecord(record, options))) stdoutTruncated = true;
|
|
392
|
+
if (resultCount >= Math.max(1, options.maxResults || 500)) stoppedForLimit = true;
|
|
393
|
+
if (stdoutTruncated || stoppedForLimit) stop();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function captureSuffix(suffix) {
|
|
397
|
+
if (pendingPath == null) return;
|
|
398
|
+
let kind = 'match';
|
|
399
|
+
if (!options.count && suffix.match(/^\d+-/)) kind = 'context';
|
|
400
|
+
captureRecord(createGrepRecord(pendingPath, suffix, kind));
|
|
401
|
+
pendingPath = null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function drainStdout(final = false) {
|
|
405
|
+
while (!stdoutTruncated && !stoppedForLimit) {
|
|
406
|
+
if (discardSuffix) {
|
|
407
|
+
const boundary = pendingStdout.indexOf('\n');
|
|
408
|
+
if (boundary < 0) break;
|
|
409
|
+
pendingStdout = pendingStdout.slice(boundary + 1);
|
|
410
|
+
pendingPath = null;
|
|
411
|
+
discardSuffix = false;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (pendingPath == null) {
|
|
415
|
+
const boundary = pendingStdout.indexOf('\0');
|
|
416
|
+
if (boundary < 0) break;
|
|
417
|
+
pendingPath = normalizeRipgrepPath(searchPath, pendingStdout.slice(0, boundary));
|
|
418
|
+
pendingStdout = pendingStdout.slice(boundary + 1);
|
|
419
|
+
if (options.filesOnly) {
|
|
420
|
+
captureRecord(createGrepRecord(pendingPath));
|
|
421
|
+
pendingPath = null;
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const boundary = pendingStdout.indexOf('\n');
|
|
426
|
+
if (boundary < 0) break;
|
|
427
|
+
const suffix = pendingStdout.slice(0, boundary).replace(/\r$/, '');
|
|
428
|
+
pendingStdout = pendingStdout.slice(boundary + 1);
|
|
429
|
+
captureSuffix(suffix);
|
|
183
430
|
}
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
431
|
+
if (final && !stdoutTruncated && !stoppedForLimit) {
|
|
432
|
+
if (pendingPath != null && pendingStdout && !discardSuffix) {
|
|
433
|
+
captureSuffix(pendingStdout.replace(/\r$/, ''));
|
|
434
|
+
}
|
|
435
|
+
pendingStdout = '';
|
|
436
|
+
pendingPath = null;
|
|
437
|
+
discardSuffix = false;
|
|
187
438
|
}
|
|
439
|
+
}
|
|
188
440
|
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
441
|
+
function captureStdout(chunk) {
|
|
442
|
+
if (stdoutTruncated || stoppedForLimit) return;
|
|
443
|
+
pendingStdout += stdoutDecoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
444
|
+
drainStdout();
|
|
445
|
+
if (pendingPath != null && !discardSuffix
|
|
446
|
+
&& Buffer.byteLength(pendingStdout, 'utf8') > MAX_LINE_BYTES) {
|
|
447
|
+
if (matchesPath(pendingPath)) {
|
|
448
|
+
captureSuffix(truncateUtf8(pendingStdout, MAX_LINE_BYTES + 1));
|
|
449
|
+
pendingStdout = '';
|
|
450
|
+
} else {
|
|
451
|
+
discardSuffix = true;
|
|
452
|
+
drainStdout();
|
|
453
|
+
if (discardSuffix) pendingStdout = '';
|
|
454
|
+
}
|
|
197
455
|
}
|
|
198
|
-
if (stdoutTruncated || stoppedForLimit) stop();
|
|
199
456
|
}
|
|
200
457
|
|
|
201
458
|
function captureStderr(chunk) {
|
|
@@ -220,117 +477,296 @@ export function runRipgrep(pattern, searchPath, options, spawnProcess = spawn) {
|
|
|
220
477
|
return truncateUtf8(decoded, maxTextBytes) + boundedMarker;
|
|
221
478
|
}
|
|
222
479
|
|
|
480
|
+
const cleanup = () => options.signal?.removeEventListener('abort', onAbort);
|
|
481
|
+
const onAbort = () => {
|
|
482
|
+
if (settled) return;
|
|
483
|
+
settled = true;
|
|
484
|
+
cleanup();
|
|
485
|
+
let abort;
|
|
486
|
+
try { throwIfAborted(options.signal); } catch (error) { abort = error; }
|
|
487
|
+
stop();
|
|
488
|
+
reject(abort);
|
|
489
|
+
};
|
|
490
|
+
|
|
223
491
|
proc.stdout.on('data', captureStdout);
|
|
224
492
|
proc.stderr.on('data', captureStderr);
|
|
225
493
|
proc.on('close', (code) => {
|
|
226
494
|
if (settled) return;
|
|
227
495
|
settled = true;
|
|
228
|
-
|
|
496
|
+
cleanup();
|
|
497
|
+
if (!stdoutTruncated && !stoppedForLimit) {
|
|
498
|
+
pendingStdout += stdoutDecoder.end();
|
|
499
|
+
drainStdout(true);
|
|
500
|
+
}
|
|
501
|
+
const output = stdout.toString();
|
|
229
502
|
const stderr = decodeCaptured(stderrChunks, MAX_OUTPUT_BYTES, stderrTruncated);
|
|
230
|
-
if (code === 0 || code === 1 || stoppedForLimit || stdoutTruncated)
|
|
231
|
-
|
|
503
|
+
if (code === 0 || code === 1 || stoppedForLimit || stdoutTruncated) {
|
|
504
|
+
resolve(options.structured
|
|
505
|
+
? { output, records, resultCount, truncated: stdout.truncated }
|
|
506
|
+
: output);
|
|
507
|
+
} else reject(new Error(stderr || `rg exited with code ${code}`));
|
|
232
508
|
});
|
|
233
509
|
proc.on('error', (err) => {
|
|
234
510
|
if (settled || stopRequested) return;
|
|
235
511
|
settled = true;
|
|
512
|
+
cleanup();
|
|
236
513
|
reject(err);
|
|
237
514
|
});
|
|
515
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
516
|
+
if (options.signal?.aborted) onAbort();
|
|
238
517
|
});
|
|
239
518
|
}
|
|
240
519
|
|
|
520
|
+
async function listRipgrepCandidatePaths(command, searchPath, options) {
|
|
521
|
+
throwIfAborted(options.signal);
|
|
522
|
+
const searchStat = await lstat(searchPath);
|
|
523
|
+
const baseDir = searchStat.isDirectory() ? searchPath : dirname(searchPath);
|
|
524
|
+
const target = searchStat.isDirectory() ? '.' : basename(searchPath);
|
|
525
|
+
const args = [
|
|
526
|
+
'--no-config',
|
|
527
|
+
'--files-with-matches',
|
|
528
|
+
'--color', 'never',
|
|
529
|
+
'--hidden',
|
|
530
|
+
'--no-ignore',
|
|
531
|
+
'--null',
|
|
532
|
+
];
|
|
533
|
+
if (options.fixedStrings) args.push('-F');
|
|
534
|
+
for (const skipGlob of SEARCH_SKIP_GLOBS) args.push('--glob', skipGlob);
|
|
535
|
+
args.push('--sort', 'path', '--', options.pattern, target);
|
|
536
|
+
const result = await runProcess(command, args, {
|
|
537
|
+
cwd: baseDir,
|
|
538
|
+
env: createRipgrepEnv(),
|
|
539
|
+
signal: options.signal,
|
|
540
|
+
timeoutMs: 120_000,
|
|
541
|
+
maxBytes: MAX_CANDIDATE_BYTES,
|
|
542
|
+
preserveCarriageReturns: true,
|
|
543
|
+
});
|
|
544
|
+
if (result.timedOut) throw new Error('rg timed out');
|
|
545
|
+
if (result.truncated) throw new Error('rg candidate output exceeded the tool limit');
|
|
546
|
+
if (result.code !== 0 && result.code !== 1) {
|
|
547
|
+
throw new Error(result.stderr.trim() || `rg exited with code ${result.code}`);
|
|
548
|
+
}
|
|
549
|
+
return result.stdout
|
|
550
|
+
.split('\0')
|
|
551
|
+
.filter(Boolean)
|
|
552
|
+
.map(path => resolve(baseDir, path));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function splitTextLines(content) {
|
|
556
|
+
const lines = [];
|
|
557
|
+
let start = 0;
|
|
558
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
559
|
+
const character = content[index];
|
|
560
|
+
if (character !== '\n' && character !== '\r'
|
|
561
|
+
&& character !== '\u2028' && character !== '\u2029') continue;
|
|
562
|
+
const lineEnd = index;
|
|
563
|
+
if (character === '\r' && content[index + 1] === '\n') index += 1;
|
|
564
|
+
lines.push({ text: content.slice(start, lineEnd), start, end: lineEnd });
|
|
565
|
+
start = index + 1;
|
|
566
|
+
}
|
|
567
|
+
lines.push({ text: content.slice(start), start, end: content.length });
|
|
568
|
+
return lines;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function advanceEmptyMatch(regex, content) {
|
|
572
|
+
if (regex.lastIndex >= content.length) return false;
|
|
573
|
+
regex.lastIndex += regex.unicode && content.codePointAt(regex.lastIndex) > 0xffff ? 2 : 1;
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
241
577
|
/**
|
|
242
|
-
* Fallback:
|
|
578
|
+
* Fallback and final verifier: JavaScript RegExp defines matching semantics.
|
|
243
579
|
*/
|
|
244
580
|
export async function nodeGrep(pattern, searchPath, options) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const regex = new RegExp(regexSource, options.caseInsensitive ? 'gi' : 'g');
|
|
581
|
+
throwIfAborted(options.signal);
|
|
582
|
+
const regexPlan = options.regexPlan || createNodeRegexPlan(pattern, options);
|
|
583
|
+
const regex = new RegExp(regexPlan.source, regexPlan.flags);
|
|
249
584
|
const output = createOutputCollector(options.byteBudget || SEARCH_RESULT_BYTES);
|
|
585
|
+
const records = [];
|
|
250
586
|
const maxResults = Math.max(1, options.maxResults || 500);
|
|
587
|
+
const matchesPath = createSearchPathMatcher(options);
|
|
588
|
+
const rootStat = await lstat(searchPath);
|
|
589
|
+
throwIfAborted(options.signal);
|
|
590
|
+
const searchBase = rootStat.isDirectory() ? searchPath : dirname(searchPath);
|
|
251
591
|
let resultCount = 0;
|
|
252
592
|
let stopped = false;
|
|
253
593
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
.replace(/\*\*/g, '\0').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]')
|
|
258
|
-
.replace(/\0/g, '.*');
|
|
259
|
-
return new RegExp(`^${escaped}$`);
|
|
260
|
-
}
|
|
261
|
-
const globMatcher = options.glob ? compileGlob(options.glob) : null;
|
|
262
|
-
const typeExtensions = {
|
|
263
|
-
js: ['.js', '.jsx', '.mjs', '.cjs'], ts: ['.ts', '.tsx', '.mts', '.cts'],
|
|
264
|
-
py: ['.py'], rust: ['.rs'], go: ['.go'], java: ['.java'],
|
|
265
|
-
json: ['.json'], yaml: ['.yaml', '.yml'], markdown: ['.md', '.markdown'],
|
|
266
|
-
html: ['.html', '.htm'], css: ['.css'], shell: ['.sh', '.bash', '.zsh'],
|
|
267
|
-
};
|
|
594
|
+
const candidatePaths = options.candidatePaths
|
|
595
|
+
? new Set(options.candidatePaths.map(path => resolve(path)))
|
|
596
|
+
: null;
|
|
268
597
|
|
|
269
598
|
function matchesFilters(fullPath) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (!options.type) return true;
|
|
273
|
-
const extensions = typeExtensions[options.type];
|
|
274
|
-
return Boolean(extensions?.includes(extname(fullPath).toLowerCase()));
|
|
599
|
+
return (!candidatePaths || candidatePaths.has(resolve(fullPath)))
|
|
600
|
+
&& matchesPath(relative(searchBase, fullPath).replace(/\\/g, '/'));
|
|
275
601
|
}
|
|
276
602
|
|
|
277
|
-
function
|
|
278
|
-
|
|
279
|
-
|
|
603
|
+
function addFileRecords(fileRecords) {
|
|
604
|
+
if (stopped) return;
|
|
605
|
+
for (const record of fileRecords) {
|
|
606
|
+
records.push(record);
|
|
607
|
+
if (!options.structured && !output.add(renderGrepRecord(record, options))) {
|
|
608
|
+
stopped = true;
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (record.kind !== 'context') resultCount += 1;
|
|
612
|
+
}
|
|
613
|
+
if (resultCount >= maxResults) stopped = true;
|
|
280
614
|
}
|
|
281
615
|
|
|
282
|
-
async function
|
|
283
|
-
|
|
616
|
+
async function collectFileRecords(fullPath) {
|
|
617
|
+
throwIfAborted(options.signal);
|
|
618
|
+
if (!matchesFilters(fullPath) || BINARY_EXTS.has(extname(fullPath).toLowerCase())) return [];
|
|
284
619
|
try {
|
|
285
|
-
const fileStat = await
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
620
|
+
const fileStat = await lstat(fullPath);
|
|
621
|
+
throwIfAborted(options.signal);
|
|
622
|
+
if (!fileStat.isFile() || fileStat.size > MAX_TEXT_FILE_BYTES) return [];
|
|
623
|
+
const decoded = decodeTextFile(await readFile(fullPath));
|
|
624
|
+
throwIfAborted(options.signal);
|
|
625
|
+
if (decoded == null) return [];
|
|
626
|
+
const content = decoded;
|
|
627
|
+
const relPath = relative(searchBase, fullPath).replace(/\\/g, '/');
|
|
628
|
+
const lines = splitTextLines(content);
|
|
629
|
+
const matchedLines = new Set();
|
|
630
|
+
const textMatchedLines = new Set();
|
|
631
|
+
let matchCount = 0;
|
|
632
|
+
const lineAtOffset = (value, zeroWidth = false) => {
|
|
633
|
+
let low = 0;
|
|
634
|
+
let high = lines.length - 1;
|
|
635
|
+
while (low < high) {
|
|
636
|
+
const middle = Math.ceil((low + high) / 2);
|
|
637
|
+
if (lines[middle].start <= value) low = middle;
|
|
638
|
+
else high = middle - 1;
|
|
639
|
+
}
|
|
640
|
+
if (zeroWidth && value > lines[low].end && low + 1 < lines.length) return low + 1;
|
|
641
|
+
return low;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
if (options.multiline) {
|
|
645
|
+
regex.lastIndex = 0;
|
|
646
|
+
let match;
|
|
647
|
+
while ((match = regex.exec(content)) !== null) {
|
|
648
|
+
matchCount += 1;
|
|
649
|
+
const zeroWidth = match[0].length === 0;
|
|
650
|
+
const startLine = lineAtOffset(match.index, zeroWidth);
|
|
651
|
+
const endOffset = zeroWidth ? match.index : match.index + match[0].length - 1;
|
|
652
|
+
const endLine = lineAtOffset(endOffset, zeroWidth);
|
|
653
|
+
for (let line = startLine; line <= endLine; line += 1) {
|
|
654
|
+
matchedLines.add(line);
|
|
655
|
+
if (!zeroWidth) textMatchedLines.add(line);
|
|
656
|
+
}
|
|
657
|
+
if (zeroWidth && match.index >= lines[startLine].start) textMatchedLines.add(startLine);
|
|
658
|
+
if (match[0].length === 0 && !advanceEmptyMatch(regex, content)) break;
|
|
659
|
+
}
|
|
296
660
|
} else {
|
|
297
|
-
|
|
298
|
-
for (let i = 0; i < lines.length && !stopped; i += 1) {
|
|
661
|
+
for (let line = 0; line < lines.length; line += 1) {
|
|
299
662
|
regex.lastIndex = 0;
|
|
300
|
-
|
|
663
|
+
let match;
|
|
664
|
+
while ((match = regex.exec(lines[line].text)) !== null) {
|
|
665
|
+
matchCount += 1;
|
|
666
|
+
matchedLines.add(line);
|
|
667
|
+
textMatchedLines.add(line);
|
|
668
|
+
if (match[0].length === 0 && !advanceEmptyMatch(regex, lines[line].text)) break;
|
|
669
|
+
}
|
|
301
670
|
}
|
|
302
671
|
}
|
|
303
|
-
|
|
304
|
-
|
|
672
|
+
|
|
673
|
+
if (options.filesOnly) {
|
|
674
|
+
return matchCount > 0 ? [createGrepRecord(relPath)] : [];
|
|
675
|
+
}
|
|
676
|
+
if (options.count) {
|
|
677
|
+
return matchCount > 0
|
|
678
|
+
? [createGrepRecord(relPath, String(matchCount))]
|
|
679
|
+
: [];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const beforeLines = Math.max(0, Number(options.before ?? options.context ?? 0));
|
|
683
|
+
const afterLines = Math.max(0, Number(options.after ?? options.context ?? 0));
|
|
684
|
+
const selected = new Map();
|
|
685
|
+
const selectedMatches = [...matchedLines].sort((a, b) => a - b).slice(0, maxResults);
|
|
686
|
+
const selectedMatchSet = new Set(selectedMatches);
|
|
687
|
+
for (const matchIndex of selectedMatches) {
|
|
688
|
+
const matchKey = `${relPath}\0${matchIndex}`;
|
|
689
|
+
const start = Math.max(0, matchIndex - beforeLines);
|
|
690
|
+
const end = Math.min(lines.length - 1, matchIndex + afterLines);
|
|
691
|
+
for (let i = start; i <= end; i += 1) {
|
|
692
|
+
const kind = selectedMatchSet.has(i) ? 'match' : 'context';
|
|
693
|
+
const entry = selected.get(i) || { kind, matchKeys: new Set() };
|
|
694
|
+
if (kind === 'match') entry.kind = 'match';
|
|
695
|
+
entry.matchKeys.add(matchKey);
|
|
696
|
+
selected.set(i, entry);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return [...selected.entries()]
|
|
700
|
+
.sort((a, b) => a[0] - b[0])
|
|
701
|
+
.map(([lineIndex, entry]) => {
|
|
702
|
+
const lineSeparator = entry.kind === 'context' ? '-' : ':';
|
|
703
|
+
const lineText = entry.kind === 'match' && !textMatchedLines.has(lineIndex)
|
|
704
|
+
? ''
|
|
705
|
+
: lines[lineIndex].text;
|
|
706
|
+
return createGrepRecord(
|
|
707
|
+
relPath,
|
|
708
|
+
`${lineIndex + 1}${lineSeparator}${lineText}`,
|
|
709
|
+
entry.kind,
|
|
710
|
+
entry.kind === 'match'
|
|
711
|
+
? { matchKey: `${relPath}\0${lineIndex}` }
|
|
712
|
+
: { matchKeys: [...entry.matchKeys] },
|
|
713
|
+
);
|
|
714
|
+
});
|
|
715
|
+
} catch (error) {
|
|
716
|
+
if (isAbortError(error)) throw error;
|
|
717
|
+
return [];
|
|
305
718
|
}
|
|
306
719
|
}
|
|
307
720
|
|
|
308
721
|
async function searchDir(dir) {
|
|
722
|
+
throwIfAborted(options.signal);
|
|
309
723
|
if (stopped) return;
|
|
310
724
|
let entries;
|
|
311
|
-
try { entries = await readdir(dir, { withFileTypes: true }); } catch {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
for (const entry of entries) {
|
|
315
|
-
const fullPath = join(dir, entry.name);
|
|
316
|
-
if (entry.isDirectory()) {
|
|
317
|
-
const relPath = relative(searchPath, fullPath).replace(/\\/g, '/');
|
|
318
|
-
if (!SKIP_DIRS.has(entry.name) && relPath !== '.yeaft/worktrees' && !relPath.startsWith('.yeaft/worktrees/')) directories.push(fullPath);
|
|
319
|
-
} else files.push(fullPath);
|
|
725
|
+
try { entries = await readdir(dir, { withFileTypes: true }); } catch (error) {
|
|
726
|
+
if (isAbortError(error)) throw error;
|
|
727
|
+
return;
|
|
320
728
|
}
|
|
321
|
-
|
|
322
|
-
|
|
729
|
+
throwIfAborted(options.signal);
|
|
730
|
+
entries.sort((left, right) => compareSearchPaths(left.name, right.name));
|
|
731
|
+
let pendingFiles = [];
|
|
732
|
+
|
|
733
|
+
async function flushFiles() {
|
|
734
|
+
for (let i = 0; i < pendingFiles.length && !stopped; i += FALLBACK_CONCURRENCY) {
|
|
735
|
+
throwIfAborted(options.signal);
|
|
736
|
+
const batches = await Promise.all(
|
|
737
|
+
pendingFiles.slice(i, i + FALLBACK_CONCURRENCY).map(collectFileRecords),
|
|
738
|
+
);
|
|
739
|
+
for (const batch of batches) {
|
|
740
|
+
addFileRecords(batch);
|
|
741
|
+
if (stopped) break;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
pendingFiles = [];
|
|
323
745
|
}
|
|
324
|
-
|
|
746
|
+
|
|
747
|
+
for (const entry of entries) {
|
|
748
|
+
throwIfAborted(options.signal);
|
|
325
749
|
if (stopped) break;
|
|
326
|
-
|
|
750
|
+
const fullPath = join(dir, entry.name);
|
|
751
|
+
if (!entry.isDirectory()) {
|
|
752
|
+
pendingFiles.push(fullPath);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
const relPath = relative(searchPath, fullPath).replace(/\\/g, '/');
|
|
756
|
+
if (isSkippedSearchDirectory(relPath, entry.name)) continue;
|
|
757
|
+
await flushFiles();
|
|
758
|
+
if (!stopped) await searchDir(fullPath);
|
|
327
759
|
}
|
|
760
|
+
await flushFiles();
|
|
328
761
|
}
|
|
329
762
|
|
|
330
|
-
|
|
763
|
+
throwIfAborted(options.signal);
|
|
331
764
|
if (rootStat.isDirectory()) await searchDir(searchPath);
|
|
332
|
-
else await
|
|
333
|
-
|
|
765
|
+
else addFileRecords(await collectFileRecords(searchPath));
|
|
766
|
+
const result = output.toString();
|
|
767
|
+
return options.structured
|
|
768
|
+
? { output: result, records, resultCount, truncated: output.truncated }
|
|
769
|
+
: result;
|
|
334
770
|
}
|
|
335
771
|
|
|
336
772
|
export default defineTool({
|
|
@@ -346,9 +782,11 @@ Output modes:
|
|
|
346
782
|
- "count" — show match count per file
|
|
347
783
|
|
|
348
784
|
Guidelines:
|
|
349
|
-
- Uses
|
|
785
|
+
- Uses the JavaScript RegExp syntax supported by the running Node.js version
|
|
786
|
+
- Escape special characters such as \\. and \\{
|
|
787
|
+
- Skips symlinks, binary files, invalid UTF-8, and text files larger than 16 MiB
|
|
350
788
|
- Use glob or type filters to narrow the search
|
|
351
|
-
- Skips
|
|
789
|
+
- Skips common large directories such as node_modules and .git
|
|
352
790
|
- Results are limited to 500 matches by default`,
|
|
353
791
|
zh: `用正则表达式搜索文件内容。
|
|
354
792
|
|
|
@@ -360,9 +798,11 @@ Guidelines:
|
|
|
360
798
|
- "count" — 显示每个文件的匹配数量
|
|
361
799
|
|
|
362
800
|
使用指南:
|
|
363
|
-
-
|
|
801
|
+
- 使用当前 Node.js 版本支持的 JavaScript RegExp 语法
|
|
802
|
+
- 特殊字符需转义,如 \\.、\\{
|
|
803
|
+
- 跳过符号链接、二进制、无效 UTF-8 和超过 16 MiB 的文本文件
|
|
364
804
|
- 用 glob 或 type 过滤缩小搜索范围
|
|
365
|
-
-
|
|
805
|
+
- 跳过 node_modules、.git 等常见大目录
|
|
366
806
|
- 默认结果限制 500 条`
|
|
367
807
|
},
|
|
368
808
|
parameters: {
|
|
@@ -491,35 +931,59 @@ Guidelines:
|
|
|
491
931
|
before,
|
|
492
932
|
after,
|
|
493
933
|
multiline,
|
|
494
|
-
maxResults: headLimit,
|
|
934
|
+
maxResults: headLimit + 1,
|
|
495
935
|
byteBudget: SEARCH_RESULT_BYTES,
|
|
936
|
+
cwd: absPath,
|
|
937
|
+
signal: ctx?.signal,
|
|
938
|
+
structured: true,
|
|
496
939
|
};
|
|
497
940
|
|
|
498
941
|
try {
|
|
942
|
+
throwIfAborted(ctx?.signal);
|
|
943
|
+
const regexPlan = prepareGrepPattern(pattern, options);
|
|
944
|
+
options.regexPlan = regexPlan;
|
|
499
945
|
let result;
|
|
500
|
-
|
|
946
|
+
let rgCommand = null;
|
|
947
|
+
if (regexPlan.useRipgrep) {
|
|
948
|
+
rgCommand = resolveManagedCliCommand('rg', { yeaftDir: ctx?.yeaftDir });
|
|
949
|
+
if (!rgCommand) {
|
|
950
|
+
await waitForAbortable(managedCliToolReady(ctx?.managedCliReady, 'rg'), ctx?.signal);
|
|
951
|
+
rgCommand = resolveManagedCliCommand('rg', { yeaftDir: ctx?.yeaftDir });
|
|
952
|
+
}
|
|
953
|
+
}
|
|
501
954
|
|
|
502
|
-
if (
|
|
503
|
-
|
|
955
|
+
if (rgCommand) {
|
|
956
|
+
try {
|
|
957
|
+
const candidatePaths = await listRipgrepCandidatePaths(rgCommand, absPath, {
|
|
958
|
+
...options,
|
|
959
|
+
pattern,
|
|
960
|
+
});
|
|
961
|
+
result = await nodeGrep(pattern, absPath, { ...options, candidatePaths });
|
|
962
|
+
} catch (error) {
|
|
963
|
+
if (isAbortError(error)) throw error;
|
|
964
|
+
result = await nodeGrep(pattern, absPath, options);
|
|
965
|
+
}
|
|
504
966
|
} else {
|
|
505
967
|
result = await nodeGrep(pattern, absPath, options);
|
|
506
968
|
}
|
|
507
969
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
970
|
+
const {
|
|
971
|
+
output = '', records = [], resultCount = 0, truncated = false,
|
|
972
|
+
} = result || {};
|
|
973
|
+
if (records.length === 0 && !output.trim()) return '(no matches)';
|
|
974
|
+
if (truncated) return output.trim();
|
|
975
|
+
const sortedRecords = [...records].sort(compareGrepRecords);
|
|
976
|
+
const visibleMatches = sortedRecords
|
|
977
|
+
.filter(record => record.kind !== 'context')
|
|
978
|
+
.slice(0, headLimit);
|
|
979
|
+
return renderSelectedRecords(
|
|
980
|
+
sortedRecords,
|
|
981
|
+
visibleMatches,
|
|
982
|
+
options,
|
|
983
|
+
resultCount > headLimit,
|
|
984
|
+
);
|
|
522
985
|
} catch (err) {
|
|
986
|
+
if (isAbortError(err)) throw err;
|
|
523
987
|
return formatGrepError(err.message);
|
|
524
988
|
}
|
|
525
989
|
},
|