token-goat 2.8.3 → 2.8.6

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.
@@ -23,7 +23,7 @@ import {
23
23
  summarizeOutputDelta,
24
24
  summarizeResidentContext,
25
25
  taskListPruneHint
26
- } from "./token-goat-chunk-4HIMCBYK.mjs";
26
+ } from "./token-goat-chunk-TELKICYU.mjs";
27
27
  import {
28
28
  BODY_FIRST_TOOL_RESPONSE_KEYS,
29
29
  OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
@@ -42,14 +42,14 @@ import {
42
42
  countSymbols,
43
43
  denyOutput,
44
44
  emitRewrite,
45
+ emitRewriteIfChanged,
45
46
  enqueueDirtyPathSafe,
46
47
  ensureWorkerAlive,
47
48
  estimateTokens,
48
- estimateTokensFromLength,
49
49
  extractCompactFromMarker,
50
50
  extractToolResponseField,
51
51
  extractToolResultText,
52
- fenceUntrustedContent,
52
+ fenceWithMatches,
53
53
  formatProjectMap,
54
54
  formatShrinkSummary,
55
55
  getBashOutputId,
@@ -89,6 +89,7 @@ import {
89
89
  matchesDenyPattern,
90
90
  meetsSavingsFloor,
91
91
  passOutput,
92
+ probeImageMeta,
92
93
  recordBashOutput,
93
94
  recordBashRerun,
94
95
  recordCliRead,
@@ -108,7 +109,7 @@ import {
108
109
  removeOutstandingAgentSpawn,
109
110
  runHook,
110
111
  saveSessionState,
111
- scanForInjectionPatterns,
112
+ scanAndRecord,
112
113
  serializeOutput,
113
114
  sessionOutputBodyBytes,
114
115
  sessionSidecarPath,
@@ -117,10 +118,12 @@ import {
117
118
  storeBlob,
118
119
  storeOutput,
119
120
  takePendingLargeFileHint,
121
+ visionTokens,
122
+ visionTokensSaved,
120
123
  wasCliReadThisSession,
121
124
  wasFileReadThisSession,
122
125
  wasHintShown
123
- } from "./token-goat-chunk-PWVXXPCC.mjs";
126
+ } from "./token-goat-chunk-DK4VLLYB.mjs";
124
127
  import {
125
128
  canRunWrappedShell,
126
129
  compressOutput,
@@ -131,7 +134,7 @@ import {
131
134
  isRewriteWorthwhile,
132
135
  resolveMinNetSavingsBytes,
133
136
  shlexSplit
134
- } from "./token-goat-chunk-EFF2XCLB.mjs";
137
+ } from "./token-goat-chunk-GUNYAGOZ.mjs";
135
138
  import {
136
139
  PER_FILE_COUNTERFACTUAL_CEILING,
137
140
  VERSION,
@@ -150,9 +153,10 @@ import {
150
153
  redactSecrets,
151
154
  resolveIndexPath,
152
155
  runGit,
156
+ savedTokensFromBytes,
153
157
  shortFingerprint,
154
158
  toKB
155
- } from "./token-goat-chunk-6ODZ6PZK.mjs";
159
+ } from "./token-goat-chunk-2JZ66BBE.mjs";
156
160
 
157
161
  // src/hooks_grep.ts
158
162
  function grepIntInput(toolInput, key) {
@@ -167,7 +171,7 @@ function grepIntInput(toolInput, key) {
167
171
  function grepSignature(toolInput) {
168
172
  const pattern = toolInput["pattern"];
169
173
  if (typeof pattern !== "string" || pattern === "") return null;
170
- const path2 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
174
+ const path3 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
171
175
  const outputMode = typeof toolInput["output_mode"] === "string" ? toolInput["output_mode"] : "files_with_matches";
172
176
  const glob = typeof toolInput["glob"] === "string" ? toolInput["glob"] : "";
173
177
  const type = typeof toolInput["type"] === "string" ? toolInput["type"] : "";
@@ -182,7 +186,7 @@ function grepSignature(toolInput) {
182
186
  const offset = grepIntInput(toolInput, "offset");
183
187
  return JSON.stringify([
184
188
  pattern,
185
- path2,
189
+ path3,
186
190
  outputMode,
187
191
  glob,
188
192
  type,
@@ -218,8 +222,7 @@ function foldGrepContentHandler(event) {
218
222
  if (toolInput["-n"] === false) return passOutput();
219
223
  const rawText = extractToolResponseField(event.raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
220
224
  if (!rawText) return passOutput();
221
- const redacted = redactSecrets(rawText);
222
- const text = redacted.text;
225
+ const text = redactSecrets(rawText).text;
223
226
  const rawLines = text.split(/\r\n|\r|\n/);
224
227
  const parsed = [];
225
228
  for (const line of rawLines) {
@@ -260,10 +263,7 @@ function foldGrepContentHandler(event) {
260
263
  })) {
261
264
  return passOutput();
262
265
  }
263
- const bytesDelta = originalBytes - rewrittenBytes;
264
- recordStat("grep:fold", bytesDelta, Math.round(bytesDelta / 4));
265
- if (redacted.count > 0) recordStat("secret_redacted", 0, redacted.count, void 0, "grep");
266
- return { hookType: "rewriteOutput", updatedOutput: rewritten };
266
+ return emitRewrite(rewritten, "grep", { kind: "grep:fold", originalBytes });
267
267
  } catch {
268
268
  return passOutput();
269
269
  }
@@ -281,8 +281,8 @@ registerHook("post_tool_use", postGrepHandler, { toolName: "Grep" });
281
281
  function globSignature(toolInput) {
282
282
  const pattern = toolInput["pattern"];
283
283
  if (typeof pattern !== "string" || pattern === "") return null;
284
- const path2 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
285
- return JSON.stringify([pattern, path2]);
284
+ const path3 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
285
+ return JSON.stringify([pattern, path3]);
286
286
  }
287
287
  var { post: postGlobHandler, pre: preGlobDedupHandler } = makeDedupHintHandlers({
288
288
  toolName: "Glob",
@@ -430,6 +430,9 @@ function renderReadRow(entry) {
430
430
  const edited = entry.wasEdited ? ", edited" : "";
431
431
  return `- ${entry.path} (${kb}kb, ${entry.readCount} ${plural}${edited})`;
432
432
  }
433
+ function renderSymbolReadRow(entry) {
434
+ return `- ${entry.path} (symbols: ${(entry.symbols_read ?? []).join(", ")})`;
435
+ }
433
436
  function mergeManifestFiles(parent, siblingFiles) {
434
437
  const byPath = /* @__PURE__ */ new Map();
435
438
  for (const f of parent) byPath.set(foldPath(f.path), f);
@@ -457,6 +460,7 @@ function buildManifest(sessionId, cwd) {
457
460
  const files = siblingFiles.length > 0 ? mergeManifestFiles(ownFiles, siblingFiles) : ownFiles;
458
461
  const editedFiles = files.filter((f) => f.wasEdited);
459
462
  const readFiles = files.filter((f) => f.readCount > 0 && !f.wasEdited);
463
+ const symbolOnlyFiles = files.filter((f) => f.readCount === 0 && !f.wasEdited && (f.symbols_read?.length ?? 0) > 0);
460
464
  const webFetches = [...getSessionWebFetches().entries()];
461
465
  const lines = [];
462
466
  lines.push("## Session context");
@@ -469,6 +473,7 @@ function buildManifest(sessionId, cwd) {
469
473
  editedFiles.map((entry) => `- ${entry.path}`),
470
474
  MAX_ROWS
471
475
  );
476
+ appendCappedSection(lines, "### Surgically read files (symbol/section reads, never read whole)", symbolOnlyFiles.map(renderSymbolReadRow), MAX_ROWS);
472
477
  appendCappedSection(
473
478
  lines,
474
479
  "### Web URLs fetched",
@@ -613,7 +618,7 @@ function postCompactHandler(event) {
613
618
  0,
614
619
  0,
615
620
  void 0,
616
- `trigger=${trigger} bytes=${bytes} est_tokens=${estimateTokensFromLength(summary.length)} manifest_paths=${survived}/${sample.length}`
621
+ `trigger=${trigger} bytes=${bytes} est_tokens=${savedTokensFromBytes(bytes)} manifest_paths=${survived}/${sample.length}`
617
622
  );
618
623
  return passOutput();
619
624
  }
@@ -626,7 +631,7 @@ import crypto from "node:crypto";
626
631
  var TRACKED_SKILL = "token-goat";
627
632
  var MAX_COMMANDS_SHOWN = 8;
628
633
  async function currentCommandNames() {
629
- const { buildProgram } = await import("./token-goat-chunk-4OM2Q2SX.mjs");
634
+ const { buildProgram } = await import("./token-goat-chunk-SHN4UTL4.mjs");
630
635
  return flattenCommandNames(buildCommandManifest(buildProgram()));
631
636
  }
632
637
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -1470,11 +1475,11 @@ function mapInner(r, f) {
1470
1475
  function mapOuter(r, f) {
1471
1476
  return r.matched ? f(r) : r;
1472
1477
  }
1473
- function ab(pa, pb, join) {
1474
- return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join(ma.value, vb, data, i, j)));
1478
+ function ab(pa, pb, join2) {
1479
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join2(ma.value, vb, data, i, j)));
1475
1480
  }
1476
- function abc(pa, pb, pc, join) {
1477
- return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join(ma.value, mb.value, vc, data, i, j))));
1481
+ function abc(pa, pb, pc, join2) {
1482
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join2(ma.value, mb.value, vc, data, i, j))));
1478
1483
  }
1479
1484
  function ahead(p) {
1480
1485
  return (data, i) => mapOuter(p(data, i), (m1) => ({
@@ -5391,8 +5396,8 @@ function trimCharacterEnd(str, char) {
5391
5396
  function unicodeEscape(str) {
5392
5397
  return str.replace(/[\s\S]/g, (c) => "\\u" + c.charCodeAt().toString(16).padStart(4, "0"));
5393
5398
  }
5394
- function get(obj, path2) {
5395
- for (const key of path2) {
5399
+ function get(obj, path3) {
5400
+ for (const key of path3) {
5396
5401
  if (!obj) {
5397
5402
  return void 0;
5398
5403
  }
@@ -6593,8 +6598,8 @@ function withBrackets(str, brackets) {
6593
6598
  const rbr = typeof brackets[1] === "string" ? brackets[1] : "]";
6594
6599
  return lbr + str + rbr;
6595
6600
  }
6596
- function pathRewrite(path2, rewriter, baseUrl, metadata, elem) {
6597
- const modifiedPath = typeof rewriter === "function" ? rewriter(path2, metadata, elem) : path2;
6601
+ function pathRewrite(path3, rewriter, baseUrl, metadata, elem) {
6602
+ const modifiedPath = typeof rewriter === "function" ? rewriter(path3, metadata, elem) : path3;
6598
6603
  return modifiedPath[0] === "/" && baseUrl ? trimCharacterEnd(baseUrl, "/") + modifiedPath : modifiedPath;
6599
6604
  }
6600
6605
  function formatImage(elem, walk, builder, formatOptions) {
@@ -6918,9 +6923,9 @@ function handleDeprecatedOptions(options) {
6918
6923
  options.selectors.push(...tagDefinitions);
6919
6924
  options.selectors = mergeDuplicatesPreferLast(options.selectors, ((s) => s.selector));
6920
6925
  }
6921
- function set(obj, path2, value) {
6922
- const valueKey = path2.pop();
6923
- for (const key of path2) {
6926
+ function set(obj, path3, value) {
6927
+ const valueKey = path3.pop();
6928
+ for (const key of path3) {
6924
6929
  let nested = obj[key];
6925
6930
  if (!nested) {
6926
6931
  nested = {};
@@ -7011,7 +7016,7 @@ function preFetchHandler(event) {
7011
7016
  if (cached !== null) {
7012
7017
  const cachedBytes = Buffer.byteLength(cached, "utf-8");
7013
7018
  if (cachedBytes >= loadConfig().hints.web_dedup_min_bytes) {
7014
- recordStat("webfetch:recall", cachedBytes, Math.round(cachedBytes / 4));
7019
+ recordStat("webfetch:recall", cachedBytes, savedTokensFromBytes(cachedBytes));
7015
7020
  return denyOutput(
7016
7021
  "Already fetched this URL with this prompt; the response is cached. Use `token-goat web-output " + cacheId + "` to recall it (append `--grep PATTERN` to filter or `--section Heading` for a markdown section) instead of re-fetching."
7017
7022
  );
@@ -7031,29 +7036,13 @@ function postFetchHandler(event) {
7031
7036
  const { toolInput, url } = urlCtx;
7032
7037
  recordStat("web_fetch");
7033
7038
  const body = extractToolResponse(event.raw);
7034
- const injCfg = loadConfig().injection;
7035
- const injectionMatches = injCfg.enabled && body ? scanForInjectionPatterns(body) : [];
7036
- if (injectionMatches.length > 0) {
7037
- recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
7038
- }
7039
+ const injectionMatches = body ? scanAndRecord(body) : [];
7039
7040
  const bodyRedacted = body ? redactSecrets(body) : { text: body, count: 0 };
7040
7041
  if (!event.sessionId) {
7041
- if (injectionMatches.length > 0) {
7042
- return emitRewrite(fenceUntrustedContent(bodyRedacted.text, injectionMatches), "fetch");
7043
- }
7044
- if (bodyRedacted.count > 0) {
7045
- return emitRewrite(bodyRedacted.text, "fetch");
7046
- }
7047
- return passOutput();
7042
+ return emitRewriteIfChanged(body, fenceWithMatches(bodyRedacted.text, injectionMatches), "fetch");
7048
7043
  }
7049
7044
  if (!body || body.length < 1024) {
7050
- if (injectionMatches.length > 0) {
7051
- return emitRewrite(fenceUntrustedContent(bodyRedacted.text, injectionMatches), "fetch");
7052
- }
7053
- if (bodyRedacted.count > 0) {
7054
- return emitRewrite(bodyRedacted.text, "fetch");
7055
- }
7056
- return passOutput();
7045
+ return emitRewriteIfChanged(body, fenceWithMatches(bodyRedacted.text, injectionMatches), "fetch");
7057
7046
  }
7058
7047
  const prompt = typeof toolInput["prompt"] === "string" ? toolInput["prompt"] : "";
7059
7048
  let storedBody = body;
@@ -7068,31 +7057,24 @@ function postFetchHandler(event) {
7068
7057
  const cacheId = storeWebOutput(url, storedBody, `${url}\0${prompt}`, storedBody !== body ? body : void 0);
7069
7058
  recordWebFetch(url, prompt, cacheId);
7070
7059
  const storedRedacted = storedBody === body ? bodyRedacted : redactSecrets(storedBody);
7071
- if (injectionMatches.length > 0) {
7072
- return emitRewrite(fenceUntrustedContent(storedRedacted.text, injectionMatches), "fetch");
7073
- }
7060
+ const fencedStored = fenceWithMatches(storedRedacted.text, injectionMatches);
7074
7061
  if (storedBody !== body) {
7075
7062
  const noticeFor = (id) => `
7076
7063
  [token-goat: WebFetch body compressed via extractCleanText; use \`token-goat web-output ${id} --raw\` to recall it]`;
7077
7064
  const notice = noticeFor(cacheId);
7078
7065
  const noticeBytes = Buffer.byteLength(notice, "utf-8");
7079
7066
  const originalBytes = Buffer.byteLength(body, "utf-8");
7080
- const rewrittenBytes = Buffer.byteLength(storedBody, "utf-8");
7067
+ const rewrittenBytes = Buffer.byteLength(fencedStored, "utf-8");
7081
7068
  if (isRewriteWorthwhile({
7082
7069
  originalBytes,
7083
7070
  rewrittenBytes,
7084
7071
  noticeBytes,
7085
7072
  minNetSavingsBytes: resolveMinNetSavingsBytes()
7086
7073
  })) {
7087
- const bytesDelta = originalBytes - rewrittenBytes;
7088
- recordStat("webfetch:compress", bytesDelta, Math.round(bytesDelta / 4));
7089
- return emitRewrite(storedRedacted.text + notice, "fetch");
7074
+ return emitRewrite(fencedStored + notice, "fetch", { kind: "webfetch:compress", originalBytes });
7090
7075
  }
7091
7076
  }
7092
- if (storedRedacted.count > 0) {
7093
- return emitRewrite(storedRedacted.text, "fetch");
7094
- }
7095
- return passOutput();
7077
+ return emitRewriteIfChanged(body, fencedStored, "fetch");
7096
7078
  } catch {
7097
7079
  return passOutput();
7098
7080
  }
@@ -7146,7 +7128,7 @@ async function preSkillHandler(event) {
7146
7128
  if (await hasSessionOutput(event.sessionId, skillName)) {
7147
7129
  const cachedBytes = await sessionOutputBodyBytes(event.sessionId, skillName);
7148
7130
  const denyCredit = cachedBytes !== null ? Math.min(cachedBytes, PER_FILE_COUNTERFACTUAL_CEILING) : 0;
7149
- recordStat("session_hint", denyCredit, Math.round(denyCredit / 4));
7131
+ recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit));
7150
7132
  return denyOutput(
7151
7133
  "Skill `" + skillName + "` was already loaded this session and is cached. Use `token-goat skill-body " + skillName + " --compact` to recall the compact slice (or `token-goat skill-body " + skillName + "` for the full body) instead of re-loading it."
7152
7134
  );
@@ -7161,7 +7143,7 @@ async function preSkillHandler(event) {
7161
7143
  const compactBytes = Buffer.byteLength(compact, "utf-8");
7162
7144
  if (compactBytes * 2 <= bodyBytes && compactBytes <= COMPACT_INLINE_MAX_BYTES) {
7163
7145
  const savedBytes = bodyBytes - compactBytes;
7164
- recordStat("skill_compact_inlined", savedBytes, Math.round(savedBytes / 4));
7146
+ recordStat("skill_compact_inlined", savedBytes, savedTokensFromBytes(savedBytes));
7165
7147
  return denyOutput(
7166
7148
  "Skill `" + skillName + "` is large (" + bodyBytes + " bytes); its compact slice (" + compactBytes + " bytes) is inlined below instead of the full body. Run `token-goat skill-body " + skillName + "` if you need the full body.\n\n" + compact
7167
7149
  );
@@ -7485,16 +7467,16 @@ function isTempPath(fp) {
7485
7467
  return /^\/tmp\//i.test(norm) || /\/var\/folders\//i.test(norm) || /AppData\/Local\/Temp\//i.test(norm) || norm.startsWith("/c/Users/") && norm.includes("/AppData/Local/Temp/") || isUnderSystemTemp(fp);
7486
7468
  }
7487
7469
  function isOrchestratorStateFile(filePath) {
7488
- const basename2 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
7489
- return /^\.improve-state-/.test(basename2);
7470
+ const basename3 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
7471
+ return /^\.improve-state-/.test(basename3);
7490
7472
  }
7491
7473
  function extractCatSourceFile(cmd) {
7492
7474
  const m = /^cat\s+(\S+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|css|scss|sass|less))\s*$/.exec(cmd);
7493
7475
  return m?.[1] ?? null;
7494
7476
  }
7495
7477
  function classifyFileExtensions(filePath) {
7496
- const basename2 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
7497
- const isEnvFile = /^\.env(\.\w+)?$/i.test(basename2);
7478
+ const basename3 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
7479
+ const isEnvFile = /^\.env(\.\w+)?$/i.test(basename3);
7498
7480
  const hasKnownExt = /\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|css|scss|sass|less|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties|sql|ps1|psm1|env)$/i.test(filePath);
7499
7481
  if (!hasKnownExt && !isEnvFile) return null;
7500
7482
  const isSql = /\.sql$/i.test(filePath);
@@ -7516,12 +7498,14 @@ function classifyCatPath(filePath, cmd0) {
7516
7498
  return { filePath, ...flags, cmd0 };
7517
7499
  }
7518
7500
  function extractCatFile(cmd) {
7519
- const m = /^(cat|bat|type|Get-Content|gc)(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+-[a-zA-Z].*)?\s*$/i.exec(cmd);
7501
+ const m = /^(cat|bat|type|Get-Content|gc)(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+-[a-zA-Z].*?)?(?:\s+2>(&1|\/dev\/null))?\s*$/i.exec(cmd);
7520
7502
  if (!m) return null;
7521
7503
  const cmd0 = m[1];
7522
7504
  const filePath = m[2] ?? m[3] ?? m[4];
7523
7505
  if (filePath === void 0) return null;
7524
- return classifyCatPath(filePath, cmd0);
7506
+ const r = classifyCatPath(filePath, cmd0);
7507
+ if (r === null) return null;
7508
+ return { ...r, advisoryOnly: m[5] === "/dev/null" };
7525
7509
  }
7526
7510
  function extractCatFilesMulti(cmd) {
7527
7511
  if (/[|<>;&`]/.test(cmd) || cmd.includes("$(")) return null;
@@ -7769,11 +7753,12 @@ function extractPythonFileRead(cmd) {
7769
7753
  return null;
7770
7754
  }
7771
7755
  function extractHeadFile(cmd) {
7772
- const m = /^head(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
7773
- if (!m) return null;
7774
- const n = parseInt(m[1] ?? m[2] ?? "0", 10);
7756
+ const direct = /^head(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
7757
+ const piped = direct === null ? /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+2>(?:&1|\/dev\/null))?\s*\|\s*head(?:\s+-n\s+(\d+)|\s+-(\d+))?\s*$/.exec(cmd) : null;
7758
+ if (direct === null && piped === null) return null;
7759
+ const n = parseInt((direct !== null ? direct[1] ?? direct[2] : piped[4] ?? piped[5]) ?? "0", 10);
7775
7760
  if (n <= 10) return null;
7776
- const filePath = m[3] ?? m[4] ?? m[5];
7761
+ const filePath = direct !== null ? direct[3] ?? direct[4] ?? direct[5] : piped[1] ?? piped[2] ?? piped[3];
7777
7762
  if (filePath === void 0) return null;
7778
7763
  if (isTempPath(filePath)) return null;
7779
7764
  if (!/\.(?:ts|tsx|js|jsx|py|go|java|rs|rb|cs|md|mdx|rst|txt|json|yaml|yml|toml|sql|sh)$/i.test(filePath)) return null;
@@ -7781,12 +7766,12 @@ function extractHeadFile(cmd) {
7781
7766
  return { filePath, isDoc, isConfig, isSql, n };
7782
7767
  }
7783
7768
  function extractSedRange(cmd) {
7784
- const m = /^sed\s+-n\s+['"](?:\d+,\d+p)(?:;\d+,\d+p)*['"]\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>\/dev\/null)?\s*$/.exec(cmd);
7769
+ const m = /^sed\s+-n\s+(?:['"]((?:\d+,\d+p)(?:;\d+,\d+p)*)['"]|(\d+,\d+p))\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>(?:\/dev\/null|&1))?\s*$/.exec(cmd);
7785
7770
  if (!m) return null;
7786
- const quotedAddress = /['"]([^'"]+)['"]/.exec(cmd);
7787
- if (!quotedAddress) return null;
7771
+ const addressList = m[1] ?? m[2];
7772
+ if (addressList === void 0) return null;
7788
7773
  const ranges = [];
7789
- for (const clause of quotedAddress[1].split(";")) {
7774
+ for (const clause of addressList.split(";")) {
7790
7775
  const cm = /^(\d+),(\d+)p$/.exec(clause ?? "");
7791
7776
  if (!cm) return null;
7792
7777
  const start = parseInt(cm[1], 10);
@@ -7795,19 +7780,21 @@ function extractSedRange(cmd) {
7795
7780
  ranges.push([start, end]);
7796
7781
  }
7797
7782
  if (ranges.length === 0) return null;
7798
- const filePath = m[1] ?? m[2] ?? m[3];
7783
+ const filePath = m[3] ?? m[4] ?? m[5];
7799
7784
  if (filePath === void 0) return null;
7800
7785
  if (isTempPath(filePath)) return null;
7801
7786
  return { filePath, ranges };
7802
7787
  }
7803
7788
  function extractAwkRange(cmd) {
7804
- const m = /^awk\s+(?:'([^']+)'|"([^"]+)")\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>\/dev\/null)?\s*$/.exec(cmd);
7789
+ const m = /^awk\s+(?:'([^']+)'|"([^"]+)")\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>(?:\/dev\/null|&1))?\s*$/.exec(cmd);
7805
7790
  if (!m) return null;
7806
7791
  const program = m[1] ?? m[2];
7807
7792
  if (program === void 0) return null;
7808
- const cmp = /^\s*NR\s*>=\s*(\d+)\s*&&\s*NR\s*<=\s*(\d+)\s*$/.exec(program);
7809
- const rng = /^\s*NR\s*==\s*(\d+)\s*,\s*NR\s*==\s*(\d+)\s*$/.exec(program);
7793
+ const cmp = /^\s*NR\s*>=\s*(\d+)\s*&&\s*NR\s*<=\s*(\d+)\s*(\{.*\})?\s*$/.exec(program);
7794
+ const rng = /^\s*NR\s*==\s*(\d+)\s*,\s*NR\s*==\s*(\d+)\s*(\{.*\})?\s*$/.exec(program);
7810
7795
  const hit = cmp ?? rng;
7796
+ const action = hit?.[3];
7797
+ if (action !== void 0 && !/^\{\s*(?:print(?:\s+NR\s*"[^"%]*"\s*\$0|\s+\$0)?|printf\s*"%d[^%"]*%s\\n"\s*,\s*NR\s*,\s*\$0)\s*\}$/.test(action)) return null;
7811
7798
  if (!hit) return null;
7812
7799
  const start = parseInt(hit[1], 10);
7813
7800
  const end = parseInt(hit[2], 10);
@@ -7817,13 +7804,47 @@ function extractAwkRange(cmd) {
7817
7804
  if (isTempPath(filePath)) return null;
7818
7805
  return { filePath, ranges: [[start, end]] };
7819
7806
  }
7807
+ function normalizeCatSedPipe(cmd) {
7808
+ const m = /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+2>(?:&1|\/dev\/null))?\s*\|\s*sed\s+-n\s+('[^']+'|"[^"]+"|\d+,\d+p)\s*$/.exec(cmd);
7809
+ if (!m) return null;
7810
+ const filePath = m[1] ?? m[2] ?? m[3];
7811
+ if (filePath === void 0 || filePath.includes('"')) return null;
7812
+ return "sed -n " + m[4] + ' "' + filePath + '"';
7813
+ }
7820
7814
  function extractLineRangeRead(cmd) {
7815
+ const catSed = normalizeCatSedPipe(cmd);
7816
+ if (catSed !== null) cmd = catSed;
7821
7817
  const sed = extractSedRange(cmd);
7822
7818
  if (sed !== null) return { ...sed, tool: "sed" };
7823
7819
  const awk = extractAwkRange(cmd);
7824
7820
  if (awk !== null) return { ...awk, tool: "awk" };
7825
7821
  return null;
7826
7822
  }
7823
+ var FORMATTING_STAGE_RE = /^(?:fold|cut|cat|nl|head|tail)(?:\s+(?:-\S+|[\d,+-]+))*\s*$/;
7824
+ function extractLineRangeReadsCompound(cmd) {
7825
+ const cleaned = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
7826
+ const segments = splitShellSegments(cleaned);
7827
+ if (segments.length < 2) return null;
7828
+ const reads = [];
7829
+ for (const seg of segments) {
7830
+ const r = extractLineRangeRead(seg);
7831
+ if (r !== null) {
7832
+ reads.push(r);
7833
+ continue;
7834
+ }
7835
+ if (/^echo\b[^<>]*$/.test(seg)) continue;
7836
+ if (FORMATTING_STAGE_RE.test(seg)) continue;
7837
+ return null;
7838
+ }
7839
+ if (reads.length === 0) return null;
7840
+ const merged = /* @__PURE__ */ new Map();
7841
+ for (const r of reads) {
7842
+ const prev = merged.get(r.filePath);
7843
+ if (prev !== void 0) prev.ranges.push(...r.ranges);
7844
+ else merged.set(r.filePath, { filePath: r.filePath, ranges: [...r.ranges], tool: r.tool });
7845
+ }
7846
+ return [...merged.values()];
7847
+ }
7827
7848
  var SYMBOL_BEARING_LANGUAGES = /* @__PURE__ */ new Set([
7828
7849
  "python",
7829
7850
  "typescript",
@@ -7923,11 +7944,12 @@ function extractTailFile(cmd) {
7923
7944
  if (/-f\b/.test(cmd)) return null;
7924
7945
  if (/-c\b/.test(cmd)) return null;
7925
7946
  if (/-n\s*\+/.test(cmd)) return null;
7926
- const m = /^tail(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
7927
- if (!m) return null;
7928
- const n = parseInt(m[1] ?? m[2] ?? "0", 10);
7947
+ const direct = /^tail(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
7948
+ const piped = direct === null ? /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+2>(?:&1|\/dev\/null))?\s*\|\s*tail(?:\s+-n\s+(\d+)|\s+-(\d+))?\s*$/.exec(cmd) : null;
7949
+ if (direct === null && piped === null) return null;
7950
+ const n = parseInt((direct !== null ? direct[1] ?? direct[2] : piped[4] ?? piped[5]) ?? "0", 10);
7929
7951
  if (n <= 10) return null;
7930
- const filePath = m[3] ?? m[4] ?? m[5];
7952
+ const filePath = direct !== null ? direct[3] ?? direct[4] ?? direct[5] : piped[1] ?? piped[2] ?? piped[3];
7931
7953
  if (!filePath) return null;
7932
7954
  if (isTempPath(filePath)) return null;
7933
7955
  if (!/\.(?:ts|tsx|js|jsx|py|go|java|rs|rb|cs|md|mdx|rst|txt|json|yaml|yml|toml|sql|sh)$/i.test(filePath)) return null;
@@ -8381,10 +8403,19 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
8381
8403
  });
8382
8404
  const minNet = resolveMinNetSavingsBytes();
8383
8405
  if (!compressed.worthApplying(minNet)) return null;
8384
- const id = await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8385
- recordStat("bash_compress:generic", compressed.bytesSaved, compressed.tokensSaved);
8406
+ const id = await commandHash(cmd, cwd);
8386
8407
  const body = compressed.withMarker(minNet) + "\n[token-goat] full output: bash-output " + id + " --full";
8387
- return { hookType: "rewriteOutput", updatedOutput: body };
8408
+ const emittedBytes = Buffer.byteLength(body, "utf-8");
8409
+ if (!isRewriteWorthwhile({
8410
+ originalBytes: compressed.originalBytes,
8411
+ rewrittenBytes: emittedBytes,
8412
+ noticeBytes: 0,
8413
+ minNetSavingsBytes: minNet
8414
+ })) {
8415
+ return null;
8416
+ }
8417
+ await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8418
+ return emitRewrite(body, "bash", { kind: "bash_compress:generic", originalBytes: compressed.originalBytes });
8388
8419
  }
8389
8420
  function unwrapCompressCommand(executed) {
8390
8421
  const t = executed.trim();
@@ -8610,25 +8641,28 @@ function preBashHandlerInner(event) {
8610
8641
  "Use `token-goat outline <file>` to see symbol names and line counts without loading files."
8611
8642
  );
8612
8643
  }
8613
- const sedRange = extractLineRangeRead(cmd);
8614
- if (sedRange !== null) {
8615
- const { filePath, ranges, tool } = sedRange;
8616
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
8644
+ const singleLineRangeRead = extractLineRangeRead(cmd);
8645
+ const sedReads = singleLineRangeRead !== null ? [singleLineRangeRead] : extractLineRangeReadsCompound(cmd);
8646
+ if (sedReads !== null) {
8617
8647
  recordStat("session_hint", 0, 0);
8618
- const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
8619
- const overlapHints = [];
8620
- const freshRanges = [];
8621
- for (const [start, end] of ranges) {
8622
- const priorOverlap = findRangeOverlap(getFileLineRanges(sedDedupKey), start, end);
8623
- recordFileLineRange(sedDedupKey, start, end);
8624
- if (priorOverlap !== null) {
8625
- overlapHints.push(sedOverlapHint(hintPath, priorOverlap, start, end));
8626
- } else {
8627
- freshRanges.push([start, end]);
8648
+ const hints = [];
8649
+ for (const { filePath, ranges, tool } of sedReads) {
8650
+ const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
8651
+ const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
8652
+ const overlapHints = [];
8653
+ const freshRanges = [];
8654
+ for (const [start, end] of ranges) {
8655
+ const priorOverlap = findRangeOverlap(getFileLineRanges(sedDedupKey), start, end);
8656
+ recordFileLineRange(sedDedupKey, start, end);
8657
+ if (priorOverlap !== null) {
8658
+ overlapHints.push(sedOverlapHint(hintPath, priorOverlap, start, end));
8659
+ } else {
8660
+ freshRanges.push([start, end]);
8661
+ }
8628
8662
  }
8663
+ hints.push(...overlapHints);
8664
+ if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
8629
8665
  }
8630
- const hints = [...overlapHints];
8631
- if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
8632
8666
  return contextOutput(hints.join(" "));
8633
8667
  }
8634
8668
  const catJsonPipe = extractCatJsonPipe(cmd);
@@ -8642,7 +8676,7 @@ function preBashHandlerInner(event) {
8642
8676
  }
8643
8677
  const catResult = extractCatFile(cmd);
8644
8678
  if (catResult !== null) {
8645
- const { filePath, isDoc, isEnv, isConfig, isSql, cmd0 } = catResult;
8679
+ const { filePath, isDoc, isEnv, isConfig, isSql, cmd0, advisoryOnly } = catResult;
8646
8680
  const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
8647
8681
  recordStat("session_hint", 0, 0);
8648
8682
  if (isSql) {
@@ -8651,7 +8685,7 @@ function preBashHandlerInner(event) {
8651
8685
  );
8652
8686
  }
8653
8687
  const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc);
8654
- return cdStripped ? contextOutput("`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
8688
+ return cdStripped || advisoryOnly ? contextOutput("`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
8655
8689
  }
8656
8690
  const catMulti = extractCatFilesMulti(cmd);
8657
8691
  if (catMulti !== null) {
@@ -8790,13 +8824,13 @@ function preBashHandlerInner(event) {
8790
8824
  const monBytes = monEntry.sizeBytes;
8791
8825
  const catFile = extractCatSourceFile(cmd);
8792
8826
  if (catFile !== null) {
8793
- recordStat("bash_compress:recall", monBytes, Math.round(monBytes / 4));
8827
+ recordStat("bash_compress:recall", monBytes, savedTokensFromBytes(monBytes));
8794
8828
  return contextOutput(
8795
8829
  "Prior output from `" + cmd + "`" + pipelineDivergenceNote(cmd, monEntry.command) + " is cached. Use `token-goat bash-output " + monOutputId + "` to recall the full file, or `token-goat read '" + catFile + "::SymbolName'` to extract only the symbol you need."
8796
8830
  );
8797
8831
  }
8798
8832
  const cmdSummary = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
8799
- recordStat("bash_compress:recall", monBytes, Math.round(monBytes / 4));
8833
+ recordStat("bash_compress:recall", monBytes, savedTokensFromBytes(monBytes));
8800
8834
  return contextOutput(
8801
8835
  "Prior output from `" + cmdSummary + "`" + pipelineDivergenceNote(cmd, monEntry.command) + " is cached.\nUse `token-goat bash-output " + monOutputId + " " + monitoringHint + "` to re-inspect without re-running."
8802
8836
  );
@@ -8823,7 +8857,7 @@ function preBashHandlerInner(event) {
8823
8857
  const curlEntry = curlEntryRaw !== null && !isBashEntryStale(curlEntryRaw, cmd, preHookCwd) ? curlEntryRaw : null;
8824
8858
  if (curlOutputId !== null && curlEntry !== null && curlEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(curlEntry.sizeBytes)) {
8825
8859
  const curlBytes = curlEntry.sizeBytes;
8826
- recordStat("bash_compress:recall", curlBytes, Math.round(curlBytes / 4));
8860
+ recordStat("bash_compress:recall", curlBytes, savedTokensFromBytes(curlBytes));
8827
8861
  const curlPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
8828
8862
  return contextOutput(
8829
8863
  "curl response cached (`" + curlPreview + "`)." + pipelineDivergenceNote(cmd, curlEntry.command) + " Use `token-goat bash-output " + curlOutputId + "` to recall it. Append `--grep PATTERN` to filter or `--section HeadingName` for a markdown section."
@@ -8837,7 +8871,7 @@ function preBashHandlerInner(event) {
8837
8871
  const ghEntry = ghEntryRaw !== null && !isBashEntryStale(ghEntryRaw, cmd, preHookCwd) ? ghEntryRaw : null;
8838
8872
  if (ghOutputId !== null && ghEntry !== null && ghEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(ghEntry.sizeBytes)) {
8839
8873
  const ghBytes = ghEntry.sizeBytes;
8840
- recordStat("bash_compress:recall", ghBytes, Math.round(ghBytes / 4));
8874
+ recordStat("bash_compress:recall", ghBytes, savedTokensFromBytes(ghBytes));
8841
8875
  const ghPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
8842
8876
  return contextOutput(
8843
8877
  "gh api response cached (`" + ghPreview + "`)." + pipelineDivergenceNote(cmd, ghEntry.command) + " Use `token-goat bash-output " + ghOutputId + "` to recall it. Append `--jq '.field'` on the original call, or `--grep PATTERN` / `--max-matches N` here, to narrow it."
@@ -8851,7 +8885,7 @@ function preBashHandlerInner(event) {
8851
8885
  const gitScopedEntry = gitScopedEntryRaw !== null && !isBashEntryStale(gitScopedEntryRaw, cmd, preHookCwd) ? gitScopedEntryRaw : null;
8852
8886
  if (gitScopedOutputId !== null && gitScopedEntry !== null && gitScopedEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(gitScopedEntry.sizeBytes)) {
8853
8887
  const gitScopedBytes = gitScopedEntry.sizeBytes;
8854
- recordStat("bash_compress:recall", gitScopedBytes, Math.round(gitScopedBytes / 4));
8888
+ recordStat("bash_compress:recall", gitScopedBytes, savedTokensFromBytes(gitScopedBytes));
8855
8889
  const gitScopedPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
8856
8890
  return contextOutput(
8857
8891
  "Output from `" + gitScopedPreview + "`" + pipelineDivergenceNote(cmd, gitScopedEntry.command) + " is cached and unchanged (no edits to that path or HEAD since). Use `token-goat bash-output " + gitScopedOutputId + "` to recall it instead of re-running."
@@ -8893,7 +8927,7 @@ function preBashHandlerInner(event) {
8893
8927
  const entry = entryRaw !== null && !isBashEntryStale(entryRaw, cmd, preHookCwd) ? entryRaw : null;
8894
8928
  if (outputId !== null && entry !== null && entry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(entry.sizeBytes)) {
8895
8929
  const bytes = entry.sizeBytes;
8896
- recordStat("bash_compress:recall", bytes, Math.round(bytes / 4));
8930
+ recordStat("bash_compress:recall", bytes, savedTokensFromBytes(bytes));
8897
8931
  return contextOutput(buildRecallHint(cmd, outputId));
8898
8932
  }
8899
8933
  return maybeCompressRewrite(event, rawCmd, cmd) ?? passOutput();
@@ -9189,12 +9223,23 @@ function getTaskId(toolInput) {
9189
9223
  const value = toolInput["task_id"];
9190
9224
  return typeof value === "string" && value !== "" ? value : void 0;
9191
9225
  }
9226
+ function extractTaskOutputText(raw) {
9227
+ const tr = raw["tool_response"];
9228
+ if (tr !== null && typeof tr === "object" && !Array.isArray(tr)) {
9229
+ const task = tr["task"];
9230
+ if (task !== null && typeof task === "object" && !Array.isArray(task)) {
9231
+ const output = task["output"];
9232
+ if (typeof output === "string") return output;
9233
+ }
9234
+ }
9235
+ return extractToolResultText(raw);
9236
+ }
9192
9237
  function postTaskOutputHandler(event) {
9193
9238
  try {
9194
9239
  if (getToolName(event) !== "TaskOutput" || !event.sessionId) return passOutput();
9195
9240
  const taskId = getTaskId(getToolInput(event));
9196
9241
  if (taskId === void 0) return passOutput();
9197
- const rawOutput = extractToolResultText(event.raw);
9242
+ const rawOutput = extractTaskOutputText(event.raw);
9198
9243
  if (!rawOutput) return passOutput();
9199
9244
  const output = redactSecrets(rawOutput).text;
9200
9245
  if (!output) return passOutput();
@@ -9366,10 +9411,10 @@ ${PLAN_OMIT_POINTER}`;
9366
9411
  })) {
9367
9412
  return passOutput();
9368
9413
  }
9369
- return {
9370
- hookType: "rewriteOutput",
9371
- updatedOutput: `${prefix}${notice}`
9372
- };
9414
+ return emitRewrite(`${prefix}${notice}`, "exitplanmode", {
9415
+ kind: "plan_echo_collapse",
9416
+ originalBytes: Buffer.byteLength(output, "utf-8")
9417
+ });
9373
9418
  } catch {
9374
9419
  return passOutput();
9375
9420
  }
@@ -9437,6 +9482,9 @@ function storeMcpOutput(sessionId, toolName, toolInput, resultText) {
9437
9482
  ${redactedOutput}`, entry.storedAt);
9438
9483
  return id;
9439
9484
  }
9485
+ function mcpOutputBytes(id) {
9486
+ return getBashOutput(id)?.sizeBytes ?? 0;
9487
+ }
9440
9488
  function getMcpOutput(sessionId, toolName, toolInput, ttlMs = Number.POSITIVE_INFINITY) {
9441
9489
  if (!sessionId) return null;
9442
9490
  const id = mcpOutputId(sessionId, mcpHash(toolName, toolInput));
@@ -9759,6 +9807,8 @@ function preMcpHandler(event) {
9759
9807
  const ttlMs = loadConfig().hints.mcp_dedup_ttl_secs * 1e3;
9760
9808
  const id = getMcpOutput(event.sessionId, toolName, toolInput, ttlMs);
9761
9809
  if (!id) return passOutput();
9810
+ const denyCredit = Math.min(mcpOutputBytes(id), PER_FILE_COUNTERFACTUAL_CEILING);
9811
+ recordStat("mcp:recall", denyCredit, savedTokensFromBytes(denyCredit));
9762
9812
  return denyOutput(
9763
9813
  "Identical read-only MCP call already cached this session. Use `token-goat bash-output " + id + "` to recall the result (add `--grep PATTERN`, `--tail N`, or `--head N` to slice) instead of repeating the call."
9764
9814
  );
@@ -9769,23 +9819,14 @@ function postMcpHandler(event) {
9769
9819
  const toolInput = getToolInput(event);
9770
9820
  const resultText = extractToolResultText(event.raw);
9771
9821
  if (!resultText) return passOutput();
9772
- let injectionMatches = [];
9773
- try {
9774
- if (loadConfig().injection.enabled) injectionMatches = scanForInjectionPatterns(resultText);
9775
- } catch {
9776
- injectionMatches = [];
9777
- }
9778
- if (injectionMatches.length > 0) recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
9822
+ const injectionMatches = scanAndRecord(resultText);
9779
9823
  const redactedResult = redactSecrets(resultText);
9780
- const fenced = () => emitRewrite(
9781
- fenceUntrustedContent(redactedResult.text, injectionMatches, UNTRUSTED_TOOL_TAG),
9824
+ const fenced = () => emitRewriteIfChanged(
9825
+ resultText,
9826
+ fenceWithMatches(redactedResult.text, injectionMatches, UNTRUSTED_TOOL_TAG),
9782
9827
  "mcp"
9783
9828
  );
9784
- const passOrFence = () => {
9785
- if (injectionMatches.length > 0) return fenced();
9786
- if (redactedResult.count > 0) return emitRewrite(redactedResult.text, "mcp");
9787
- return passOutput();
9788
- };
9829
+ const passOrFence = () => fenced();
9789
9830
  if (!event.sessionId) return passOrFence();
9790
9831
  if (isMcpErrorResponse(event.raw)) return passOrFence();
9791
9832
  const readOnly = isMcpReadOnly(toolName, toolInput);
@@ -9803,16 +9844,18 @@ function postMcpHandler(event) {
9803
9844
  const redactedBody = redactSecrets(compressed).text;
9804
9845
  const notice = `[token-goat: compressed, full via mcp-output ${id}]
9805
9846
  `;
9847
+ const originalBytes = Buffer.byteLength(resultText, "utf-8");
9806
9848
  const worthwhile = isRewriteWorthwhile({
9807
- originalBytes: Buffer.byteLength(resultText, "utf-8"),
9849
+ originalBytes,
9808
9850
  rewrittenBytes: Buffer.byteLength(redactedBody, "utf-8"),
9809
9851
  noticeBytes: Buffer.byteLength(notice, "utf-8"),
9810
9852
  minNetSavingsBytes: resolveMinNetSavingsBytes()
9811
9853
  });
9812
9854
  if (worthwhile) {
9813
9855
  return emitRewrite(
9814
- injectionMatches.length > 0 ? `${notice}${fenceUntrustedContent(redactedBody, injectionMatches, UNTRUSTED_TOOL_TAG)}` : `${notice}${redactedBody}`,
9815
- "mcp"
9856
+ `${notice}${fenceWithMatches(redactedBody, injectionMatches, UNTRUSTED_TOOL_TAG)}`,
9857
+ "mcp",
9858
+ { kind: "mcp:compress", originalBytes }
9816
9859
  );
9817
9860
  }
9818
9861
  }
@@ -9862,23 +9905,9 @@ function postWebSearchHandler(event) {
9862
9905
  if (getToolName(event) !== "WebSearch") return passOutput();
9863
9906
  const resultText = extractToolResultText(event.raw);
9864
9907
  if (!resultText) return passOutput();
9865
- let injectionMatches = [];
9866
- try {
9867
- if (loadConfig().injection.enabled) injectionMatches = scanForInjectionPatterns(resultText);
9868
- } catch {
9869
- injectionMatches = [];
9870
- }
9871
- if (injectionMatches.length > 0) recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
9908
+ const injectionMatches = scanAndRecord(resultText);
9872
9909
  const redacted = redactSecrets(resultText);
9873
- const passOrFence = () => {
9874
- if (injectionMatches.length > 0) {
9875
- return emitRewrite(fenceUntrustedContent(redacted.text, injectionMatches), "websearch");
9876
- }
9877
- if (redacted.count > 0) {
9878
- return emitRewrite(redacted.text, "websearch");
9879
- }
9880
- return passOutput();
9881
- };
9910
+ const passOrFence = () => emitRewriteIfChanged(resultText, fenceWithMatches(redacted.text, injectionMatches), "websearch");
9882
9911
  if (!event.sessionId) return passOrFence();
9883
9912
  const signature = webSearchSignatureInput(getToolInput(event));
9884
9913
  if (signature === null) return passOrFence();
@@ -9934,13 +9963,15 @@ async function shrinkImageBlock(block) {
9934
9963
  const mediaType = typeof source?.media_type === "string" ? source.media_type : "image/png";
9935
9964
  const data = typeof source?.data === "string" ? source.data : "";
9936
9965
  const originalDataUrl = `data:${mediaType};base64,${data}`;
9937
- if (source?.type !== "base64" || data === "") return { text: originalDataUrl, changed: false, savedBytes: 0 };
9966
+ if (source?.type !== "base64" || data === "") return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
9938
9967
  let buffer;
9939
9968
  try {
9940
9969
  buffer = Buffer.from(data, "base64");
9941
9970
  } catch {
9942
- return { text: originalDataUrl, changed: false, savedBytes: 0 };
9971
+ return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
9943
9972
  }
9973
+ const meta = await probeImageMeta(buffer);
9974
+ const tier = loadConfig().image_shrink.vision_tier;
9944
9975
  const fingerprint = screenshotFingerprint(data);
9945
9976
  const alreadyShown = hasSeenImage(fingerprint);
9946
9977
  recordSeenImage(fingerprint);
@@ -9955,16 +9986,23 @@ async function shrinkImageBlock(block) {
9955
9986
  return {
9956
9987
  text: SCREENSHOT_REPEAT_NOTICE,
9957
9988
  changed: true,
9958
- savedBytes: originalDataUrl.length - SCREENSHOT_REPEAT_NOTICE.length
9989
+ // Decoded image bytes, not base64 characters. Both branches of this function file under the same `image_shrink` kind, and the shrink branch below credits `result.originalBytes - result.shrunkBytes` -- decoded bytes, the convention documented at image_shrink.ts's own recordStat call. Base64 inflates by 4/3, so crediting `originalDataUrl.length` here booked roughly 33% more for a repeat screenshot than an identical shrink of the same image, and the ledger summed the two units into one row. The gate above deliberately still measures the data URL: it decides whether the rewrite pays off on the wire, where base64 characters are what is actually sent.
9990
+ savedBytes: buffer.length - Buffer.byteLength(SCREENSHOT_REPEAT_NOTICE, "utf-8"),
9991
+ // The whole image is withheld here rather than resized, so the visual tokens saved are its
9992
+ // entire billed cost less the notice standing in for it. Billed cost, not raw patch count:
9993
+ // an oversized screenshot would have been capped by the API's own downscale, so pricing the
9994
+ // untouched dimensions would credit a bill that was never going to be sent.
9995
+ savedTokens: Math.max(0, visionTokens(meta?.width ?? 0, meta?.height ?? 0, tier) - savedTokensFromBytes(Buffer.byteLength(SCREENSHOT_REPEAT_NOTICE, "utf-8")))
9959
9996
  };
9960
9997
  }
9961
9998
  }
9962
9999
  const result = await imageQualifiesForShrink(buffer) ? await shrinkImage(buffer, { sizeThresholdBytes: 0 }) : null;
9963
- if (result === null) return { text: originalDataUrl, changed: false, savedBytes: 0 };
10000
+ if (result === null) return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
9964
10001
  const saved = result.originalBytes - result.shrunkBytes;
9965
10002
  const { summary, dataUrl } = formatShrinkSummary(result, "an inline browser screenshot");
10003
+ const savedTokens = visionTokensSaved(result.originalWidth, result.originalHeight, result.width, result.height, tier);
9966
10004
  return { text: `${summary}
9967
- ${dataUrl}`, changed: true, savedBytes: saved };
10005
+ ${dataUrl}`, changed: true, savedBytes: saved, savedTokens };
9968
10006
  }
9969
10007
  var TAB_CONTEXT_UNCHANGED_NOTICE = "(tabs unchanged since last check)";
9970
10008
  function dedupTabContext(text) {
@@ -9992,15 +10030,19 @@ async function postBrowserImageHandler(event) {
9992
10030
  const parts = [];
9993
10031
  for (const block of blocks) {
9994
10032
  if (block.type === "image") {
9995
- const { text, changed, savedBytes } = await shrinkImageBlock(block);
10033
+ const { text, changed, savedBytes, savedTokens } = await shrinkImageBlock(block);
9996
10034
  if (changed) {
9997
10035
  anyChanged = true;
9998
- recordStat("image_shrink", savedBytes, Math.round(savedBytes / 4), void 0, toolName);
10036
+ recordStat("image_shrink", savedBytes, savedTokens, void 0, toolName);
9999
10037
  }
10000
10038
  parts.push(text);
10001
10039
  } else if (block.type === "text" && typeof block.text === "string") {
10002
10040
  const { text, changed } = dedupTabContext(block.text);
10003
- if (changed) anyChanged = true;
10041
+ if (changed) {
10042
+ anyChanged = true;
10043
+ const savedBytes = Buffer.byteLength(block.text, "utf-8") - Buffer.byteLength(text, "utf-8");
10044
+ if (savedBytes > 0) recordStat("browser_tab_dedup", savedBytes, savedTokensFromBytes(savedBytes), void 0, toolName);
10045
+ }
10004
10046
  parts.push(text);
10005
10047
  } else {
10006
10048
  try {
@@ -10011,7 +10053,7 @@ async function postBrowserImageHandler(event) {
10011
10053
  }
10012
10054
  }
10013
10055
  if (!anyChanged) return passOutput();
10014
- return { hookType: "rewriteOutput", updatedOutput: parts.join("\n") };
10056
+ return emitRewrite(parts.join("\n"), toolName);
10015
10057
  } catch {
10016
10058
  return passOutput();
10017
10059
  }
@@ -10020,6 +10062,9 @@ registerHook("post_tool_use", postBrowserImageHandler, { toolPattern: "^mcp__" }
10020
10062
 
10021
10063
  // src/hooks_agent_spawn.ts
10022
10064
  import { createHash as createHash2 } from "node:crypto";
10065
+ import * as fs from "node:fs";
10066
+ import * as os from "node:os";
10067
+ import * as path2 from "node:path";
10023
10068
  var BRIEFING_TARGET_TOKENS = 550;
10024
10069
  function buildSubagentBriefing() {
10025
10070
  try {
@@ -10091,8 +10136,11 @@ function findDuplicateOutstandingPrompt(prompt) {
10091
10136
  function truncateForWarning(text, max) {
10092
10137
  return text.length > max ? text.slice(0, max) + "..." : text;
10093
10138
  }
10139
+ function isAgentTool(toolName) {
10140
+ return toolName === "Agent" || toolName === "task" || toolName === "Task";
10141
+ }
10094
10142
  function preAgentHandler(event) {
10095
- if (event.toolName !== "Agent") return passOutput();
10143
+ if (!isAgentTool(event.toolName)) return passOutput();
10096
10144
  const toolInput = event.toolInput;
10097
10145
  const prompt = toolInput["prompt"];
10098
10146
  if (typeof prompt !== "string" || prompt.trim() === "") return passOutput();
@@ -10114,7 +10162,7 @@ function preAgentHandler(event) {
10114
10162
  return passOutput();
10115
10163
  }
10116
10164
  }
10117
- var FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
10165
+ var FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})([^\n]*)$/;
10118
10166
  function findFencedBlockLines(lines) {
10119
10167
  const blocks = [];
10120
10168
  let fenceStart = -1;
@@ -10214,9 +10262,100 @@ function collapseBlankRunsInFences(text) {
10214
10262
  out.push(...lines.slice(cursor));
10215
10263
  return changedAny ? out.join("\n") : text;
10216
10264
  }
10265
+ var SPAWN_RESTRICT_HINT_KEY = "agent-spawn-restrict-hint";
10266
+ var SPAWN_RESTRICT_MAX_NAMES = 3;
10267
+ var ROSTER_WALK_MAX_DEPTH = 4;
10268
+ var ROSTER_WALK_MAX_FILES = 400;
10269
+ function parseAgentDefinition(text, fallbackName) {
10270
+ const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
10271
+ if (!fmMatch) return null;
10272
+ const fm = fmMatch[1];
10273
+ const nameMatch = /^name:(.*)$/m.exec(fm);
10274
+ const rawName = nameMatch ? nameMatch[1].trim().replace(/^["']|["']$/g, "") : "";
10275
+ const name2 = rawName !== "" ? rawName : fallbackName;
10276
+ const toolsMatch = /^tools:(.*)$/m.exec(fm);
10277
+ if (!toolsMatch) return { name: name2, restricted: false };
10278
+ const inline = toolsMatch[1].trim();
10279
+ if (inline === "*" || inline === '"*"' || inline === "'*'") return { name: name2, restricted: false };
10280
+ if (inline !== "") return { name: name2, restricted: true };
10281
+ const after = fm.slice(toolsMatch.index + toolsMatch[0].length);
10282
+ for (const line of after.split(/\r?\n/)) {
10283
+ if (/^\s+-\s*\S/.test(line)) return { name: name2, restricted: true };
10284
+ if (/^\S/.test(line)) break;
10285
+ }
10286
+ return { name: name2, restricted: false };
10287
+ }
10288
+ function findRestrictedAgentNames(roots) {
10289
+ const scanRoots = roots ?? [path2.join(os.homedir(), ".claude", "agents")];
10290
+ const names = /* @__PURE__ */ new Set();
10291
+ const visited = /* @__PURE__ */ new Set();
10292
+ let filesSeen = 0;
10293
+ const walk = (dir, depth) => {
10294
+ if (depth > ROSTER_WALK_MAX_DEPTH || filesSeen >= ROSTER_WALK_MAX_FILES) return;
10295
+ let real;
10296
+ try {
10297
+ real = fs.realpathSync(dir);
10298
+ } catch {
10299
+ return;
10300
+ }
10301
+ const key = process.platform === "win32" ? real.toLocaleLowerCase() : real;
10302
+ if (visited.has(key)) return;
10303
+ visited.add(key);
10304
+ let entries;
10305
+ try {
10306
+ entries = fs.readdirSync(dir, { withFileTypes: true });
10307
+ } catch {
10308
+ return;
10309
+ }
10310
+ for (const entry of entries) {
10311
+ if (filesSeen >= ROSTER_WALK_MAX_FILES) return;
10312
+ const full = path2.join(dir, entry.name);
10313
+ let stat;
10314
+ try {
10315
+ stat = fs.statSync(full);
10316
+ } catch {
10317
+ continue;
10318
+ }
10319
+ if (stat.isDirectory()) {
10320
+ walk(full, depth + 1);
10321
+ continue;
10322
+ }
10323
+ if (!stat.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
10324
+ filesSeen++;
10325
+ let text;
10326
+ try {
10327
+ text = fs.readFileSync(full, "utf-8");
10328
+ } catch {
10329
+ continue;
10330
+ }
10331
+ const parsed = parseAgentDefinition(text, path2.basename(entry.name, path2.extname(entry.name)));
10332
+ if (parsed !== null && parsed.restricted) names.add(parsed.name);
10333
+ }
10334
+ };
10335
+ for (const root of scanRoots) walk(root, 0);
10336
+ return Array.from(names).sort();
10337
+ }
10338
+ function buildUnrestrictedSpawnAdvisory(toolInput) {
10339
+ try {
10340
+ if (getHarnessName() === "copilot_cli") return "";
10341
+ const rawType = toolInput["subagent_type"];
10342
+ const spawnType = typeof rawType === "string" ? rawType.trim() : "";
10343
+ if (spawnType !== "" && spawnType !== "general-purpose") return "";
10344
+ if (wasHintShown(SPAWN_RESTRICT_HINT_KEY)) return "";
10345
+ const names = findRestrictedAgentNames();
10346
+ if (names.length === 0) return "";
10347
+ markHintShown(SPAWN_RESTRICT_HINT_KEY);
10348
+ recordStat("session_hint", 0, 0, void 0, "agent-spawn-restrict");
10349
+ const shown = names.slice(0, SPAWN_RESTRICT_MAX_NAMES).join(", ");
10350
+ const more = names.length > SPAWN_RESTRICT_MAX_NAMES ? ` and ${names.length - SPAWN_RESTRICT_MAX_NAMES} more` : "";
10351
+ return `[token-goat] This spawn ran as general-purpose (the default when subagent_type is omitted), which is unrestricted: its lane starts by paying for every tool and MCP schema on the machine. Tools-restricted agent definitions exist here: ${shown}${more}. A future spawn that fits one of them can pass that name as subagent_type to start with a much smaller prefix. Advisory only: this spawn has already run, and this notice saved nothing.`;
10352
+ } catch {
10353
+ return "";
10354
+ }
10355
+ }
10217
10356
  function postAgentHandler(event) {
10218
10357
  try {
10219
- if (event.toolName !== "Agent" || !event.sessionId) return passOutput();
10358
+ if (!isAgentTool(event.toolName) || !event.sessionId) return passOutput();
10220
10359
  const finishedPrompt = event.toolInput["prompt"];
10221
10360
  if (typeof finishedPrompt === "string" && finishedPrompt !== "") {
10222
10361
  removeOutstandingAgentSpawn(finishedPrompt);
@@ -10224,13 +10363,17 @@ function postAgentHandler(event) {
10224
10363
  const redactedReport = redactSecrets(extractToolResultText(event.raw));
10225
10364
  const resultText = redactedReport.text;
10226
10365
  const agentReportCfg = loadConfig().agent_report;
10227
- if (!resultText || resultText.length < agentReportCfg.min_bytes) return passOutput();
10228
- const id = storeMcpOutput(event.sessionId, "Agent", event.toolInput, resultText);
10229
- if (id === null) return passOutput();
10366
+ const spawnAdvisory = buildUnrestrictedSpawnAdvisory(event.toolInput);
10367
+ if (!resultText || resultText.length < agentReportCfg.min_bytes) {
10368
+ return spawnAdvisory === "" ? passOutput() : contextOutput(spawnAdvisory);
10369
+ }
10370
+ const id = storeMcpOutput(event.sessionId, event.toolName ?? "Agent", event.toolInput, resultText);
10371
+ if (id === null) return spawnAdvisory === "" ? passOutput() : contextOutput(spawnAdvisory);
10230
10372
  if (redactedReport.count > 0) recordStat("secret_redacted", 0, redactedReport.count, void 0, "agent");
10231
10373
  recordStat("session_hint", 0, 0);
10232
10374
  const recallHint = `token-goat mcp-output ${id} --full`;
10233
- const notice = `[token-goat] This subagent report (${toKB(resultText.length)}KB) is cached for later recall: ${recallHint}`;
10375
+ const notice = `[token-goat] This subagent report (${toKB(resultText.length)}KB) is cached for later recall: ${recallHint}${spawnAdvisory === "" ? "" : `
10376
+ ${spawnAdvisory}`}`;
10234
10377
  const collapsed = collapseFencedBlocks(resultText, recallHint, agentReportCfg.fence_collapse_min_lines, agentReportCfg.fence_collapse_keep_lines);
10235
10378
  const deduped = dedupeFencedBlocks(collapsed, resultText, recallHint);
10236
10379
  const final = collapseBlankRunsInFences(deduped);
@@ -10247,8 +10390,8 @@ function postAgentHandler(event) {
10247
10390
 
10248
10391
  ${notice}`;
10249
10392
  const savedBytes = originalBytes - Buffer.byteLength(updatedOutput, "utf-8");
10250
- if (savedBytes > 0) recordStat("agent_report_compact", savedBytes, Math.round(savedBytes / 4));
10251
- return { hookType: "rewriteOutput", updatedOutput };
10393
+ if (savedBytes > 0) recordStat("agent_report_compact", savedBytes, savedTokensFromBytes(savedBytes));
10394
+ return emitRewrite(updatedOutput, "agent", void 0, "counted-elsewhere");
10252
10395
  }
10253
10396
  recordStat("agent_report_compact_declined", 0, 0);
10254
10397
  }
@@ -10258,7 +10401,11 @@ ${notice}`;
10258
10401
  }
10259
10402
  }
10260
10403
  registerHook("pre_tool_use", preAgentHandler, { toolName: "Agent" });
10404
+ registerHook("pre_tool_use", preAgentHandler, { toolName: "task" });
10405
+ registerHook("pre_tool_use", preAgentHandler, { toolName: "Task" });
10261
10406
  registerHook("post_tool_use", postAgentHandler, { toolName: "Agent" });
10407
+ registerHook("post_tool_use", postAgentHandler, { toolName: "task" });
10408
+ registerHook("post_tool_use", postAgentHandler, { toolName: "Task" });
10262
10409
 
10263
10410
  // src/relay.ts
10264
10411
  function isHookEventName(name2) {
@@ -10274,7 +10421,11 @@ function buildEvent(eventName, payload) {
10274
10421
  const sessionId = typeof rawSession === "string" ? rawSession : "";
10275
10422
  const rawAgentId = obj["agent_id"] ?? obj["agentId"];
10276
10423
  const agentId = typeof rawAgentId === "string" && rawAgentId !== "" ? rawAgentId : void 0;
10277
- return { eventName, toolName, toolInput, sessionId, agentId, raw: obj };
10424
+ const rawTraceparent = obj["traceparent"] ?? obj["traceParent"] ?? process.env["TRACEPARENT"] ?? process.env["traceparent"];
10425
+ const traceparent = typeof rawTraceparent === "string" && rawTraceparent.trim() !== "" ? rawTraceparent.trim() : void 0;
10426
+ const rawTracestate = obj["tracestate"] ?? obj["traceState"] ?? process.env["TRACESTATE"] ?? process.env["tracestate"];
10427
+ const tracestate = typeof rawTracestate === "string" && rawTracestate.trim() !== "" ? rawTracestate.trim() : void 0;
10428
+ return { eventName, toolName, toolInput, sessionId, agentId, traceparent, tracestate, raw: obj };
10278
10429
  }
10279
10430
  function sessionStateKey(event) {
10280
10431
  return event.agentId !== void 0 ? `${event.sessionId}:agent:${event.agentId}` : event.sessionId;
@@ -10285,6 +10436,7 @@ function harnessForNormalization() {
10285
10436
  if (detected === "gemini") return "gemini";
10286
10437
  if (detected === "grok") return "grok";
10287
10438
  if (detected === "kimi") return "kimi";
10439
+ if (detected === "qwen") return "qwen";
10288
10440
  if (detected === "copilot_cli") return "copilot_cli";
10289
10441
  return "claude";
10290
10442
  }
@@ -10299,6 +10451,12 @@ async function relayInProcess(eventName, rawPayload) {
10299
10451
  if (!process.env["CLAUDE_CODE_SESSION_ID"] && event.sessionId) {
10300
10452
  process.env["CLAUDE_CODE_SESSION_ID"] = event.sessionId;
10301
10453
  }
10454
+ if (!process.env["TRACEPARENT"] && event.traceparent) {
10455
+ process.env["TRACEPARENT"] = event.traceparent;
10456
+ }
10457
+ if (!process.env["TRACESTATE"] && event.tracestate) {
10458
+ process.env["TRACESTATE"] = event.tracestate;
10459
+ }
10302
10460
  const stateKey = sessionStateKey(event);
10303
10461
  try {
10304
10462
  loadSessionState(stateKey);