mixdog 0.9.52 → 0.9.53
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/bench-run.mjs +2 -2
- package/scripts/compact-pressure-test.mjs +104 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/lifecycle-api-test.mjs +65 -4
- package/scripts/max-output-recovery-test.mjs +31 -0
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
- package/scripts/openai-ws-early-settle-test.mjs +40 -0
- package/scripts/parent-abort-link-test.mjs +24 -0
- package/scripts/process-lifecycle-test.mjs +80 -22
- package/scripts/provider-contract-test.mjs +257 -0
- package/scripts/provider-toolcall-test.mjs +172 -10
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
- package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
- package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
- package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +6 -6
- package/src/runtime/channels/lib/config.mjs +15 -2
- package/src/runtime/channels/lib/memory-client.mjs +20 -3
- package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
- package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
- package/src/runtime/channels/lib/worker-main.mjs +16 -5
- package/src/runtime/memory/index.mjs +24 -198
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
- package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
- package/src/runtime/memory/lib/query-handlers.mjs +2 -122
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
- package/src/runtime/search/lib/http-fetch.mjs +1 -1
- package/src/runtime/shared/config.mjs +58 -13
- package/src/runtime/shared/llm/cost.mjs +14 -4
- package/src/runtime/shared/memory-snapshot.mjs +236 -0
- package/src/runtime/shared/process-lifecycle.mjs +92 -19
- package/src/runtime/shared/process-shutdown.mjs +6 -0
- package/src/runtime/shared/resource-admission.mjs +7 -2
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/cwd-plugins.mjs +9 -7
- package/src/session-runtime/env.mjs +1 -2
- package/src/session-runtime/hitch-profile.mjs +45 -0
- package/src/session-runtime/lifecycle-api.mjs +36 -9
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/runtime-core.mjs +44 -103
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +15 -89
- package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
- package/src/standalone/agent-tool/worker-rows.mjs +93 -0
- package/src/standalone/agent-tool.mjs +12 -162
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +202 -65
- package/src/tui/engine/session-api-ext.mjs +37 -4
- package/src/tui/index.jsx +1 -0
- package/src/tui/lib/voice-setup.mjs +5 -5
- package/scripts/_devtools-stub.mjs +0 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -288
|
@@ -8,9 +8,7 @@ import {
|
|
|
8
8
|
normalizeOutputPath,
|
|
9
9
|
resolveAgainstCwd,
|
|
10
10
|
} from './path-utils.mjs';
|
|
11
|
-
import { buildNotFoundHint, finalizeReadFamilyEnoentTail, tryReadFamilyEnoentRedirect } from './search-path-diagnostics.mjs';
|
|
12
11
|
import { normalizeErrorMessage } from './path-diagnostics.mjs';
|
|
13
|
-
import { isUncPath, isWindowsDevicePath, hasUnsafeWin32Component } from './device-paths.mjs';
|
|
14
12
|
import {
|
|
15
13
|
buildListCacheKey,
|
|
16
14
|
DEFAULT_IGNORE_GLOBS,
|
|
@@ -36,49 +34,12 @@ import { runRg } from './rg-runner.mjs';
|
|
|
36
34
|
import { hasSpareCapacity as childSpawnHasSpareCapacity } from '../../../../shared/child-spawn-gate.mjs';
|
|
37
35
|
import { fuzzyRank } from './fuzzy-match.mjs';
|
|
38
36
|
import { assertPathReachable } from './fs-reachability.mjs';
|
|
37
|
+
import { listGuardPath, normalizeListHeadLimit, readFamilyPathEnoentOrError } from './lib/list-helpers.mjs';
|
|
39
38
|
|
|
40
39
|
const FIND_WALK_TIMEOUT_MS = 20_000;
|
|
41
40
|
const LIST_WALK_TIMEOUT_MS = 20_000;
|
|
42
41
|
const LIST_ABSOLUTE_CAP = 50_000;
|
|
43
42
|
|
|
44
|
-
/** undefined / invalid / negative → defaultCap; 0 = no page cap (absolute caps still apply). */
|
|
45
|
-
async function readFamilyPathEnoentOrError(workDir, fullPath, inputPath, args, options, err, rerunTool) {
|
|
46
|
-
const redirected = await tryReadFamilyEnoentRedirect({
|
|
47
|
-
workDir,
|
|
48
|
-
resolvedPath: fullPath,
|
|
49
|
-
requestedPath: inputPath,
|
|
50
|
-
errCode: err?.code,
|
|
51
|
-
options,
|
|
52
|
-
rerun: (target, opts) => rerunTool({ ...args, path: target }, workDir, opts),
|
|
53
|
-
});
|
|
54
|
-
if (redirected) return redirected;
|
|
55
|
-
const msg = `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
56
|
-
const hint = buildNotFoundHint(workDir, fullPath, 'List', err?.code);
|
|
57
|
-
return msg + finalizeReadFamilyEnoentTail(hint, inputPath, err?.code);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function normalizeListHeadLimit(raw, defaultCap) {
|
|
61
|
-
if (raw === undefined || raw === null || raw === '') return defaultCap;
|
|
62
|
-
const n = Number(raw);
|
|
63
|
-
if (!Number.isFinite(n) || n < 0) return defaultCap;
|
|
64
|
-
return Math.floor(n);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// UNC / Windows-device / NTFS-ADS guard for directory-walking modes
|
|
68
|
-
// (list / tree / find). Walking a UNC share auto-authenticates to the
|
|
69
|
-
// remote host (NTLM hash leak); a raw-device / reserved-name path can
|
|
70
|
-
// hang or grant raw access. Mirrors the read path's string-based checks.
|
|
71
|
-
// Returns an Error string when the path is blocked, else null.
|
|
72
|
-
function listGuardPath(p) {
|
|
73
|
-
if (typeof isUncPath === 'function' && isUncPath(p))
|
|
74
|
-
return `Error: cannot walk UNC / SMB path (network credential leak risk): ${normalizeOutputPath(p)}`;
|
|
75
|
-
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(p))
|
|
76
|
-
return `Error: cannot walk Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(p)}`;
|
|
77
|
-
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(p))
|
|
78
|
-
return `Error: cannot walk Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(p)}`;
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
43
|
export async function executeListTool(args, workDir, options = {}) {
|
|
83
44
|
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
84
45
|
if (Array.isArray(args.path)) {
|
|
@@ -62,6 +62,25 @@ import {
|
|
|
62
62
|
statPathsForMtime,
|
|
63
63
|
} from './cache-layers.mjs';
|
|
64
64
|
import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX, hasUnsupportedRipgrepRegex } from './arg-guard.mjs';
|
|
65
|
+
import {
|
|
66
|
+
buildGrepChunkMergePrefix,
|
|
67
|
+
chunkPatternList,
|
|
68
|
+
computeGrepChunkAggregateBudget,
|
|
69
|
+
extractGrepChunkResultLines,
|
|
70
|
+
mergeGrepChunkLines,
|
|
71
|
+
} from './lib/search-grep-chunks.mjs';
|
|
72
|
+
import {
|
|
73
|
+
expandLegacyEscapedAlternationPattern,
|
|
74
|
+
relativeGrepLine,
|
|
75
|
+
uniqueStrings,
|
|
76
|
+
isRgRegexParseError,
|
|
77
|
+
regexPatternToFixedTerms,
|
|
78
|
+
coerceNonNegInt,
|
|
79
|
+
globMtimeTiePath,
|
|
80
|
+
splitGlobString,
|
|
81
|
+
isRedundantAllFilesGlob,
|
|
82
|
+
parseGrepCountLine,
|
|
83
|
+
} from './lib/search-input-helpers.mjs';
|
|
65
84
|
|
|
66
85
|
// Default surrounding-lines window applied by output_mode:'content_with_context'
|
|
67
86
|
// when the caller does not pass an explicit -A/-B/-C/context. Sized to cover a
|
|
@@ -70,104 +89,11 @@ import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX, hasUnsupportedRipgrepRege
|
|
|
70
89
|
// context-mode clamp by policy); only head_limit blocks are context-clamped.
|
|
71
90
|
const GREP_AUTO_CONTEXT_LINES = 25;
|
|
72
91
|
|
|
73
|
-
function expandLegacyEscapedAlternationPattern(rawPattern) {
|
|
74
|
-
if (typeof rawPattern !== 'string' || !rawPattern.includes('\\|')) return null;
|
|
75
|
-
const parts = rawPattern.split('\\|').map((part) => part.trim()).filter(Boolean);
|
|
76
|
-
return parts.length > 1 ? parts : null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function relativeGrepLine(line, workDir, pathOnly = false, outputMode = 'content', filenameOmitted = false) {
|
|
80
|
-
const normalized = normalizeGrepLine(line, pathOnly, outputMode, filenameOmitted);
|
|
81
|
-
if (!workDir) return normalized;
|
|
82
|
-
if (pathOnly) return relativePathPrefix(normalized, workDir);
|
|
83
|
-
if (filenameOmitted) return normalized;
|
|
84
|
-
const split = splitGrepLinePrefix(normalized);
|
|
85
|
-
if (split) {
|
|
86
|
-
return relativePathPrefix(normalized.slice(0, split.pathEnd), workDir) + normalized.slice(split.pathEnd);
|
|
87
|
-
}
|
|
88
|
-
if (outputMode === 'count') {
|
|
89
|
-
const countSplit = splitGrepCountPrefix(normalized);
|
|
90
|
-
if (countSplit) {
|
|
91
|
-
return relativePathPrefix(normalized.slice(0, countSplit.pathEnd), workDir) + normalized.slice(countSplit.pathEnd);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return normalized;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function uniqueStrings(values) {
|
|
98
|
-
return Array.from(new Set(values.filter((value) => typeof value === 'string' && value)));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function isRgRegexParseError(err) {
|
|
102
|
-
const msg = `${err?.stderr || ''}\n${err?.message || err || ''}`;
|
|
103
|
-
return /regex parse error/i.test(msg);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function regexPatternToFixedTerms(pattern) {
|
|
107
|
-
const raw = String(pattern || '');
|
|
108
|
-
if (!raw) return [];
|
|
109
|
-
return raw
|
|
110
|
-
.split(/\\?\|/g)
|
|
111
|
-
.map((part) => part.trim())
|
|
112
|
-
.map((part) => part
|
|
113
|
-
.replace(/\\[bB]/g, '')
|
|
114
|
-
.replace(/^\^/, '')
|
|
115
|
-
.replace(/\$$/, '')
|
|
116
|
-
.replace(/\\([\\.^$*+?()[\]{}|/-])/g, '$1')
|
|
117
|
-
.trim())
|
|
118
|
-
.filter((part) => part.length > 0);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function coerceNonNegInt(value) {
|
|
122
|
-
if (value === undefined || value === null || value === '') return null;
|
|
123
|
-
const n = Number(value);
|
|
124
|
-
if (!Number.isFinite(n) || n < 0) return NaN;
|
|
125
|
-
return Math.floor(n);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function globMtimeTiePath(entry) {
|
|
129
|
-
const p = String(entry?.path ?? entry?.full ?? '');
|
|
130
|
-
return process.platform === 'win32' ? p.toLocaleLowerCase() : p;
|
|
131
|
-
}
|
|
132
92
|
|
|
133
93
|
// CC parity (GrepTool.ts): a single glob string may pack multiple filters
|
|
134
94
|
// separated by whitespace or commas, e.g. "*.ts,*.tsx" or "*.ts *.tsx". Split
|
|
135
95
|
// each into its own --glob. Brace patterns ("*.{ts,tsx}") are left intact so
|
|
136
96
|
// their internal commas are not torn apart.
|
|
137
|
-
function splitGlobString(value) {
|
|
138
|
-
const out = [];
|
|
139
|
-
const str = String(value);
|
|
140
|
-
let depth = 0;
|
|
141
|
-
let token = '';
|
|
142
|
-
const flush = () => {
|
|
143
|
-
const trimmed = token.trim();
|
|
144
|
-
if (trimmed) out.push(trimmed);
|
|
145
|
-
token = '';
|
|
146
|
-
};
|
|
147
|
-
for (const ch of str) {
|
|
148
|
-
if (ch === '{') {
|
|
149
|
-
depth++;
|
|
150
|
-
token += ch;
|
|
151
|
-
} else if (ch === '}') {
|
|
152
|
-
if (depth > 0) depth--;
|
|
153
|
-
token += ch;
|
|
154
|
-
} else if (depth === 0 && (ch === ',' || /\s/.test(ch))) {
|
|
155
|
-
flush();
|
|
156
|
-
} else {
|
|
157
|
-
token += ch;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
flush();
|
|
161
|
-
return out;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function isRedundantAllFilesGlob(value) {
|
|
165
|
-
const g = canonicalizeGlobSlashes(String(value || '').trim())
|
|
166
|
-
.replace(/^\.\//, '')
|
|
167
|
-
.replace(/^\/+/, '')
|
|
168
|
-
.replace(/\/+$/, '');
|
|
169
|
-
return g === '**/*' || g === '**';
|
|
170
|
-
}
|
|
171
97
|
|
|
172
98
|
function grepMissingPatternMessage() {
|
|
173
99
|
return 'Error: grep requires pattern.';
|
|
@@ -177,190 +103,6 @@ function globMissingPatternMessage() {
|
|
|
177
103
|
return 'Error: glob requires pattern.';
|
|
178
104
|
}
|
|
179
105
|
|
|
180
|
-
function parseGrepCountLine(line) {
|
|
181
|
-
const text = String(line || '');
|
|
182
|
-
const searchFrom = /^[A-Za-z]:/.test(text) ? 2 : 0;
|
|
183
|
-
const idx = text.lastIndexOf(':');
|
|
184
|
-
if (idx <= searchFrom) return null;
|
|
185
|
-
const count = Number(text.slice(idx + 1));
|
|
186
|
-
if (!Number.isFinite(count) || count <= 0) return null;
|
|
187
|
-
const path = text.slice(0, idx);
|
|
188
|
-
if (!path) return null;
|
|
189
|
-
return { path, count };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const GREP_RESULT_LINE_SKIP = /^\[(?:Showing|total|pattern set|capped|warning|redirected|regex parse)/;
|
|
193
|
-
const GREP_CHUNK_AGGREGATE_FLOOR = 200;
|
|
194
|
-
const GREP_CHUNK_AGGREGATE_DEFAULT = 800;
|
|
195
|
-
const GREP_CHUNK_AGGREGATE_MAX = 4000;
|
|
196
|
-
|
|
197
|
-
function chunkPatternList(patterns, cap) {
|
|
198
|
-
const out = [];
|
|
199
|
-
for (let i = 0; i < patterns.length; i += cap) out.push(patterns.slice(i, i + cap));
|
|
200
|
-
return out;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function computeGrepChunkAggregateBudget(offset, headLimit, headLimitCoerced) {
|
|
204
|
-
if (headLimitCoerced === 0 && headLimit === Infinity) return GREP_CHUNK_AGGREGATE_MAX;
|
|
205
|
-
if (headLimit === Infinity) return GREP_CHUNK_AGGREGATE_DEFAULT;
|
|
206
|
-
const need = offset + headLimit;
|
|
207
|
-
return Math.min(GREP_CHUNK_AGGREGATE_MAX, Math.max(GREP_CHUNK_AGGREGATE_FLOOR, need * 2));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function compareGrepLinesByPathLine(a, b) {
|
|
211
|
-
const pa = splitGrepLinePrefix(a);
|
|
212
|
-
const pb = splitGrepLinePrefix(b);
|
|
213
|
-
if (!pa && !pb) return String(a).localeCompare(String(b));
|
|
214
|
-
if (!pa) return 1;
|
|
215
|
-
if (!pb) return -1;
|
|
216
|
-
const byPath = pa.path.localeCompare(pb.path);
|
|
217
|
-
if (byPath !== 0) return byPath;
|
|
218
|
-
return pa.lineNo - pb.lineNo;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function deriveGrepCountLinesFromMatchContent(lines) {
|
|
222
|
-
const byPath = new Map();
|
|
223
|
-
for (const line of lines) {
|
|
224
|
-
const split = splitGrepLinePrefix(line);
|
|
225
|
-
if (!split || split.delimiter !== ':') continue;
|
|
226
|
-
if (!byPath.has(split.path)) byPath.set(split.path, new Set());
|
|
227
|
-
byPath.get(split.path).add(split.lineNo);
|
|
228
|
-
}
|
|
229
|
-
return [...byPath.entries()]
|
|
230
|
-
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
231
|
-
.map(([path, lineNos]) => `${path}:${lineNos.size}`);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function isGrepMatchLine(line) {
|
|
235
|
-
const split = splitGrepLinePrefix(line);
|
|
236
|
-
return !!(split && split.delimiter === ':');
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function grepMatchAnchorKey(line) {
|
|
240
|
-
const split = splitGrepLinePrefix(line);
|
|
241
|
-
if (!split || split.delimiter !== ':') return '';
|
|
242
|
-
return `${split.path}\0${split.lineNo}`;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function parseGrepContextBlocksInSegment(segmentLines) {
|
|
246
|
-
const blocks = [];
|
|
247
|
-
let pending = [];
|
|
248
|
-
let i = 0;
|
|
249
|
-
while (i < segmentLines.length) {
|
|
250
|
-
const line = segmentLines[i];
|
|
251
|
-
if (isGrepMatchLine(line)) {
|
|
252
|
-
const blockLines = pending.concat([line]);
|
|
253
|
-
pending = [];
|
|
254
|
-
i += 1;
|
|
255
|
-
while (i < segmentLines.length) {
|
|
256
|
-
const next = segmentLines[i];
|
|
257
|
-
if (next === '--' || isGrepMatchLine(next)) break;
|
|
258
|
-
blockLines.push(next);
|
|
259
|
-
i += 1;
|
|
260
|
-
}
|
|
261
|
-
const anchor = grepMatchAnchorKey(line);
|
|
262
|
-
if (anchor) blocks.push({ anchor, lines: blockLines });
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
pending.push(line);
|
|
266
|
-
i += 1;
|
|
267
|
-
}
|
|
268
|
-
return blocks;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function compareGrepAnchorKeys(a, b) {
|
|
272
|
-
const [pa, la] = String(a || '').split('\0');
|
|
273
|
-
const [pb, lb] = String(b || '').split('\0');
|
|
274
|
-
const byPath = pa.localeCompare(pb);
|
|
275
|
-
if (byPath !== 0) return byPath;
|
|
276
|
-
return Number(la) - Number(lb);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function mergeGrepContextChunkLines(lines) {
|
|
280
|
-
const segments = [];
|
|
281
|
-
let current = [];
|
|
282
|
-
for (const line of lines) {
|
|
283
|
-
if (line === '--') {
|
|
284
|
-
segments.push(current);
|
|
285
|
-
current = [];
|
|
286
|
-
} else {
|
|
287
|
-
current.push(line);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
segments.push(current);
|
|
291
|
-
const seen = new Set();
|
|
292
|
-
const blocks = [];
|
|
293
|
-
for (const segment of segments) {
|
|
294
|
-
if (!segment.length) continue;
|
|
295
|
-
for (const block of parseGrepContextBlocksInSegment(segment)) {
|
|
296
|
-
if (!block.anchor || seen.has(block.anchor)) continue;
|
|
297
|
-
seen.add(block.anchor);
|
|
298
|
-
blocks.push(block);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
blocks.sort((a, b) => compareGrepAnchorKeys(a.anchor, b.anchor));
|
|
302
|
-
const out = [];
|
|
303
|
-
for (const block of blocks) {
|
|
304
|
-
if (out.length) out.push('--');
|
|
305
|
-
out.push(...block.lines);
|
|
306
|
-
}
|
|
307
|
-
return out;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function mergeGrepChunkLines(lines, { outputMode, beforeN, afterN, contextN }) {
|
|
311
|
-
const hasContext = (beforeN > 0 || afterN > 0 || contextN > 0);
|
|
312
|
-
if (outputMode === 'count') {
|
|
313
|
-
return deriveGrepCountLinesFromMatchContent(lines);
|
|
314
|
-
}
|
|
315
|
-
if (outputMode === 'files_with_matches') {
|
|
316
|
-
const seen = new Set();
|
|
317
|
-
const out = [];
|
|
318
|
-
for (const line of lines) {
|
|
319
|
-
const path = String(line || '').trim();
|
|
320
|
-
if (!path || seen.has(path)) continue;
|
|
321
|
-
seen.add(path);
|
|
322
|
-
out.push(path);
|
|
323
|
-
}
|
|
324
|
-
out.sort((a, b) => a.localeCompare(b));
|
|
325
|
-
return out;
|
|
326
|
-
}
|
|
327
|
-
if (outputMode === 'content' && hasContext) {
|
|
328
|
-
return mergeGrepContextChunkLines(lines);
|
|
329
|
-
}
|
|
330
|
-
const matches = new Map();
|
|
331
|
-
for (const line of lines) {
|
|
332
|
-
const split = splitGrepLinePrefix(line);
|
|
333
|
-
if (split && split.delimiter === ':') {
|
|
334
|
-
const key = `${split.path}\0${split.lineNo}`;
|
|
335
|
-
if (!matches.has(key)) matches.set(key, line);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return [...matches.values()].sort(compareGrepLinesByPathLine);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function extractGrepChunkResultLines(body, room = Infinity) {
|
|
342
|
-
const text = String(body || '').trim();
|
|
343
|
-
if (!text || /^Error:/i.test(text)) return { error: text || 'Error: empty grep chunk result' };
|
|
344
|
-
if (/^\(no matches\)/i.test(text)) return { lines: [], truncated: false };
|
|
345
|
-
const rawLines = text.split('\n');
|
|
346
|
-
const childShowingTruncated = rawLines.some((line) => /^\[Showing /i.test(String(line || '').trim()));
|
|
347
|
-
const lines = rawLines.filter((line) => line && !GREP_RESULT_LINE_SKIP.test(line));
|
|
348
|
-
const truncated = childShowingTruncated
|
|
349
|
-
|| (Number.isFinite(room) && room >= 0 && lines.length >= room);
|
|
350
|
-
return { lines, truncated };
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function buildGrepChunkMergePrefix(patternChunkCount, truncated, aggregateBudget, outputMode = 'content') {
|
|
354
|
-
if (patternChunkCount <= 1 && !truncated) return '';
|
|
355
|
-
const parts = [];
|
|
356
|
-
if (patternChunkCount > 1) parts.push(`pattern set split into ${patternChunkCount} chunks`);
|
|
357
|
-
if (truncated) {
|
|
358
|
-
parts.push(`chunk results truncated at aggregate budget ${aggregateBudget} lines; results are partial`);
|
|
359
|
-
if (outputMode === 'count') parts.push('counts are lower bounds (>=)');
|
|
360
|
-
}
|
|
361
|
-
return `[${parts.join('; ')}]\n`;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
106
|
// --- context-mode match-block windowing (Parts 2 & 3) ---------------------
|
|
365
107
|
// In context mode (explicit -A/-B/-C or content_with_context auto), head_limit
|
|
366
108
|
// and offset count MATCH BLOCKS, not raw output lines, and truncation keeps a
|