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.
Files changed (52) hide show
  1. package/package.json +1 -1
  2. package/src/rules/agent/30-explorer.md +22 -16
  3. package/src/rules/shared/01-tool.md +23 -18
  4. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +22 -6
  5. package/src/runtime/agent/orchestrator/context/collect.mjs +6 -2
  6. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
  7. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
  8. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +100 -86
  9. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
  10. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
  11. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +3 -3
  12. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +4 -1
  13. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
  14. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +12 -3
  15. package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +22 -0
  16. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +491 -0
  17. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +91 -13
  18. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +90 -27
  19. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +6 -1
  20. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
  21. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
  22. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +7 -2
  23. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +32 -4
  24. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +16 -1
  25. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +543 -19
  26. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +5 -2
  27. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
  28. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +48 -0
  29. package/src/runtime/agent/orchestrator/tools/builtin.mjs +71 -1
  30. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +4 -2
  31. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +51 -2
  32. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +6 -17
  33. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -4
  34. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +3 -3
  35. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +3 -3
  36. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
  37. package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
  38. package/src/runtime/shared/background-tasks.mjs +10 -3
  39. package/src/runtime/shared/child-spawn-gate.mjs +50 -26
  40. package/src/runtime/shared/task-notification-envelope.mjs +11 -2
  41. package/src/runtime/shared/tool-card-model.mjs +6 -2
  42. package/src/runtime/shared/tool-surface.mjs +7 -2
  43. package/src/session-runtime/provider-usage.mjs +26 -2
  44. package/src/standalone/explore-tool.mjs +1 -1
  45. package/src/tui/app/use-transcript-window.mjs +7 -1
  46. package/src/tui/components/Spinner.jsx +18 -9
  47. package/src/tui/dist/index.mjs +111 -25
  48. package/src/tui/engine/live-share.mjs +23 -3
  49. package/src/tui/engine/session-api.mjs +7 -0
  50. package/src/tui/engine/turn.mjs +77 -4
  51. package/src/tui/engine.mjs +14 -6
  52. package/src/tui/index.jsx +7 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.93",
3
+ "version": "0.9.94",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -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. Split broad/uncertain input into every
13
- known facet and send one batch under the shared one-route contract. Use
14
- `pattern[]` with 4–8 code-token variants for concept facets, `code_graph`
15
- `symbol_search` for symbol facets, and `find` `query[]` for unknown/broad
16
- targets or unverified path/name fragments. For a symptom/behavior query, add
17
- the upstream producer/derivation layer of the reported surface as extra facets
18
- in the SAME batch (more `pattern[]` variants or `code_graph` `symbol_search`),
19
- never as a later turn. Follow-up turns batch every unresolved facet in
20
- parallel; a single-tool turn is allowed only when exactly one
21
- pre-anchor/zero-hit facet remains.
22
-
23
- For broad grep use `output_mode:"files_with_matches"`. Use
24
- `content_with_context` with `head_limit` only on paths returned this session.
25
- Each pattern is one identifier, camel/snake variant, or concept synonym; never
26
- a prose phrase. Spaces and non-ASCII are allowed only in verbatim quoted
27
- error/log literals. Translate other non-English queries to English identifiers.
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, and failure checks — in one bounded tool message. Use one shortest
5
- route per facet: broad/uncertain→`explore` (roles without it: `find`);
6
- partial path/name→`find`; verified root+wildcard→`glob`;
7
- quoted/non-identifier literal or regex→`grep`; exact code
8
- identifier/relation→`code_graph` before grep; known file/span→`read`
9
- directly without `grep`; verified directory→`list`; known
10
- edit→`apply_patch`; program/state change→`shell`; web/current external
11
- info→`search`.
12
- - Shortest total calls, maximum batching every turn: every determined call
13
- in one concurrent message (mix `shell` in), merged per tool one `shell`
14
- chain, one `read`, one `apply_patch` carrying full verification in
15
- `post_shell`; a later turn only for steps needing unseen output.
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 themso
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 act on it (inspecting it
21
- via read/code_graph is valid); only zero/error results justify changed
22
- tokens or scope. Don't re-locate, re-verify, or reread returned spans.
23
- - Verify changes in proportion to risk with one decisive batched boundary
24
- probe. A pass is final; on failure, fix and rerun only what failed. Keep
25
- optional diagnostics non-fatal; report verified vs assumed.
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
- if (!path || !Number.isInteger(lineNo) || lineNo < 1) continue;
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
- const tools = [...toolsByServer.get(server)].sort((a, b) => a.localeCompare(b));
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
- const current = await fetchOpenAICodexResetCreditsWithAuth(auth);
305
- if (!current || current.availableCount < 1 || current.offerRevision !== expectedOfferRevision) {
306
- return { status: 'offerChanged', resetCredits: current };
307
- }
308
- const response = await fetch(CODEX_RESET_CONSUME_URL, {
309
- ...fetchOptions({
310
- ...codexHeaders(auth),
311
- 'Content-Type': 'application/json',
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
- if (next.output_mode && next.mode === next.output_mode) delete next.mode;
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']);