mixdog 0.9.93 → 0.9.94
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/src/rules/agent/30-explorer.md +22 -16
- package/src/rules/shared/01-tool.md +23 -18
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +22 -6
- package/src/runtime/agent/orchestrator/context/collect.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +100 -86
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +491 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +91 -13
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +90 -27
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +32 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +16 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +543 -19
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +48 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +71 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +51 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +6 -17
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -4
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
- package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
- package/src/runtime/shared/background-tasks.mjs +10 -3
- package/src/runtime/shared/child-spawn-gate.mjs +50 -26
- package/src/runtime/shared/task-notification-envelope.mjs +11 -2
- package/src/runtime/shared/tool-card-model.mjs +6 -2
- package/src/runtime/shared/tool-surface.mjs +7 -2
- package/src/session-runtime/provider-usage.mjs +26 -2
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/app/use-transcript-window.mjs +7 -1
- package/src/tui/components/Spinner.jsx +18 -9
- package/src/tui/dist/index.mjs +111 -25
- package/src/tui/engine/live-share.mjs +23 -3
- package/src/tui/engine/session-api.mjs +7 -0
- package/src/tui/engine/turn.mjs +77 -4
- package/src/tui/engine.mjs +14 -6
- package/src/tui/index.jsx +7 -0
package/package.json
CHANGED
|
@@ -9,22 +9,28 @@ kind: retrieval
|
|
|
9
9
|
Return only WHERE (`path:line`), never WHY. You ARE `explore`; never call it.
|
|
10
10
|
Use only grep/find/glob/code_graph; `read` and `list` are forbidden.
|
|
11
11
|
|
|
12
|
-
Turn 1 (`turn 1/3`) is the whole search
|
|
13
|
-
known facet and send one batch under the
|
|
14
|
-
|
|
15
|
-
`symbol_search`
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
12
|
+
Turn 1 (`turn 1/3`) is the whole search and should already mint anchors. Split
|
|
13
|
+
broad/uncertain input into every known facet and send one batch under the
|
|
14
|
+
shared one-route contract. Route each facet to the cheapest anchor source:
|
|
15
|
+
`code_graph` `symbol_search` whenever the facet names a plausible
|
|
16
|
+
symbol/identifier; grep `content_with_context` with `pattern[]` of 4–8
|
|
17
|
+
code-token variants for concept facets — its hits carry `path:line`, cite them
|
|
18
|
+
directly instead of re-mining; `find` `query[]` ONLY when the target is itself
|
|
19
|
+
a file/dir name or an unverified path fragment, never as a default extra
|
|
20
|
+
facet. For a symptom/behavior query, add the upstream producer/derivation
|
|
21
|
+
layer of the reported surface as extra facets in the SAME batch, never as a
|
|
22
|
+
later turn. Follow-up turns batch every unresolved facet in parallel; a
|
|
23
|
+
single-tool turn is allowed only when exactly one pre-anchor/zero-hit facet
|
|
24
|
+
remains.
|
|
25
|
+
|
|
26
|
+
Grep defaults to `output_mode:"content_with_context"` with `context:0`
|
|
27
|
+
(matches only — the match line already carries its citable `path:line`) and a
|
|
28
|
+
tight `head_limit` (≤20); never request surrounding context lines. Use
|
|
29
|
+
`files_with_matches` only as a cheap existence probe when a facet must be
|
|
30
|
+
scoped before searching. Each pattern is one identifier, camel/snake variant,
|
|
31
|
+
or concept synonym; never a prose phrase. Spaces and non-ASCII are allowed
|
|
32
|
+
only in verbatim quoted error/log literals. Translate other non-English
|
|
33
|
+
queries to English identifiers.
|
|
28
34
|
|
|
29
35
|
Scope is session cwd; `path` may be omitted. For unverified `src` paths, use
|
|
30
36
|
`find` first; never guess or invent directories or pair `path:"."` with guessed
|
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
# Tool Use
|
|
2
2
|
|
|
3
3
|
- Before the first call, gather every known facet — environment, capability,
|
|
4
|
-
artifact,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
artifact, failure checks — in one bounded tool message, one shortest route
|
|
5
|
+
per facet: broad/uncertain→`explore` (roles without it: `find`); known
|
|
6
|
+
name fragment→`find`; verified root+wildcard→`glob`; text/code→`grep`;
|
|
7
|
+
symbol body/relation→`code_graph`; known file/span→`read`, not `grep`;
|
|
8
|
+
verified directory→`list`; known edit→`apply_patch`; program/state
|
|
9
|
+
change→`shell`; web/current info→`search`.
|
|
10
|
+
- A turn is a plan, not a step: emit every already-determined call in one
|
|
11
|
+
concurrent message, merged per tool — one `shell` chain (`&&`/`;`), one
|
|
12
|
+
`read`, one `apply_patch` with verification in `post_shell`. In-message
|
|
13
|
+
order is guaranteed — edits land before the shell that checks them — so
|
|
14
|
+
produce and its check always ride one message, never a follow-up turn.
|
|
15
|
+
Distinct facets only — never two routes per facet. The archetype is two
|
|
16
|
+
turns — one message observes through the dedicated tools (`shell` beside
|
|
17
|
+
them, not instead of them), one chain produces and proves itself; a new
|
|
18
|
+
turn exists only at a true data dependency.
|
|
16
19
|
- Verified paths: project root, session cwd, user-provided, tool-returned.
|
|
17
20
|
`find` first for guessed path/name fragments; on ENOENT, find the basename.
|
|
18
21
|
Retry `EXPLORATION_FAILED` once with changed tokens.
|
|
19
22
|
- Stop when evidence covers the deliverable: a returned `path:line` or
|
|
20
|
-
nonzero `content_with_context` result is final
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- Verify
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
nonzero `content_with_context` result is final for its returned range. Read
|
|
24
|
+
is allowed for new/uncovered lines; do not call read when grep/read already
|
|
25
|
+
fully covers the requested range. Only zero/error results justify new scope.
|
|
26
|
+
- Verify in proportion to risk, appended to the producing chain (`shell`
|
|
27
|
+
tail or `post_shell`) — one decisive boundary probe covering its failure
|
|
28
|
+
modes. A pass is final — observed matching output IS the verification,
|
|
29
|
+
never re-checked in a later turn; on failure fix and rerun only what
|
|
30
|
+
failed. Optional diagnostics non-fatal; report verified vs assumed.
|
|
26
31
|
- `apply_patch` is the primary edit tool: once target path and new content are
|
|
27
32
|
known, include the patch in the current tool batch, hunk context verbatim
|
|
28
33
|
from the newest tool output of that span (post-patch content after edits).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { countJsonNextCalls } from './tools/next-call-utils.mjs';
|
|
3
|
-
import { splitGrepLinePrefix } from './tools/builtin/grep-formatting.mjs';
|
|
3
|
+
import { parseGrepContextHeader, splitGrepLinePrefix } from './tools/builtin/grep-formatting.mjs';
|
|
4
4
|
import {
|
|
5
5
|
appendAgentTrace,
|
|
6
6
|
normalizeSessionId,
|
|
@@ -236,7 +236,27 @@ export function parseGrepCoverage(resultText, toolName, toolArgs, resultKind) {
|
|
|
236
236
|
const out = [];
|
|
237
237
|
const seen = new Set();
|
|
238
238
|
let sectionPath = null;
|
|
239
|
+
let rawSourceLinesRemaining = 0;
|
|
240
|
+
const addLine = (path, lineNo) => {
|
|
241
|
+
if (!path || !Number.isInteger(lineNo) || lineNo < 1 || out.length >= GREP_COVERAGE_MAX) return;
|
|
242
|
+
const key = `${path}\0${lineNo}`;
|
|
243
|
+
if (seen.has(key)) return;
|
|
244
|
+
seen.add(key);
|
|
245
|
+
out.push({ path: String(path).replace(/\\/g, '/'), line: lineNo });
|
|
246
|
+
};
|
|
239
247
|
for (const line of String(resultText ?? '').split(/\r?\n/)) {
|
|
248
|
+
if (rawSourceLinesRemaining > 0) {
|
|
249
|
+
rawSourceLinesRemaining--;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const header = parseGrepContextHeader(line);
|
|
253
|
+
if (header) {
|
|
254
|
+
for (let lineNo = header.startLine; lineNo <= header.endLine && out.length < GREP_COVERAGE_MAX; lineNo++) {
|
|
255
|
+
addLine(header.path, lineNo);
|
|
256
|
+
}
|
|
257
|
+
rawSourceLinesRemaining = header.sourceLineCount;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
240
260
|
const section = line.match(/^# grep (.+)$/);
|
|
241
261
|
if (section) {
|
|
242
262
|
if (!section[1].startsWith('pattern:')) sectionPath = section[1];
|
|
@@ -252,11 +272,7 @@ export function parseGrepCoverage(resultText, toolName, toolArgs, resultKind) {
|
|
|
252
272
|
const path = split?.path || (omitted ? toolArgs.path : null) || (sectionOmitted ? sectionPath : null);
|
|
253
273
|
const lineNo = split?.lineNo || (omitted ? Number(omitted[1]) : null)
|
|
254
274
|
|| (sectionOmitted ? Number(sectionOmitted[1]) : null);
|
|
255
|
-
|
|
256
|
-
const key = `${path}\0${lineNo}`;
|
|
257
|
-
if (seen.has(key)) continue;
|
|
258
|
-
seen.add(key);
|
|
259
|
-
out.push({ path: String(path).replace(/\\/g, '/'), line: lineNo });
|
|
275
|
+
addLine(path, lineNo);
|
|
260
276
|
if (out.length >= GREP_COVERAGE_MAX) break;
|
|
261
277
|
}
|
|
262
278
|
return out.length ? out : null;
|
|
@@ -374,6 +374,11 @@ function sanitizeMcpInstructionText(text, max = MCP_INSTRUCTION_MAX_CHARS) {
|
|
|
374
374
|
/**
|
|
375
375
|
* Per-server MCP initialize instructions for deferred-pool tools only.
|
|
376
376
|
* Empty when no instructions or no matching deferred MCP tools → omit block.
|
|
377
|
+
* Emits ONLY the server heading + instruction body: the per-server tool names
|
|
378
|
+
* are deliberately NOT repeated here — every pool tool is already listed once
|
|
379
|
+
* (with its description) in <available-deferred-tools>, and re-listing ~30
|
|
380
|
+
* names per server doubled the MCP share of the BP1 prefix (2026-08-05 audit).
|
|
381
|
+
* Server membership stays evident from the mcp__<server>__ name prefix.
|
|
377
382
|
*/
|
|
378
383
|
function buildMcpInstructionsManifest(mcpServerInstructions, poolNames) {
|
|
379
384
|
const map = mcpServerInstructions && typeof mcpServerInstructions === 'object'
|
|
@@ -400,8 +405,7 @@ function buildMcpInstructionsManifest(mcpServerInstructions, poolNames) {
|
|
|
400
405
|
for (const server of servers) {
|
|
401
406
|
const safeServer = sanitizeMcpManifestServerName(server);
|
|
402
407
|
const body = sanitizeMcpInstructionText(map[server]);
|
|
403
|
-
|
|
404
|
-
lines.push(`## ${safeServer}`, body, ...tools.map((tool) => `- ${tool}`));
|
|
408
|
+
lines.push(`## ${safeServer}`, body);
|
|
405
409
|
}
|
|
406
410
|
lines.push('</mcp-instructions>');
|
|
407
411
|
return lines.join('\n');
|
|
@@ -18,6 +18,10 @@ const FETCH_TIMEOUT_MS = 4500;
|
|
|
18
18
|
const WARN_TTL_MS = 5 * 60_000;
|
|
19
19
|
const CODEX_RESET_CREDITS_URL = 'https://chatgpt.com/backend-api/wham/rate-limit-reset-credits';
|
|
20
20
|
const CODEX_RESET_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
|
|
21
|
+
// Redeeming a reset credit is an explicit user action, not a poll: it gets the
|
|
22
|
+
// generous budget the orca client uses (REDEEM_BACKEND_TIMEOUT_MS) so a slow
|
|
23
|
+
// backend cannot abort a request the server is already applying.
|
|
24
|
+
const CODEX_REDEEM_TIMEOUT_MS = 30_000;
|
|
21
25
|
|
|
22
26
|
const memoryCache = new Map();
|
|
23
27
|
const inflight = new Map();
|
|
@@ -99,6 +103,38 @@ try {
|
|
|
99
103
|
// Embedded runtimes may not expose process lifecycle hooks.
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
/** Drops every cached usage snapshot of one provider (memory, queued disk
|
|
107
|
+
* writes and the persisted routes). A mutation that changes quota state
|
|
108
|
+
* server-side — redeeming a Codex reset credit — must not keep serving the
|
|
109
|
+
* pre-mutation meters from a 60s/10min cache. */
|
|
110
|
+
export function invalidateOAuthUsageSnapshots(provider) {
|
|
111
|
+
const providerOnly = String(provider || '').toLowerCase();
|
|
112
|
+
if (!providerOnly) return;
|
|
113
|
+
const routePrefix = `${providerOnly}\u0001`;
|
|
114
|
+
const owned = (key) => key === providerOnly || String(key).startsWith(routePrefix);
|
|
115
|
+
for (const key of [...memoryCache.keys()]) {
|
|
116
|
+
if (owned(key)) memoryCache.delete(key);
|
|
117
|
+
}
|
|
118
|
+
for (const key of [...pendingDiskSnapshots.keys()]) {
|
|
119
|
+
if (owned(key)) pendingDiskSnapshots.delete(key);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
updateJsonAtomicSync(cachePath(), (curRaw) => {
|
|
123
|
+
const cur = curRaw && typeof curRaw === 'object' ? curRaw : {};
|
|
124
|
+
const routes = cur.routes && typeof cur.routes === 'object' ? cur.routes : {};
|
|
125
|
+
return {
|
|
126
|
+
version: 1,
|
|
127
|
+
updatedAt: Date.now(),
|
|
128
|
+
routes: Object.fromEntries(
|
|
129
|
+
Object.entries(routes).filter(([key]) => !owned(key)),
|
|
130
|
+
),
|
|
131
|
+
};
|
|
132
|
+
}, { compact: true, fsync: false, fsyncDir: false });
|
|
133
|
+
} catch {
|
|
134
|
+
// Usage display must never break the reset path.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
102
138
|
function isContentfulSnapshot(snapshot) {
|
|
103
139
|
return !!snapshot
|
|
104
140
|
&& typeof snapshot === 'object'
|
|
@@ -253,11 +289,15 @@ function normalizeOpenAICodexResetCredits(data, accountId = '') {
|
|
|
253
289
|
.filter((value) => Number.isFinite(value) && value > 0);
|
|
254
290
|
const nextExpiresAt = resetAtMs(data.next_expires_at ?? data.nextExpiresAt)
|
|
255
291
|
|| (expiryCandidates.length ? Math.min(...expiryCandidates) : null);
|
|
292
|
+
// Identity of the OFFER, not of one payload shape: the detail endpoint and
|
|
293
|
+
// the counts embedded in /wham/usage describe the same credits with
|
|
294
|
+
// different fields, so hashing the raw rows made the same offer produce two
|
|
295
|
+
// revisions — and the desktop scopes its durable idempotency key by
|
|
296
|
+
// revision. Count + soonest expiry is what a user is offered.
|
|
256
297
|
const offerRevision = `v1:${createHash('sha256').update(JSON.stringify({
|
|
257
298
|
accountId,
|
|
258
299
|
availableCount,
|
|
259
300
|
nextExpiresAt,
|
|
260
|
-
credits,
|
|
261
301
|
})).digest('hex')}`;
|
|
262
302
|
return {
|
|
263
303
|
availableCount,
|
|
@@ -290,6 +330,32 @@ function codexResetOutcome(code) {
|
|
|
290
330
|
throw new Error(`Unknown Codex reset outcome: ${cleanString(code) || 'missing'}`);
|
|
291
331
|
}
|
|
292
332
|
|
|
333
|
+
async function postOpenAICodexResetConsume(auth, idempotencyKey) {
|
|
334
|
+
return await fetch(CODEX_RESET_CONSUME_URL, {
|
|
335
|
+
...fetchOptions({
|
|
336
|
+
...codexHeaders(auth),
|
|
337
|
+
'Content-Type': 'application/json',
|
|
338
|
+
}, CODEX_REDEEM_TIMEOUT_MS),
|
|
339
|
+
method: 'POST',
|
|
340
|
+
body: JSON.stringify({ redeem_request_id: idempotencyKey }),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function redeemOpenAICodexResetCredit(auth, idempotencyKey) {
|
|
345
|
+
// A transport failure (abort, dropped socket) leaves the outcome unknown
|
|
346
|
+
// while the credit may already be spent. redeem_request_id makes the request
|
|
347
|
+
// idempotent, so ONE replay turns that unknown into the server's real answer
|
|
348
|
+
// instead of reporting "could not be confirmed" over a consumed credit.
|
|
349
|
+
let response;
|
|
350
|
+
try {
|
|
351
|
+
response = await postOpenAICodexResetConsume(auth, idempotencyKey);
|
|
352
|
+
} catch {
|
|
353
|
+
response = await postOpenAICodexResetConsume(auth, idempotencyKey);
|
|
354
|
+
}
|
|
355
|
+
if (!response.ok) throw new Error(`Codex reset failed: HTTP ${response.status}`);
|
|
356
|
+
return codexResetOutcome((await response.json())?.code);
|
|
357
|
+
}
|
|
358
|
+
|
|
293
359
|
export async function consumeOpenAICodexResetCredit(providerObj, options = {}) {
|
|
294
360
|
const expectedOfferRevision = cleanString(options?.expectedOfferRevision);
|
|
295
361
|
const idempotencyKey = cleanString(options?.idempotencyKey);
|
|
@@ -301,20 +367,14 @@ export async function consumeOpenAICodexResetCredit(providerObj, options = {}) {
|
|
|
301
367
|
}
|
|
302
368
|
const auth = await resolveOpenAICodexAuth(providerObj);
|
|
303
369
|
if (!auth) throw new Error('Codex is not signed in');
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}, 15_000),
|
|
313
|
-
method: 'POST',
|
|
314
|
-
body: JSON.stringify({ redeem_request_id: idempotencyKey }),
|
|
315
|
-
});
|
|
316
|
-
if (!response.ok) throw new Error(`Codex reset failed: HTTP ${response.status}`);
|
|
317
|
-
const outcome = codexResetOutcome((await response.json())?.code);
|
|
370
|
+
// The SERVER decides the outcome (orca parity): redeem_request_id makes the
|
|
371
|
+
// call idempotent and `already_redeemed`/`no_credit` are real answers. The
|
|
372
|
+
// old client-side offer gate ran before every attempt, so retrying an
|
|
373
|
+
// unconfirmed redeem — whose credit was already spent, hence a changed
|
|
374
|
+
// revision — could only ever report "offer changed" and never the truth.
|
|
375
|
+
const outcome = await redeemOpenAICodexResetCredit(auth, idempotencyKey);
|
|
376
|
+
// Quota meters just changed server-side; cached snapshots are now wrong.
|
|
377
|
+
invalidateOAuthUsageSnapshots('openai-oauth');
|
|
318
378
|
const resetCredits = await fetchOpenAICodexResetCreditsWithAuth(auth).catch(() => null);
|
|
319
379
|
return { outcome, resetCredits };
|
|
320
380
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { join, resolve as _pathResolve, isAbsolute as _pathIsAbs, normalize as _pathNorm } from 'node:path';
|
|
6
6
|
import { writeJsonAtomicSync } from '../../../../shared/atomic-file.mjs';
|
|
7
7
|
import { _normalizeCacheKey } from './util.mjs';
|
|
8
|
+
import { GREP_AUTO_CONTEXT_LINES } from '../../tools/builtin/path-utils.mjs';
|
|
8
9
|
|
|
9
10
|
const MAX_PER_SESSION = 100;
|
|
10
11
|
|
|
@@ -43,6 +44,22 @@ function _firstArg(args, names) {
|
|
|
43
44
|
return undefined;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
const _GREP_CONTEXT_GROUPS = [
|
|
48
|
+
['-A', ['-A', 'A', 'after', 'after_context', 'afterContext', '--after-context', 'after-context', 'afterLines', 'after_lines']],
|
|
49
|
+
['-B', ['-B', 'B', 'before', 'before_context', 'beforeContext', '--before-context', 'before-context', 'beforeLines', 'before_lines']],
|
|
50
|
+
['context', ['context', '-C', 'C', 'context_lines', 'contextLines', '--context', 'contextN', 'around', 'surrounding']],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function _canonicalizeGrepContextArgs(args) {
|
|
54
|
+
for (const [canonical, aliases] of _GREP_CONTEXT_GROUPS) {
|
|
55
|
+
const value = _firstArg(args, aliases);
|
|
56
|
+
for (const alias of aliases) delete args[alias];
|
|
57
|
+
if (value === undefined) continue;
|
|
58
|
+
const numeric = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
59
|
+
args[canonical] = Number.isFinite(numeric) && Number.isInteger(numeric) ? numeric : value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
46
63
|
function _canonicalToolArgs(toolName, args) {
|
|
47
64
|
if (!args || typeof args !== 'object') return args;
|
|
48
65
|
const next = { ...args };
|
|
@@ -59,12 +76,35 @@ function _canonicalToolArgs(toolName, args) {
|
|
|
59
76
|
const alias = _firstArg(next, ['root', 'directory', 'dir']);
|
|
60
77
|
if (alias !== undefined) next.path = alias;
|
|
61
78
|
}
|
|
79
|
+
_canonicalizeGrepContextArgs(next);
|
|
62
80
|
if ((next.output_mode === undefined || next.output_mode === null || next.output_mode === '') && typeof next.mode === 'string') {
|
|
63
81
|
const mode = next.mode.trim();
|
|
64
|
-
if (['files_with_matches', 'content', 'count'].includes(mode)) next.output_mode = mode;
|
|
82
|
+
if (['files_with_matches', 'content', 'content_with_context', 'count'].includes(mode)) next.output_mode = mode;
|
|
65
83
|
}
|
|
66
84
|
for (const k of ['query', 'regex', 'regexp', 'needle', 'search', 'literal', 'file_pattern', 'filePattern', 'include', 'includes', 'files', 'root', 'directory', 'dir']) delete next[k];
|
|
67
|
-
|
|
85
|
+
delete next.mode;
|
|
86
|
+
|
|
87
|
+
// Canonicalize by execution semantics, not caller spelling:
|
|
88
|
+
// omitted/content_with_context => content + automatic 25-line context;
|
|
89
|
+
// context:0 => bare content; context flags are ignored in count/files.
|
|
90
|
+
const requestedMode = typeof next.output_mode === 'string' ? next.output_mode.trim() : '';
|
|
91
|
+
if (requestedMode === 'files_with_matches' || requestedMode === 'count') {
|
|
92
|
+
next.output_mode = requestedMode;
|
|
93
|
+
delete next['-A'];
|
|
94
|
+
delete next['-B'];
|
|
95
|
+
delete next.context;
|
|
96
|
+
} else {
|
|
97
|
+
const hasExplicitContext = ['-A', '-B', 'context']
|
|
98
|
+
.some((key) => Object.prototype.hasOwnProperty.call(next, key));
|
|
99
|
+
next.output_mode = 'content';
|
|
100
|
+
if ((requestedMode === '' || requestedMode === 'content_with_context') && !hasExplicitContext) {
|
|
101
|
+
next.context = GREP_AUTO_CONTEXT_LINES;
|
|
102
|
+
}
|
|
103
|
+
if (next.context === 0 && !Object.prototype.hasOwnProperty.call(next, '-A')
|
|
104
|
+
&& !Object.prototype.hasOwnProperty.call(next, '-B')) {
|
|
105
|
+
delete next.context;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
68
108
|
} else if (toolName === 'glob') {
|
|
69
109
|
if (next.pattern === undefined || next.pattern === null || next.pattern === '') {
|
|
70
110
|
const alias = _firstArg(next, ['glob', 'file_pattern', 'filePattern', 'name', 'include', 'includes', 'files']);
|