mixdog 0.9.9 → 0.9.11
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/package.json +1 -1
- package/scripts/agent-tag-reuse-smoke.mjs +12 -8
- package/scripts/bench/cache-probe-tasks.json +8 -0
- package/scripts/bench/lead-review-tasks-r3.json +1 -1
- package/scripts/bench/r4-mixed-tasks.json +1 -1
- package/scripts/bench/review-tasks.json +1 -1
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/tool-smoke.mjs +109 -0
- package/src/defaults/mixdog-config.template.json +1 -0
- package/src/mixdog-session-runtime.mjs +8 -0
- package/src/rules/agent/30-explorer.md +18 -20
- package/src/rules/lead/02-channels.md +3 -3
- package/src/runtime/agent/orchestrator/config.mjs +30 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +160 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +25 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +24 -9
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +18 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +79 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -22
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +29 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +17 -16
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +30 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +333 -14
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +129 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +58 -18
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +56 -83
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +26 -56
- package/src/runtime/channels/lib/webhook/deliveries.mjs +4 -1
- package/src/session-runtime/settings-api.mjs +13 -0
- package/src/session-runtime/tool-catalog.mjs +34 -7
- package/src/standalone/agent-tool/render.mjs +2 -0
- package/src/standalone/agent-tool.mjs +28 -6
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +85 -21
- package/src/tui/App.jsx +20 -1
- package/src/tui/app/extension-pickers.mjs +6 -3
- package/src/tui/dist/index.mjs +28 -4
- package/src/tui/engine.mjs +2 -0
|
@@ -3,6 +3,7 @@ import { isAbsolute, resolve } from 'path';
|
|
|
3
3
|
import { trueCasePath } from './path-utils.mjs';
|
|
4
4
|
import {
|
|
5
5
|
canonicalizeGlobSlashes,
|
|
6
|
+
coerceReadFamilyPathArg,
|
|
6
7
|
coerceShapeFlex,
|
|
7
8
|
extractGlobBaseDirectory,
|
|
8
9
|
hasGlobMagic,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
_suggestIndexedPaths,
|
|
18
19
|
basePathDiagnostic,
|
|
19
20
|
buildNotFoundHint,
|
|
21
|
+
tryReadFamilyEnoentRedirect,
|
|
20
22
|
isUncOrSmbPath,
|
|
21
23
|
relativePathPrefix,
|
|
22
24
|
relativeSearchResultPath,
|
|
@@ -186,7 +188,179 @@ function parseGrepCountLine(line) {
|
|
|
186
188
|
return { path, count };
|
|
187
189
|
}
|
|
188
190
|
|
|
189
|
-
|
|
191
|
+
const GREP_RESULT_LINE_SKIP = /^\[(?:Showing|total|pattern set|capped|warning|redirected|regex parse)/;
|
|
192
|
+
const GREP_CHUNK_AGGREGATE_FLOOR = 200;
|
|
193
|
+
const GREP_CHUNK_AGGREGATE_DEFAULT = 800;
|
|
194
|
+
const GREP_CHUNK_AGGREGATE_MAX = 4000;
|
|
195
|
+
|
|
196
|
+
function chunkPatternList(patterns, cap) {
|
|
197
|
+
const out = [];
|
|
198
|
+
for (let i = 0; i < patterns.length; i += cap) out.push(patterns.slice(i, i + cap));
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function computeGrepChunkAggregateBudget(offset, headLimit, headLimitCoerced) {
|
|
203
|
+
if (headLimitCoerced === 0 && headLimit === Infinity) return GREP_CHUNK_AGGREGATE_MAX;
|
|
204
|
+
if (headLimit === Infinity) return GREP_CHUNK_AGGREGATE_DEFAULT;
|
|
205
|
+
const need = offset + headLimit;
|
|
206
|
+
return Math.min(GREP_CHUNK_AGGREGATE_MAX, Math.max(GREP_CHUNK_AGGREGATE_FLOOR, need * 2));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function compareGrepLinesByPathLine(a, b) {
|
|
210
|
+
const pa = splitGrepLinePrefix(a);
|
|
211
|
+
const pb = splitGrepLinePrefix(b);
|
|
212
|
+
if (!pa && !pb) return String(a).localeCompare(String(b));
|
|
213
|
+
if (!pa) return 1;
|
|
214
|
+
if (!pb) return -1;
|
|
215
|
+
const byPath = pa.path.localeCompare(pb.path);
|
|
216
|
+
if (byPath !== 0) return byPath;
|
|
217
|
+
return pa.lineNo - pb.lineNo;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function deriveGrepCountLinesFromMatchContent(lines) {
|
|
221
|
+
const byPath = new Map();
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
const split = splitGrepLinePrefix(line);
|
|
224
|
+
if (!split || split.delimiter !== ':') continue;
|
|
225
|
+
if (!byPath.has(split.path)) byPath.set(split.path, new Set());
|
|
226
|
+
byPath.get(split.path).add(split.lineNo);
|
|
227
|
+
}
|
|
228
|
+
return [...byPath.entries()]
|
|
229
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
230
|
+
.map(([path, lineNos]) => `${path}:${lineNos.size}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isGrepMatchLine(line) {
|
|
234
|
+
const split = splitGrepLinePrefix(line);
|
|
235
|
+
return !!(split && split.delimiter === ':');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function grepMatchAnchorKey(line) {
|
|
239
|
+
const split = splitGrepLinePrefix(line);
|
|
240
|
+
if (!split || split.delimiter !== ':') return '';
|
|
241
|
+
return `${split.path}\0${split.lineNo}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseGrepContextBlocksInSegment(segmentLines) {
|
|
245
|
+
const blocks = [];
|
|
246
|
+
let pending = [];
|
|
247
|
+
let i = 0;
|
|
248
|
+
while (i < segmentLines.length) {
|
|
249
|
+
const line = segmentLines[i];
|
|
250
|
+
if (isGrepMatchLine(line)) {
|
|
251
|
+
const blockLines = pending.concat([line]);
|
|
252
|
+
pending = [];
|
|
253
|
+
i += 1;
|
|
254
|
+
while (i < segmentLines.length) {
|
|
255
|
+
const next = segmentLines[i];
|
|
256
|
+
if (next === '--' || isGrepMatchLine(next)) break;
|
|
257
|
+
blockLines.push(next);
|
|
258
|
+
i += 1;
|
|
259
|
+
}
|
|
260
|
+
const anchor = grepMatchAnchorKey(line);
|
|
261
|
+
if (anchor) blocks.push({ anchor, lines: blockLines });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
pending.push(line);
|
|
265
|
+
i += 1;
|
|
266
|
+
}
|
|
267
|
+
return blocks;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function compareGrepAnchorKeys(a, b) {
|
|
271
|
+
const [pa, la] = String(a || '').split('\0');
|
|
272
|
+
const [pb, lb] = String(b || '').split('\0');
|
|
273
|
+
const byPath = pa.localeCompare(pb);
|
|
274
|
+
if (byPath !== 0) return byPath;
|
|
275
|
+
return Number(la) - Number(lb);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function mergeGrepContextChunkLines(lines) {
|
|
279
|
+
const segments = [];
|
|
280
|
+
let current = [];
|
|
281
|
+
for (const line of lines) {
|
|
282
|
+
if (line === '--') {
|
|
283
|
+
segments.push(current);
|
|
284
|
+
current = [];
|
|
285
|
+
} else {
|
|
286
|
+
current.push(line);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
segments.push(current);
|
|
290
|
+
const seen = new Set();
|
|
291
|
+
const blocks = [];
|
|
292
|
+
for (const segment of segments) {
|
|
293
|
+
if (!segment.length) continue;
|
|
294
|
+
for (const block of parseGrepContextBlocksInSegment(segment)) {
|
|
295
|
+
if (!block.anchor || seen.has(block.anchor)) continue;
|
|
296
|
+
seen.add(block.anchor);
|
|
297
|
+
blocks.push(block);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
blocks.sort((a, b) => compareGrepAnchorKeys(a.anchor, b.anchor));
|
|
301
|
+
const out = [];
|
|
302
|
+
for (const block of blocks) {
|
|
303
|
+
if (out.length) out.push('--');
|
|
304
|
+
out.push(...block.lines);
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function mergeGrepChunkLines(lines, { outputMode, beforeN, afterN, contextN }) {
|
|
310
|
+
const hasContext = (beforeN > 0 || afterN > 0 || contextN > 0);
|
|
311
|
+
if (outputMode === 'count') {
|
|
312
|
+
return deriveGrepCountLinesFromMatchContent(lines);
|
|
313
|
+
}
|
|
314
|
+
if (outputMode === 'files_with_matches') {
|
|
315
|
+
const seen = new Set();
|
|
316
|
+
const out = [];
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
const path = String(line || '').trim();
|
|
319
|
+
if (!path || seen.has(path)) continue;
|
|
320
|
+
seen.add(path);
|
|
321
|
+
out.push(path);
|
|
322
|
+
}
|
|
323
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
if (outputMode === 'content' && hasContext) {
|
|
327
|
+
return mergeGrepContextChunkLines(lines);
|
|
328
|
+
}
|
|
329
|
+
const matches = new Map();
|
|
330
|
+
for (const line of lines) {
|
|
331
|
+
const split = splitGrepLinePrefix(line);
|
|
332
|
+
if (split && split.delimiter === ':') {
|
|
333
|
+
const key = `${split.path}\0${split.lineNo}`;
|
|
334
|
+
if (!matches.has(key)) matches.set(key, line);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return [...matches.values()].sort(compareGrepLinesByPathLine);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function extractGrepChunkResultLines(body, room = Infinity) {
|
|
341
|
+
const text = String(body || '').trim();
|
|
342
|
+
if (!text || /^Error:/i.test(text)) return { error: text || 'Error: empty grep chunk result' };
|
|
343
|
+
if (/^\(no matches\)/i.test(text)) return { lines: [], truncated: false };
|
|
344
|
+
const rawLines = text.split('\n');
|
|
345
|
+
const childShowingTruncated = rawLines.some((line) => /^\[Showing /i.test(String(line || '').trim()));
|
|
346
|
+
const lines = rawLines.filter((line) => line && !GREP_RESULT_LINE_SKIP.test(line));
|
|
347
|
+
const truncated = childShowingTruncated
|
|
348
|
+
|| (Number.isFinite(room) && room >= 0 && lines.length >= room);
|
|
349
|
+
return { lines, truncated };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function buildGrepChunkMergePrefix(patternChunkCount, truncated, aggregateBudget, outputMode = 'content') {
|
|
353
|
+
if (patternChunkCount <= 1 && !truncated) return '';
|
|
354
|
+
const parts = [];
|
|
355
|
+
if (patternChunkCount > 1) parts.push(`pattern set split into ${patternChunkCount} chunks`);
|
|
356
|
+
if (truncated) {
|
|
357
|
+
parts.push(`chunk results truncated at aggregate budget ${aggregateBudget} lines; results are partial`);
|
|
358
|
+
if (outputMode === 'count') parts.push('counts are lower bounds (>=)');
|
|
359
|
+
}
|
|
360
|
+
return `[${parts.join('; ')}]\n`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimit, offset, outputMode, patterns: _patterns, beforeN, afterN, contextN, searchPath, grepResolvedPath: _grepResolvedPath, workDir, globPatterns: _globPatterns, fileType: _fileType, filenameOmitted = false, prefix = '', broadAdvisory: _broadAdvisory = true, disableContentGrouping = false }) {
|
|
190
364
|
const lines = headLimit === Infinity ? windowed : windowed.slice(0, headLimit);
|
|
191
365
|
const normalized = lines.map((line) => relativeGrepLine(line, workDir, outputMode === 'files_with_matches', outputMode, filenameOmitted));
|
|
192
366
|
const remaining = Math.max(0, totalWindowed - lines.length);
|
|
@@ -210,7 +384,7 @@ function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimit, offs
|
|
|
210
384
|
countSummary = `\n[total ${totalMatches} match${totalMatches === 1 ? '' : 'es'} across ${fileCount} file${fileCount === 1 ? '' : 's'}]`;
|
|
211
385
|
}
|
|
212
386
|
const hasContext = (beforeN > 0 || afterN > 0 || contextN > 0);
|
|
213
|
-
const groupedBody = (outputMode === 'content' && !hasContext && !filenameOmitted)
|
|
387
|
+
const groupedBody = (outputMode === 'content' && !hasContext && !filenameOmitted && !disableContentGrouping)
|
|
214
388
|
? groupGrepContentByFile(normalized)
|
|
215
389
|
: normalized.join('\n');
|
|
216
390
|
const body = groupedBody + truncated + countSummary;
|
|
@@ -219,6 +393,7 @@ function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimit, offs
|
|
|
219
393
|
|
|
220
394
|
export async function executeGrepTool(args, workDir, executeChildBuiltinTool, readStateScope = null, options = {}) {
|
|
221
395
|
args = normalizeGrepArgs(args);
|
|
396
|
+
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
222
397
|
// Fan-out guard: batch multiple string paths sequentially, mirroring
|
|
223
398
|
// code_graph files[] batching. Recursive calls pass a single string
|
|
224
399
|
// path, so recursion bottoms out after one level.
|
|
@@ -280,12 +455,6 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
280
455
|
// \n; an explicit multiline:true from the caller still wins outright.
|
|
281
456
|
const patternsWantMultiline = patterns.some((p) => /\\n/.test(p));
|
|
282
457
|
const multilineMode = args.multiline === true || patternsWantMultiline;
|
|
283
|
-
if (multilineMode && patterns.length > GREP_MULTILINE_PATTERN_CAP) {
|
|
284
|
-
return `Error: multiline:true with more than ${GREP_MULTILINE_PATTERN_CAP} patterns is not allowed (got ${patterns.length}); split into separate grep calls`;
|
|
285
|
-
}
|
|
286
|
-
if (patterns.length > GREP_ARRAY_PATTERN_CAP) {
|
|
287
|
-
return `Error: pattern array exceeds the ${GREP_ARRAY_PATTERN_CAP}-pattern cap (got ${patterns.length}); split into separate grep calls`;
|
|
288
|
-
}
|
|
289
458
|
// Rescue: lookaround/backreference patterns are rejected by rg's default
|
|
290
459
|
// Rust regex engine ("look-around ... is not supported" / "backreferences
|
|
291
460
|
// ... not supported"). rg builds compiled with the optional `pcre2`
|
|
@@ -427,6 +596,80 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
427
596
|
}
|
|
428
597
|
if (fileTypes.length > 1) fileType = fileTypes;
|
|
429
598
|
else if (fileTypes.length === 1) fileType = fileTypes[0];
|
|
599
|
+
|
|
600
|
+
const patternChunkCap = multilineMode ? GREP_MULTILINE_PATTERN_CAP : GREP_ARRAY_PATTERN_CAP;
|
|
601
|
+
if (patterns.length > patternChunkCap) {
|
|
602
|
+
const patternChunks = chunkPatternList(patterns, patternChunkCap);
|
|
603
|
+
const aggregateBudget = computeGrepChunkAggregateBudget(offset, headLimit, headLimitCoerced);
|
|
604
|
+
let truncatedAggregate = false;
|
|
605
|
+
const chunkBaseArgs = {
|
|
606
|
+
...args,
|
|
607
|
+
offset: 0,
|
|
608
|
+
...(outputMode === 'count' ? { output_mode: 'content' } : {}),
|
|
609
|
+
};
|
|
610
|
+
const chunkMergeOptions = { ...options, _grepChunkMerge: true };
|
|
611
|
+
const mergedRaw = [];
|
|
612
|
+
for (const chunk of patternChunks) {
|
|
613
|
+
if (mergedRaw.length >= aggregateBudget) {
|
|
614
|
+
truncatedAggregate = true;
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
const room = aggregateBudget - mergedRaw.length;
|
|
618
|
+
const chunkBody = await executeGrepTool(
|
|
619
|
+
{ ...chunkBaseArgs, pattern: chunk, head_limit: room },
|
|
620
|
+
workDir,
|
|
621
|
+
executeChildBuiltinTool,
|
|
622
|
+
readStateScope,
|
|
623
|
+
chunkMergeOptions,
|
|
624
|
+
);
|
|
625
|
+
const extracted = extractGrepChunkResultLines(chunkBody, room);
|
|
626
|
+
if (extracted.error) return extracted.error.startsWith('Error:') ? extracted.error : `Error: ${extracted.error}`;
|
|
627
|
+
const slice = extracted.lines.slice(0, room);
|
|
628
|
+
mergedRaw.push(...slice);
|
|
629
|
+
if (extracted.truncated || extracted.lines.length > room) truncatedAggregate = true;
|
|
630
|
+
}
|
|
631
|
+
const chunkPrefix = buildGrepChunkMergePrefix(
|
|
632
|
+
patternChunks.length,
|
|
633
|
+
truncatedAggregate,
|
|
634
|
+
aggregateBudget,
|
|
635
|
+
outputMode,
|
|
636
|
+
);
|
|
637
|
+
const merged = mergeGrepChunkLines(mergedRaw, {
|
|
638
|
+
outputMode,
|
|
639
|
+
beforeN,
|
|
640
|
+
afterN,
|
|
641
|
+
contextN,
|
|
642
|
+
});
|
|
643
|
+
const sliced = offset > 0 ? merged.slice(offset) : merged;
|
|
644
|
+
const limit = headLimit === Infinity ? sliced.length : headLimit;
|
|
645
|
+
const windowed = limit === Infinity ? sliced : sliced.slice(0, limit);
|
|
646
|
+
if (!windowed.length) {
|
|
647
|
+
const patternStr = patterns.length === 1 ? JSON.stringify(patterns[0]) : JSON.stringify(patterns);
|
|
648
|
+
const globStr = normalizedGlobPatterns.length > 0 ? ` glob=${JSON.stringify(normalizedGlobPatterns)}` : '';
|
|
649
|
+
return `${chunkPrefix}(no matches) pattern=${patternStr} path=${searchPath}${globStr}`;
|
|
650
|
+
}
|
|
651
|
+
return formatGrepOutput({
|
|
652
|
+
windowed,
|
|
653
|
+
totalWindowed: merged.length,
|
|
654
|
+
totalKnown: !truncatedAggregate,
|
|
655
|
+
headLimit,
|
|
656
|
+
offset,
|
|
657
|
+
outputMode,
|
|
658
|
+
patterns,
|
|
659
|
+
beforeN,
|
|
660
|
+
afterN,
|
|
661
|
+
contextN,
|
|
662
|
+
searchPath,
|
|
663
|
+
grepResolvedPath,
|
|
664
|
+
workDir,
|
|
665
|
+
globPatterns: normalizedGlobPatterns,
|
|
666
|
+
fileType,
|
|
667
|
+
filenameOmitted: false,
|
|
668
|
+
prefix: chunkPrefix,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const forceGrepFilename = !!options._grepChunkMerge;
|
|
430
673
|
const cacheKey = buildGrepCacheKey({
|
|
431
674
|
patterns,
|
|
432
675
|
searchPath: normalizeOutputPath(grepResolvedPath),
|
|
@@ -443,6 +686,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
443
686
|
onlyMatching: args['-o'] === true,
|
|
444
687
|
fileType,
|
|
445
688
|
pcre2: pcre2Mode,
|
|
689
|
+
withFilename: forceGrepFilename,
|
|
446
690
|
});
|
|
447
691
|
// Single-file grep registers a whole-file read snapshot (parity with
|
|
448
692
|
// apply_patch), satisfying the read-before-edit guard while keeping drift
|
|
@@ -467,12 +711,27 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
467
711
|
let grepStat;
|
|
468
712
|
try { grepStat = statSync(grepResolvedPath); }
|
|
469
713
|
catch (err) {
|
|
714
|
+
const redirected = await tryReadFamilyEnoentRedirect({
|
|
715
|
+
workDir,
|
|
716
|
+
resolvedPath: grepResolvedPath,
|
|
717
|
+
requestedPath: searchPath,
|
|
718
|
+
errCode: err?.code,
|
|
719
|
+
options,
|
|
720
|
+
rerun: (target, opts) => executeGrepTool(
|
|
721
|
+
{ ...args, path: target },
|
|
722
|
+
workDir,
|
|
723
|
+
executeChildBuiltinTool,
|
|
724
|
+
readStateScope,
|
|
725
|
+
opts,
|
|
726
|
+
),
|
|
727
|
+
});
|
|
728
|
+
if (redirected) return redirected;
|
|
470
729
|
const msg = `Error: path does not exist: ${normalizeOutputPath(grepResolvedPath)} (${err?.code || 'ENOENT'})`;
|
|
471
730
|
const hint = buildNotFoundHint(workDir, grepResolvedPath, 'Search', err?.code);
|
|
472
731
|
if (hint) return msg + hint;
|
|
473
732
|
return msg + await _suggestIndexedPaths(grepResolvedPath, executeChildBuiltinTool, workDir);
|
|
474
733
|
}
|
|
475
|
-
const filenameOmitted = grepStat.isFile();
|
|
734
|
+
const filenameOmitted = forceGrepFilename ? false : grepStat.isFile();
|
|
476
735
|
|
|
477
736
|
// rg builds --glob overrides rooted at its process cwd and relativizes each
|
|
478
737
|
// candidate against it with a CASE-SENSITIVE prefix strip; workDir is
|
|
@@ -507,6 +766,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
507
766
|
fileType,
|
|
508
767
|
onlyMatching: args['-o'] === true,
|
|
509
768
|
pcre2: pcre2Mode,
|
|
769
|
+
withFilename: forceGrepFilename,
|
|
510
770
|
});
|
|
511
771
|
let windowed;
|
|
512
772
|
let totalWindowed = 0;
|
|
@@ -565,6 +825,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
565
825
|
globPatterns: normalizedGlobPatterns,
|
|
566
826
|
fileType,
|
|
567
827
|
filenameOmitted,
|
|
828
|
+
disableContentGrouping: !!options._grepChunkMerge,
|
|
568
829
|
});
|
|
569
830
|
if (!body) {
|
|
570
831
|
const pathInfo = grepStat.isDirectory() ? 'path exists (dir)' : 'path exists (file)';
|
|
@@ -650,6 +911,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
650
911
|
fileType,
|
|
651
912
|
onlyMatching: args['-o'] === true,
|
|
652
913
|
fixedStrings: true,
|
|
914
|
+
withFilename: forceGrepFilename,
|
|
653
915
|
});
|
|
654
916
|
const effectiveHeadLimit = headLimit === Infinity
|
|
655
917
|
? (outputMode === 'content' ? GREP_CONTENT_HARD_CAP : Infinity)
|
|
@@ -723,11 +985,34 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
723
985
|
|
|
724
986
|
export async function executeGlobTool(args, workDir, options = {}) {
|
|
725
987
|
args = normalizeGlobArgs(args);
|
|
726
|
-
args.path =
|
|
727
|
-
|
|
728
|
-
|
|
988
|
+
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
989
|
+
if (Array.isArray(args.path)) {
|
|
990
|
+
const GLOB_PATH_CAP = 10;
|
|
991
|
+
const seen = new Set();
|
|
992
|
+
const list = args.path
|
|
993
|
+
.map((p) => (typeof p === 'string' ? stripEmbeddedPathQuotes(normalizeInputPath(p)).trim() : ''))
|
|
994
|
+
.filter((p) => p && !seen.has(p) && seen.add(p));
|
|
995
|
+
if (list.length > 1) {
|
|
996
|
+
const capped = list.slice(0, GLOB_PATH_CAP);
|
|
997
|
+
const parts = [];
|
|
998
|
+
for (const p of capped) {
|
|
999
|
+
let body;
|
|
1000
|
+
try {
|
|
1001
|
+
body = await executeGlobTool({ ...args, path: p }, workDir, options);
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
body = `Error: ${err && err.message ? err.message : err}`;
|
|
1004
|
+
}
|
|
1005
|
+
parts.push(`# glob ${p}\n${body}`);
|
|
1006
|
+
}
|
|
1007
|
+
if (list.length > GLOB_PATH_CAP) parts.push(`[capped at ${GLOB_PATH_CAP} of ${list.length} paths]`);
|
|
1008
|
+
return parts.join('\n\n');
|
|
1009
|
+
}
|
|
1010
|
+
args.path = list[0] ?? '.';
|
|
1011
|
+
} else {
|
|
1012
|
+
args.path = stripEmbeddedPathQuotes(normalizeInputPath(args.path));
|
|
1013
|
+
}
|
|
729
1014
|
if (Array.isArray(args.path) && args.path.length === 0) {
|
|
730
|
-
|
|
1015
|
+
args.path = '.';
|
|
731
1016
|
}
|
|
732
1017
|
args.pattern = coerceShapeFlex(args.pattern);
|
|
733
1018
|
const rawPattern = args.pattern;
|
|
@@ -750,12 +1035,29 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
750
1035
|
}
|
|
751
1036
|
}
|
|
752
1037
|
if (patterns.length === 0) {
|
|
753
|
-
|
|
1038
|
+
patterns = ['*'];
|
|
754
1039
|
}
|
|
755
1040
|
|
|
756
1041
|
const basePaths = (Array.isArray(args.path) && args.path.length > 0)
|
|
757
1042
|
? args.path
|
|
758
1043
|
: [args.path || '.'];
|
|
1044
|
+
if (!options._enoentRedirectFrom) {
|
|
1045
|
+
for (const only of basePaths) {
|
|
1046
|
+
const resolvedOnly = resolveSearchScope(only, workDir);
|
|
1047
|
+
try { statSync(resolvedOnly); }
|
|
1048
|
+
catch (err) {
|
|
1049
|
+
const redirected = await tryReadFamilyEnoentRedirect({
|
|
1050
|
+
workDir,
|
|
1051
|
+
resolvedPath: resolvedOnly,
|
|
1052
|
+
requestedPath: only,
|
|
1053
|
+
errCode: err?.code,
|
|
1054
|
+
options,
|
|
1055
|
+
rerun: (target, opts) => executeGlobTool({ ...args, path: target }, workDir, opts),
|
|
1056
|
+
});
|
|
1057
|
+
if (redirected) return redirected;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
759
1061
|
// A base path carrying glob magic (path:'src/**/cache/*') names a SET of
|
|
760
1062
|
// directories, not a literal one — resolving it literally ENOENTs. Split
|
|
761
1063
|
// it the way grep's path handling does: walk from the static baseDir and
|
|
@@ -854,6 +1156,22 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
854
1156
|
rgArgs.push('.');
|
|
855
1157
|
try { statSync(rgCwd); }
|
|
856
1158
|
catch (err) {
|
|
1159
|
+
const redirected = await tryReadFamilyEnoentRedirect({
|
|
1160
|
+
workDir,
|
|
1161
|
+
resolvedPath: rgCwd,
|
|
1162
|
+
requestedPath: root,
|
|
1163
|
+
errCode: err?.code,
|
|
1164
|
+
options,
|
|
1165
|
+
rerun: (target, opts) => executeGlobTool({ ...args, path: target }, workDir, opts),
|
|
1166
|
+
});
|
|
1167
|
+
if (redirected) {
|
|
1168
|
+
return {
|
|
1169
|
+
error: null,
|
|
1170
|
+
paths: [],
|
|
1171
|
+
stdoutTruncated: false,
|
|
1172
|
+
redirected,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
857
1175
|
const hint = buildNotFoundHint(workDir, rgCwd, 'Search', err?.code);
|
|
858
1176
|
return {
|
|
859
1177
|
error: `path does not exist: ${normalizeOutputPath(rgCwd)} (${err?.code || 'ENOENT'})${hint}`,
|
|
@@ -884,6 +1202,7 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
884
1202
|
}));
|
|
885
1203
|
|
|
886
1204
|
outer: for (const run of groupRuns) {
|
|
1205
|
+
if (run.redirected) return run.redirected;
|
|
887
1206
|
if (run.error) {
|
|
888
1207
|
rgErrors.push(run.error);
|
|
889
1208
|
continue;
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// verbatim from patch.mjs; native protocol, cache/snapshot side effects, and
|
|
3
3
|
// output formatting are unchanged.
|
|
4
4
|
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { readFileSync, lstatSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { unlink } from 'node:fs/promises';
|
|
7
|
+
import { dirname as pathDirname } from 'node:path';
|
|
6
8
|
import { performance } from 'node:perf_hooks';
|
|
7
9
|
import {
|
|
8
10
|
normalizeOutputPath,
|
|
@@ -10,8 +12,18 @@ import {
|
|
|
10
12
|
recordReadSnapshotForPath,
|
|
11
13
|
clearReadSnapshotForPath,
|
|
12
14
|
} from '../builtin.mjs';
|
|
15
|
+
import { atomicWrite } from '../builtin/atomic-write.mjs';
|
|
16
|
+
import { isSpecialFileStat } from '../builtin/device-paths.mjs';
|
|
13
17
|
import { markCodeGraphDirtyPaths } from '../code-graph-state.mjs';
|
|
14
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
classifyEntry,
|
|
20
|
+
stripDiffPrefix,
|
|
21
|
+
resolveEntryPath,
|
|
22
|
+
parsedEntryResolvedPath,
|
|
23
|
+
isResolvedPathOutsideBase,
|
|
24
|
+
countHunkChanges,
|
|
25
|
+
} from './paths.mjs';
|
|
26
|
+
import { applyV4AHunksToLines } from './v4a-convert.mjs';
|
|
15
27
|
import {
|
|
16
28
|
getNativePatchServer,
|
|
17
29
|
scheduleNativePatchIdleClose,
|
|
@@ -169,3 +181,118 @@ export async function dispatchNativePatch({ entries, basePath, nativePatchStr, f
|
|
|
169
181
|
}
|
|
170
182
|
return lines.join('\n');
|
|
171
183
|
}
|
|
184
|
+
|
|
185
|
+
function entryPathKey(fullPath) {
|
|
186
|
+
return process.platform === 'win32' ? String(fullPath || '').toLowerCase() : String(fullPath || '');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function joinTextLinesForPatch(lines) {
|
|
190
|
+
const body = (lines || []).join('\n');
|
|
191
|
+
return lines?.hasFinalNewline !== false ? `${body}\n` : body;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function lstatRegularPatchFile(fullPath, displayPath) {
|
|
195
|
+
const st = lstatSync(fullPath);
|
|
196
|
+
if (isSpecialFileStat(st)) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`apply_patch: cannot patch special file (FIFO / character / block device / socket): ${normalizeOutputPath(displayPath)}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return st;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function findParsedForRow(row, parsed, basePath) {
|
|
205
|
+
const key = entryPathKey(row.fullPath);
|
|
206
|
+
for (const entry of parsed || []) {
|
|
207
|
+
if (entryPathKey(parsedEntryResolvedPath(entry, basePath)) === key) return entry;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function applyJsParsedEntry(entry, basePath, { dryRun, fuzzy, readStateScope }) {
|
|
213
|
+
const kind = classifyEntry(entry);
|
|
214
|
+
const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
|
|
215
|
+
const fullPath = resolveEntryPath(basePath, headerName);
|
|
216
|
+
const displayPath = normalizeOutputPath(stripDiffPrefix(headerName));
|
|
217
|
+
if (!isResolvedPathOutsideBase(fullPath, basePath)) {
|
|
218
|
+
throw new Error(`apply_patch: internal JS dispatch invoked for in-base path ${displayPath}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (kind === 'create') {
|
|
222
|
+
const addedLines = [];
|
|
223
|
+
for (const hunk of entry.hunks || []) {
|
|
224
|
+
for (const line of hunk.lines || []) {
|
|
225
|
+
if (line.startsWith('+')) addedLines.push(line.slice(1));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const content = joinTextLinesForPatch(addedLines);
|
|
229
|
+
if (!dryRun) {
|
|
230
|
+
mkdirSync(pathDirname(fullPath), { recursive: true });
|
|
231
|
+
await atomicWrite(fullPath, content, { sessionId: readStateScope });
|
|
232
|
+
invalidateBuiltinResultCache([fullPath]);
|
|
233
|
+
markCodeGraphDirtyPaths([fullPath]);
|
|
234
|
+
recordReadSnapshotForPath(fullPath, readStateScope, { source: 'apply_patch_js', isPartialView: false });
|
|
235
|
+
}
|
|
236
|
+
const { added, removed } = countHunkChanges(entry.hunks);
|
|
237
|
+
return { kind, fullPath, displayPath, added, removed };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (kind === 'delete') {
|
|
241
|
+
lstatRegularPatchFile(fullPath, displayPath);
|
|
242
|
+
if (!dryRun) {
|
|
243
|
+
await unlink(fullPath);
|
|
244
|
+
invalidateBuiltinResultCache([fullPath]);
|
|
245
|
+
markCodeGraphDirtyPaths([fullPath]);
|
|
246
|
+
clearReadSnapshotForPath(fullPath, readStateScope);
|
|
247
|
+
}
|
|
248
|
+
const { added, removed } = countHunkChanges(entry.hunks);
|
|
249
|
+
return { kind, fullPath, displayPath, added, removed };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
lstatRegularPatchFile(fullPath, displayPath);
|
|
253
|
+
const raw = readFileSync(fullPath);
|
|
254
|
+
const sourceLines = splitTextLinesForPatch(raw.toString('utf8'));
|
|
255
|
+
const v4aHunks = (entry.hunks || []).map((h) => ({ lines: h.lines || [] }));
|
|
256
|
+
const updatedLines = applyV4AHunksToLines(sourceLines, v4aHunks, { fuzzy });
|
|
257
|
+
const content = joinTextLinesForPatch(updatedLines);
|
|
258
|
+
if (!dryRun) {
|
|
259
|
+
await atomicWrite(fullPath, content, { sessionId: readStateScope });
|
|
260
|
+
invalidateBuiltinResultCache([fullPath]);
|
|
261
|
+
markCodeGraphDirtyPaths([fullPath]);
|
|
262
|
+
recordReadSnapshotForPath(fullPath, readStateScope, { source: 'apply_patch_js', isPartialView: false });
|
|
263
|
+
}
|
|
264
|
+
const { added, removed } = countHunkChanges(entry.hunks);
|
|
265
|
+
return { kind, fullPath, displayPath, added, removed };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function dispatchJsPatchEntries({ rows, parsed, basePath, dryRun, fuzzy, readStateScope }) {
|
|
269
|
+
const applied = [];
|
|
270
|
+
for (const row of rows || []) {
|
|
271
|
+
const entry = findParsedForRow(row, parsed, basePath);
|
|
272
|
+
if (!entry) throw new Error(`apply_patch: missing parsed entry for ${row.displayPath}`);
|
|
273
|
+
try {
|
|
274
|
+
applied.push(await applyJsParsedEntry(entry, basePath, { dryRun, fuzzy, readStateScope }));
|
|
275
|
+
} catch (err) {
|
|
276
|
+
return `Error: ${err?.message || String(err)}`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const verbLabel = dryRun ? 'Checked' : 'Applied';
|
|
280
|
+
const countLabel = (count, singular, plural = `${singular}s`) => `${count} ${count === 1 ? singular : plural}`;
|
|
281
|
+
const kindLabel = (kind) => {
|
|
282
|
+
const text = String(kind || '').trim();
|
|
283
|
+
return text ? `${text.charAt(0).toUpperCase()}${text.slice(1).toLowerCase()}` : 'Update';
|
|
284
|
+
};
|
|
285
|
+
const lines = [`${verbLabel} ${countLabel(applied.length, 'File')} (JS)${dryRun ? ' Dry Run' : ''}`];
|
|
286
|
+
for (const entry of applied) {
|
|
287
|
+
const added = entry.added || 0;
|
|
288
|
+
const removed = entry.removed || 0;
|
|
289
|
+
const parts = [];
|
|
290
|
+
if (added > 0) parts.push(`+${countLabel(added, 'Line')}`);
|
|
291
|
+
if (removed > 0) parts.push(`-${countLabel(removed, 'Line')}`);
|
|
292
|
+
const detail = parts.join(' · ');
|
|
293
|
+
lines.push(detail
|
|
294
|
+
? ` OK ${kindLabel(entry.kind)} ${entry.displayPath} — ${detail}`
|
|
295
|
+
: ` OK ${kindLabel(entry.kind)} ${entry.displayPath}`);
|
|
296
|
+
}
|
|
297
|
+
return lines.join('\n');
|
|
298
|
+
}
|