token-goat 2.8.3 → 2.8.4

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-IZRXU64B.mjs";
27
27
  import {
28
28
  BODY_FIRST_TOOL_RESPONSE_KEYS,
29
29
  OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
@@ -45,7 +45,6 @@ import {
45
45
  enqueueDirtyPathSafe,
46
46
  ensureWorkerAlive,
47
47
  estimateTokens,
48
- estimateTokensFromLength,
49
48
  extractCompactFromMarker,
50
49
  extractToolResponseField,
51
50
  extractToolResultText,
@@ -89,6 +88,7 @@ import {
89
88
  matchesDenyPattern,
90
89
  meetsSavingsFloor,
91
90
  passOutput,
91
+ probeImageMeta,
92
92
  recordBashOutput,
93
93
  recordBashRerun,
94
94
  recordCliRead,
@@ -117,10 +117,12 @@ import {
117
117
  storeBlob,
118
118
  storeOutput,
119
119
  takePendingLargeFileHint,
120
+ visionTokens,
121
+ visionTokensSaved,
120
122
  wasCliReadThisSession,
121
123
  wasFileReadThisSession,
122
124
  wasHintShown
123
- } from "./token-goat-chunk-PWVXXPCC.mjs";
125
+ } from "./token-goat-chunk-4OTIB7SB.mjs";
124
126
  import {
125
127
  canRunWrappedShell,
126
128
  compressOutput,
@@ -131,7 +133,7 @@ import {
131
133
  isRewriteWorthwhile,
132
134
  resolveMinNetSavingsBytes,
133
135
  shlexSplit
134
- } from "./token-goat-chunk-EFF2XCLB.mjs";
136
+ } from "./token-goat-chunk-CZALRRGN.mjs";
135
137
  import {
136
138
  PER_FILE_COUNTERFACTUAL_CEILING,
137
139
  VERSION,
@@ -150,9 +152,10 @@ import {
150
152
  redactSecrets,
151
153
  resolveIndexPath,
152
154
  runGit,
155
+ savedTokensFromBytes,
153
156
  shortFingerprint,
154
157
  toKB
155
- } from "./token-goat-chunk-6ODZ6PZK.mjs";
158
+ } from "./token-goat-chunk-E76UNTVK.mjs";
156
159
 
157
160
  // src/hooks_grep.ts
158
161
  function grepIntInput(toolInput, key) {
@@ -167,7 +170,7 @@ function grepIntInput(toolInput, key) {
167
170
  function grepSignature(toolInput) {
168
171
  const pattern = toolInput["pattern"];
169
172
  if (typeof pattern !== "string" || pattern === "") return null;
170
- const path2 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
173
+ const path3 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
171
174
  const outputMode = typeof toolInput["output_mode"] === "string" ? toolInput["output_mode"] : "files_with_matches";
172
175
  const glob = typeof toolInput["glob"] === "string" ? toolInput["glob"] : "";
173
176
  const type = typeof toolInput["type"] === "string" ? toolInput["type"] : "";
@@ -182,7 +185,7 @@ function grepSignature(toolInput) {
182
185
  const offset = grepIntInput(toolInput, "offset");
183
186
  return JSON.stringify([
184
187
  pattern,
185
- path2,
188
+ path3,
186
189
  outputMode,
187
190
  glob,
188
191
  type,
@@ -218,8 +221,7 @@ function foldGrepContentHandler(event) {
218
221
  if (toolInput["-n"] === false) return passOutput();
219
222
  const rawText = extractToolResponseField(event.raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
220
223
  if (!rawText) return passOutput();
221
- const redacted = redactSecrets(rawText);
222
- const text = redacted.text;
224
+ const text = redactSecrets(rawText).text;
223
225
  const rawLines = text.split(/\r\n|\r|\n/);
224
226
  const parsed = [];
225
227
  for (const line of rawLines) {
@@ -260,10 +262,7 @@ function foldGrepContentHandler(event) {
260
262
  })) {
261
263
  return passOutput();
262
264
  }
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 };
265
+ return emitRewrite(rewritten, "grep", { kind: "grep:fold", originalBytes });
267
266
  } catch {
268
267
  return passOutput();
269
268
  }
@@ -281,8 +280,8 @@ registerHook("post_tool_use", postGrepHandler, { toolName: "Grep" });
281
280
  function globSignature(toolInput) {
282
281
  const pattern = toolInput["pattern"];
283
282
  if (typeof pattern !== "string" || pattern === "") return null;
284
- const path2 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
285
- return JSON.stringify([pattern, path2]);
283
+ const path3 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
284
+ return JSON.stringify([pattern, path3]);
286
285
  }
287
286
  var { post: postGlobHandler, pre: preGlobDedupHandler } = makeDedupHintHandlers({
288
287
  toolName: "Glob",
@@ -430,6 +429,9 @@ function renderReadRow(entry) {
430
429
  const edited = entry.wasEdited ? ", edited" : "";
431
430
  return `- ${entry.path} (${kb}kb, ${entry.readCount} ${plural}${edited})`;
432
431
  }
432
+ function renderSymbolReadRow(entry) {
433
+ return `- ${entry.path} (symbols: ${(entry.symbols_read ?? []).join(", ")})`;
434
+ }
433
435
  function mergeManifestFiles(parent, siblingFiles) {
434
436
  const byPath = /* @__PURE__ */ new Map();
435
437
  for (const f of parent) byPath.set(foldPath(f.path), f);
@@ -457,6 +459,7 @@ function buildManifest(sessionId, cwd) {
457
459
  const files = siblingFiles.length > 0 ? mergeManifestFiles(ownFiles, siblingFiles) : ownFiles;
458
460
  const editedFiles = files.filter((f) => f.wasEdited);
459
461
  const readFiles = files.filter((f) => f.readCount > 0 && !f.wasEdited);
462
+ const symbolOnlyFiles = files.filter((f) => f.readCount === 0 && !f.wasEdited && (f.symbols_read?.length ?? 0) > 0);
460
463
  const webFetches = [...getSessionWebFetches().entries()];
461
464
  const lines = [];
462
465
  lines.push("## Session context");
@@ -469,6 +472,7 @@ function buildManifest(sessionId, cwd) {
469
472
  editedFiles.map((entry) => `- ${entry.path}`),
470
473
  MAX_ROWS
471
474
  );
475
+ appendCappedSection(lines, "### Surgically read files (symbol/section reads, never read whole)", symbolOnlyFiles.map(renderSymbolReadRow), MAX_ROWS);
472
476
  appendCappedSection(
473
477
  lines,
474
478
  "### Web URLs fetched",
@@ -613,7 +617,7 @@ function postCompactHandler(event) {
613
617
  0,
614
618
  0,
615
619
  void 0,
616
- `trigger=${trigger} bytes=${bytes} est_tokens=${estimateTokensFromLength(summary.length)} manifest_paths=${survived}/${sample.length}`
620
+ `trigger=${trigger} bytes=${bytes} est_tokens=${savedTokensFromBytes(bytes)} manifest_paths=${survived}/${sample.length}`
617
621
  );
618
622
  return passOutput();
619
623
  }
@@ -626,7 +630,7 @@ import crypto from "node:crypto";
626
630
  var TRACKED_SKILL = "token-goat";
627
631
  var MAX_COMMANDS_SHOWN = 8;
628
632
  async function currentCommandNames() {
629
- const { buildProgram } = await import("./token-goat-chunk-4OM2Q2SX.mjs");
633
+ const { buildProgram } = await import("./token-goat-chunk-HNODKNWO.mjs");
630
634
  return flattenCommandNames(buildCommandManifest(buildProgram()));
631
635
  }
632
636
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -1470,11 +1474,11 @@ function mapInner(r, f) {
1470
1474
  function mapOuter(r, f) {
1471
1475
  return r.matched ? f(r) : r;
1472
1476
  }
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)));
1477
+ function ab(pa, pb, join2) {
1478
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join2(ma.value, vb, data, i, j)));
1475
1479
  }
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))));
1480
+ function abc(pa, pb, pc, join2) {
1481
+ 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
1482
  }
1479
1483
  function ahead(p) {
1480
1484
  return (data, i) => mapOuter(p(data, i), (m1) => ({
@@ -5391,8 +5395,8 @@ function trimCharacterEnd(str, char) {
5391
5395
  function unicodeEscape(str) {
5392
5396
  return str.replace(/[\s\S]/g, (c) => "\\u" + c.charCodeAt().toString(16).padStart(4, "0"));
5393
5397
  }
5394
- function get(obj, path2) {
5395
- for (const key of path2) {
5398
+ function get(obj, path3) {
5399
+ for (const key of path3) {
5396
5400
  if (!obj) {
5397
5401
  return void 0;
5398
5402
  }
@@ -6593,8 +6597,8 @@ function withBrackets(str, brackets) {
6593
6597
  const rbr = typeof brackets[1] === "string" ? brackets[1] : "]";
6594
6598
  return lbr + str + rbr;
6595
6599
  }
6596
- function pathRewrite(path2, rewriter, baseUrl, metadata, elem) {
6597
- const modifiedPath = typeof rewriter === "function" ? rewriter(path2, metadata, elem) : path2;
6600
+ function pathRewrite(path3, rewriter, baseUrl, metadata, elem) {
6601
+ const modifiedPath = typeof rewriter === "function" ? rewriter(path3, metadata, elem) : path3;
6598
6602
  return modifiedPath[0] === "/" && baseUrl ? trimCharacterEnd(baseUrl, "/") + modifiedPath : modifiedPath;
6599
6603
  }
6600
6604
  function formatImage(elem, walk, builder, formatOptions) {
@@ -6918,9 +6922,9 @@ function handleDeprecatedOptions(options) {
6918
6922
  options.selectors.push(...tagDefinitions);
6919
6923
  options.selectors = mergeDuplicatesPreferLast(options.selectors, ((s) => s.selector));
6920
6924
  }
6921
- function set(obj, path2, value) {
6922
- const valueKey = path2.pop();
6923
- for (const key of path2) {
6925
+ function set(obj, path3, value) {
6926
+ const valueKey = path3.pop();
6927
+ for (const key of path3) {
6924
6928
  let nested = obj[key];
6925
6929
  if (!nested) {
6926
6930
  nested = {};
@@ -7011,7 +7015,7 @@ function preFetchHandler(event) {
7011
7015
  if (cached !== null) {
7012
7016
  const cachedBytes = Buffer.byteLength(cached, "utf-8");
7013
7017
  if (cachedBytes >= loadConfig().hints.web_dedup_min_bytes) {
7014
- recordStat("webfetch:recall", cachedBytes, Math.round(cachedBytes / 4));
7018
+ recordStat("webfetch:recall", cachedBytes, savedTokensFromBytes(cachedBytes));
7015
7019
  return denyOutput(
7016
7020
  "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
7021
  );
@@ -7084,9 +7088,7 @@ function postFetchHandler(event) {
7084
7088
  noticeBytes,
7085
7089
  minNetSavingsBytes: resolveMinNetSavingsBytes()
7086
7090
  })) {
7087
- const bytesDelta = originalBytes - rewrittenBytes;
7088
- recordStat("webfetch:compress", bytesDelta, Math.round(bytesDelta / 4));
7089
- return emitRewrite(storedRedacted.text + notice, "fetch");
7091
+ return emitRewrite(storedRedacted.text + notice, "fetch", { kind: "webfetch:compress", originalBytes });
7090
7092
  }
7091
7093
  }
7092
7094
  if (storedRedacted.count > 0) {
@@ -7146,7 +7148,7 @@ async function preSkillHandler(event) {
7146
7148
  if (await hasSessionOutput(event.sessionId, skillName)) {
7147
7149
  const cachedBytes = await sessionOutputBodyBytes(event.sessionId, skillName);
7148
7150
  const denyCredit = cachedBytes !== null ? Math.min(cachedBytes, PER_FILE_COUNTERFACTUAL_CEILING) : 0;
7149
- recordStat("session_hint", denyCredit, Math.round(denyCredit / 4));
7151
+ recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit));
7150
7152
  return denyOutput(
7151
7153
  "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
7154
  );
@@ -7161,7 +7163,7 @@ async function preSkillHandler(event) {
7161
7163
  const compactBytes = Buffer.byteLength(compact, "utf-8");
7162
7164
  if (compactBytes * 2 <= bodyBytes && compactBytes <= COMPACT_INLINE_MAX_BYTES) {
7163
7165
  const savedBytes = bodyBytes - compactBytes;
7164
- recordStat("skill_compact_inlined", savedBytes, Math.round(savedBytes / 4));
7166
+ recordStat("skill_compact_inlined", savedBytes, savedTokensFromBytes(savedBytes));
7165
7167
  return denyOutput(
7166
7168
  "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
7169
  );
@@ -7485,16 +7487,16 @@ function isTempPath(fp) {
7485
7487
  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
7488
  }
7487
7489
  function isOrchestratorStateFile(filePath) {
7488
- const basename2 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
7489
- return /^\.improve-state-/.test(basename2);
7490
+ const basename3 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
7491
+ return /^\.improve-state-/.test(basename3);
7490
7492
  }
7491
7493
  function extractCatSourceFile(cmd) {
7492
7494
  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
7495
  return m?.[1] ?? null;
7494
7496
  }
7495
7497
  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);
7498
+ const basename3 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
7499
+ const isEnvFile = /^\.env(\.\w+)?$/i.test(basename3);
7498
7500
  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
7501
  if (!hasKnownExt && !isEnvFile) return null;
7500
7502
  const isSql = /\.sql$/i.test(filePath);
@@ -7516,12 +7518,14 @@ function classifyCatPath(filePath, cmd0) {
7516
7518
  return { filePath, ...flags, cmd0 };
7517
7519
  }
7518
7520
  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);
7521
+ 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
7522
  if (!m) return null;
7521
7523
  const cmd0 = m[1];
7522
7524
  const filePath = m[2] ?? m[3] ?? m[4];
7523
7525
  if (filePath === void 0) return null;
7524
- return classifyCatPath(filePath, cmd0);
7526
+ const r = classifyCatPath(filePath, cmd0);
7527
+ if (r === null) return null;
7528
+ return { ...r, advisoryOnly: m[5] === "/dev/null" };
7525
7529
  }
7526
7530
  function extractCatFilesMulti(cmd) {
7527
7531
  if (/[|<>;&`]/.test(cmd) || cmd.includes("$(")) return null;
@@ -7769,11 +7773,12 @@ function extractPythonFileRead(cmd) {
7769
7773
  return null;
7770
7774
  }
7771
7775
  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);
7776
+ const direct = /^head(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
7777
+ 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;
7778
+ if (direct === null && piped === null) return null;
7779
+ const n = parseInt((direct !== null ? direct[1] ?? direct[2] : piped[4] ?? piped[5]) ?? "0", 10);
7775
7780
  if (n <= 10) return null;
7776
- const filePath = m[3] ?? m[4] ?? m[5];
7781
+ const filePath = direct !== null ? direct[3] ?? direct[4] ?? direct[5] : piped[1] ?? piped[2] ?? piped[3];
7777
7782
  if (filePath === void 0) return null;
7778
7783
  if (isTempPath(filePath)) return null;
7779
7784
  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 +7786,12 @@ function extractHeadFile(cmd) {
7781
7786
  return { filePath, isDoc, isConfig, isSql, n };
7782
7787
  }
7783
7788
  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);
7789
+ 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
7790
  if (!m) return null;
7786
- const quotedAddress = /['"]([^'"]+)['"]/.exec(cmd);
7787
- if (!quotedAddress) return null;
7791
+ const addressList = m[1] ?? m[2];
7792
+ if (addressList === void 0) return null;
7788
7793
  const ranges = [];
7789
- for (const clause of quotedAddress[1].split(";")) {
7794
+ for (const clause of addressList.split(";")) {
7790
7795
  const cm = /^(\d+),(\d+)p$/.exec(clause ?? "");
7791
7796
  if (!cm) return null;
7792
7797
  const start = parseInt(cm[1], 10);
@@ -7795,19 +7800,21 @@ function extractSedRange(cmd) {
7795
7800
  ranges.push([start, end]);
7796
7801
  }
7797
7802
  if (ranges.length === 0) return null;
7798
- const filePath = m[1] ?? m[2] ?? m[3];
7803
+ const filePath = m[3] ?? m[4] ?? m[5];
7799
7804
  if (filePath === void 0) return null;
7800
7805
  if (isTempPath(filePath)) return null;
7801
7806
  return { filePath, ranges };
7802
7807
  }
7803
7808
  function extractAwkRange(cmd) {
7804
- const m = /^awk\s+(?:'([^']+)'|"([^"]+)")\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>\/dev\/null)?\s*$/.exec(cmd);
7809
+ const m = /^awk\s+(?:'([^']+)'|"([^"]+)")\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>(?:\/dev\/null|&1))?\s*$/.exec(cmd);
7805
7810
  if (!m) return null;
7806
7811
  const program = m[1] ?? m[2];
7807
7812
  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);
7813
+ const cmp = /^\s*NR\s*>=\s*(\d+)\s*&&\s*NR\s*<=\s*(\d+)\s*(\{.*\})?\s*$/.exec(program);
7814
+ const rng = /^\s*NR\s*==\s*(\d+)\s*,\s*NR\s*==\s*(\d+)\s*(\{.*\})?\s*$/.exec(program);
7810
7815
  const hit = cmp ?? rng;
7816
+ const action = hit?.[3];
7817
+ 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
7818
  if (!hit) return null;
7812
7819
  const start = parseInt(hit[1], 10);
7813
7820
  const end = parseInt(hit[2], 10);
@@ -7817,13 +7824,47 @@ function extractAwkRange(cmd) {
7817
7824
  if (isTempPath(filePath)) return null;
7818
7825
  return { filePath, ranges: [[start, end]] };
7819
7826
  }
7827
+ function normalizeCatSedPipe(cmd) {
7828
+ 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);
7829
+ if (!m) return null;
7830
+ const filePath = m[1] ?? m[2] ?? m[3];
7831
+ if (filePath === void 0 || filePath.includes('"')) return null;
7832
+ return "sed -n " + m[4] + ' "' + filePath + '"';
7833
+ }
7820
7834
  function extractLineRangeRead(cmd) {
7835
+ const catSed = normalizeCatSedPipe(cmd);
7836
+ if (catSed !== null) cmd = catSed;
7821
7837
  const sed = extractSedRange(cmd);
7822
7838
  if (sed !== null) return { ...sed, tool: "sed" };
7823
7839
  const awk = extractAwkRange(cmd);
7824
7840
  if (awk !== null) return { ...awk, tool: "awk" };
7825
7841
  return null;
7826
7842
  }
7843
+ var FORMATTING_STAGE_RE = /^(?:fold|cut|cat|nl|head|tail)(?:\s+(?:-\S+|[\d,+-]+))*\s*$/;
7844
+ function extractLineRangeReadsCompound(cmd) {
7845
+ const cleaned = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
7846
+ const segments = splitShellSegments(cleaned);
7847
+ if (segments.length < 2) return null;
7848
+ const reads = [];
7849
+ for (const seg of segments) {
7850
+ const r = extractLineRangeRead(seg);
7851
+ if (r !== null) {
7852
+ reads.push(r);
7853
+ continue;
7854
+ }
7855
+ if (/^echo\b[^<>]*$/.test(seg)) continue;
7856
+ if (FORMATTING_STAGE_RE.test(seg)) continue;
7857
+ return null;
7858
+ }
7859
+ if (reads.length === 0) return null;
7860
+ const merged = /* @__PURE__ */ new Map();
7861
+ for (const r of reads) {
7862
+ const prev = merged.get(r.filePath);
7863
+ if (prev !== void 0) prev.ranges.push(...r.ranges);
7864
+ else merged.set(r.filePath, { filePath: r.filePath, ranges: [...r.ranges], tool: r.tool });
7865
+ }
7866
+ return [...merged.values()];
7867
+ }
7827
7868
  var SYMBOL_BEARING_LANGUAGES = /* @__PURE__ */ new Set([
7828
7869
  "python",
7829
7870
  "typescript",
@@ -7923,11 +7964,12 @@ function extractTailFile(cmd) {
7923
7964
  if (/-f\b/.test(cmd)) return null;
7924
7965
  if (/-c\b/.test(cmd)) return null;
7925
7966
  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);
7967
+ const direct = /^tail(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
7968
+ 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;
7969
+ if (direct === null && piped === null) return null;
7970
+ const n = parseInt((direct !== null ? direct[1] ?? direct[2] : piped[4] ?? piped[5]) ?? "0", 10);
7929
7971
  if (n <= 10) return null;
7930
- const filePath = m[3] ?? m[4] ?? m[5];
7972
+ const filePath = direct !== null ? direct[3] ?? direct[4] ?? direct[5] : piped[1] ?? piped[2] ?? piped[3];
7931
7973
  if (!filePath) return null;
7932
7974
  if (isTempPath(filePath)) return null;
7933
7975
  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 +8423,19 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
8381
8423
  });
8382
8424
  const minNet = resolveMinNetSavingsBytes();
8383
8425
  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);
8426
+ const id = await commandHash(cmd, cwd);
8386
8427
  const body = compressed.withMarker(minNet) + "\n[token-goat] full output: bash-output " + id + " --full";
8387
- return { hookType: "rewriteOutput", updatedOutput: body };
8428
+ const emittedBytes = Buffer.byteLength(body, "utf-8");
8429
+ if (!isRewriteWorthwhile({
8430
+ originalBytes: compressed.originalBytes,
8431
+ rewrittenBytes: emittedBytes,
8432
+ noticeBytes: 0,
8433
+ minNetSavingsBytes: minNet
8434
+ })) {
8435
+ return null;
8436
+ }
8437
+ await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8438
+ return emitRewrite(body, "bash", { kind: "bash_compress:generic", originalBytes: compressed.originalBytes });
8388
8439
  }
8389
8440
  function unwrapCompressCommand(executed) {
8390
8441
  const t = executed.trim();
@@ -8610,25 +8661,28 @@ function preBashHandlerInner(event) {
8610
8661
  "Use `token-goat outline <file>` to see symbol names and line counts without loading files."
8611
8662
  );
8612
8663
  }
8613
- const sedRange = extractLineRangeRead(cmd);
8614
- if (sedRange !== null) {
8615
- const { filePath, ranges, tool } = sedRange;
8616
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
8664
+ const singleLineRangeRead = extractLineRangeRead(cmd);
8665
+ const sedReads = singleLineRangeRead !== null ? [singleLineRangeRead] : extractLineRangeReadsCompound(cmd);
8666
+ if (sedReads !== null) {
8617
8667
  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]);
8668
+ const hints = [];
8669
+ for (const { filePath, ranges, tool } of sedReads) {
8670
+ const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
8671
+ const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
8672
+ const overlapHints = [];
8673
+ const freshRanges = [];
8674
+ for (const [start, end] of ranges) {
8675
+ const priorOverlap = findRangeOverlap(getFileLineRanges(sedDedupKey), start, end);
8676
+ recordFileLineRange(sedDedupKey, start, end);
8677
+ if (priorOverlap !== null) {
8678
+ overlapHints.push(sedOverlapHint(hintPath, priorOverlap, start, end));
8679
+ } else {
8680
+ freshRanges.push([start, end]);
8681
+ }
8628
8682
  }
8683
+ hints.push(...overlapHints);
8684
+ if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
8629
8685
  }
8630
- const hints = [...overlapHints];
8631
- if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
8632
8686
  return contextOutput(hints.join(" "));
8633
8687
  }
8634
8688
  const catJsonPipe = extractCatJsonPipe(cmd);
@@ -8642,7 +8696,7 @@ function preBashHandlerInner(event) {
8642
8696
  }
8643
8697
  const catResult = extractCatFile(cmd);
8644
8698
  if (catResult !== null) {
8645
- const { filePath, isDoc, isEnv, isConfig, isSql, cmd0 } = catResult;
8699
+ const { filePath, isDoc, isEnv, isConfig, isSql, cmd0, advisoryOnly } = catResult;
8646
8700
  const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
8647
8701
  recordStat("session_hint", 0, 0);
8648
8702
  if (isSql) {
@@ -8651,7 +8705,7 @@ function preBashHandlerInner(event) {
8651
8705
  );
8652
8706
  }
8653
8707
  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);
8708
+ return cdStripped || advisoryOnly ? contextOutput("`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
8655
8709
  }
8656
8710
  const catMulti = extractCatFilesMulti(cmd);
8657
8711
  if (catMulti !== null) {
@@ -8790,13 +8844,13 @@ function preBashHandlerInner(event) {
8790
8844
  const monBytes = monEntry.sizeBytes;
8791
8845
  const catFile = extractCatSourceFile(cmd);
8792
8846
  if (catFile !== null) {
8793
- recordStat("bash_compress:recall", monBytes, Math.round(monBytes / 4));
8847
+ recordStat("bash_compress:recall", monBytes, savedTokensFromBytes(monBytes));
8794
8848
  return contextOutput(
8795
8849
  "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
8850
  );
8797
8851
  }
8798
8852
  const cmdSummary = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
8799
- recordStat("bash_compress:recall", monBytes, Math.round(monBytes / 4));
8853
+ recordStat("bash_compress:recall", monBytes, savedTokensFromBytes(monBytes));
8800
8854
  return contextOutput(
8801
8855
  "Prior output from `" + cmdSummary + "`" + pipelineDivergenceNote(cmd, monEntry.command) + " is cached.\nUse `token-goat bash-output " + monOutputId + " " + monitoringHint + "` to re-inspect without re-running."
8802
8856
  );
@@ -8823,7 +8877,7 @@ function preBashHandlerInner(event) {
8823
8877
  const curlEntry = curlEntryRaw !== null && !isBashEntryStale(curlEntryRaw, cmd, preHookCwd) ? curlEntryRaw : null;
8824
8878
  if (curlOutputId !== null && curlEntry !== null && curlEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(curlEntry.sizeBytes)) {
8825
8879
  const curlBytes = curlEntry.sizeBytes;
8826
- recordStat("bash_compress:recall", curlBytes, Math.round(curlBytes / 4));
8880
+ recordStat("bash_compress:recall", curlBytes, savedTokensFromBytes(curlBytes));
8827
8881
  const curlPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
8828
8882
  return contextOutput(
8829
8883
  "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 +8891,7 @@ function preBashHandlerInner(event) {
8837
8891
  const ghEntry = ghEntryRaw !== null && !isBashEntryStale(ghEntryRaw, cmd, preHookCwd) ? ghEntryRaw : null;
8838
8892
  if (ghOutputId !== null && ghEntry !== null && ghEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(ghEntry.sizeBytes)) {
8839
8893
  const ghBytes = ghEntry.sizeBytes;
8840
- recordStat("bash_compress:recall", ghBytes, Math.round(ghBytes / 4));
8894
+ recordStat("bash_compress:recall", ghBytes, savedTokensFromBytes(ghBytes));
8841
8895
  const ghPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
8842
8896
  return contextOutput(
8843
8897
  "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 +8905,7 @@ function preBashHandlerInner(event) {
8851
8905
  const gitScopedEntry = gitScopedEntryRaw !== null && !isBashEntryStale(gitScopedEntryRaw, cmd, preHookCwd) ? gitScopedEntryRaw : null;
8852
8906
  if (gitScopedOutputId !== null && gitScopedEntry !== null && gitScopedEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(gitScopedEntry.sizeBytes)) {
8853
8907
  const gitScopedBytes = gitScopedEntry.sizeBytes;
8854
- recordStat("bash_compress:recall", gitScopedBytes, Math.round(gitScopedBytes / 4));
8908
+ recordStat("bash_compress:recall", gitScopedBytes, savedTokensFromBytes(gitScopedBytes));
8855
8909
  const gitScopedPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
8856
8910
  return contextOutput(
8857
8911
  "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 +8947,7 @@ function preBashHandlerInner(event) {
8893
8947
  const entry = entryRaw !== null && !isBashEntryStale(entryRaw, cmd, preHookCwd) ? entryRaw : null;
8894
8948
  if (outputId !== null && entry !== null && entry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(entry.sizeBytes)) {
8895
8949
  const bytes = entry.sizeBytes;
8896
- recordStat("bash_compress:recall", bytes, Math.round(bytes / 4));
8950
+ recordStat("bash_compress:recall", bytes, savedTokensFromBytes(bytes));
8897
8951
  return contextOutput(buildRecallHint(cmd, outputId));
8898
8952
  }
8899
8953
  return maybeCompressRewrite(event, rawCmd, cmd) ?? passOutput();
@@ -9189,12 +9243,23 @@ function getTaskId(toolInput) {
9189
9243
  const value = toolInput["task_id"];
9190
9244
  return typeof value === "string" && value !== "" ? value : void 0;
9191
9245
  }
9246
+ function extractTaskOutputText(raw) {
9247
+ const tr = raw["tool_response"];
9248
+ if (tr !== null && typeof tr === "object" && !Array.isArray(tr)) {
9249
+ const task = tr["task"];
9250
+ if (task !== null && typeof task === "object" && !Array.isArray(task)) {
9251
+ const output = task["output"];
9252
+ if (typeof output === "string") return output;
9253
+ }
9254
+ }
9255
+ return extractToolResultText(raw);
9256
+ }
9192
9257
  function postTaskOutputHandler(event) {
9193
9258
  try {
9194
9259
  if (getToolName(event) !== "TaskOutput" || !event.sessionId) return passOutput();
9195
9260
  const taskId = getTaskId(getToolInput(event));
9196
9261
  if (taskId === void 0) return passOutput();
9197
- const rawOutput = extractToolResultText(event.raw);
9262
+ const rawOutput = extractTaskOutputText(event.raw);
9198
9263
  if (!rawOutput) return passOutput();
9199
9264
  const output = redactSecrets(rawOutput).text;
9200
9265
  if (!output) return passOutput();
@@ -9366,10 +9431,10 @@ ${PLAN_OMIT_POINTER}`;
9366
9431
  })) {
9367
9432
  return passOutput();
9368
9433
  }
9369
- return {
9370
- hookType: "rewriteOutput",
9371
- updatedOutput: `${prefix}${notice}`
9372
- };
9434
+ return emitRewrite(`${prefix}${notice}`, "exitplanmode", {
9435
+ kind: "plan_echo_collapse",
9436
+ originalBytes: Buffer.byteLength(output, "utf-8")
9437
+ });
9373
9438
  } catch {
9374
9439
  return passOutput();
9375
9440
  }
@@ -9437,6 +9502,9 @@ function storeMcpOutput(sessionId, toolName, toolInput, resultText) {
9437
9502
  ${redactedOutput}`, entry.storedAt);
9438
9503
  return id;
9439
9504
  }
9505
+ function mcpOutputBytes(id) {
9506
+ return getBashOutput(id)?.sizeBytes ?? 0;
9507
+ }
9440
9508
  function getMcpOutput(sessionId, toolName, toolInput, ttlMs = Number.POSITIVE_INFINITY) {
9441
9509
  if (!sessionId) return null;
9442
9510
  const id = mcpOutputId(sessionId, mcpHash(toolName, toolInput));
@@ -9759,6 +9827,8 @@ function preMcpHandler(event) {
9759
9827
  const ttlMs = loadConfig().hints.mcp_dedup_ttl_secs * 1e3;
9760
9828
  const id = getMcpOutput(event.sessionId, toolName, toolInput, ttlMs);
9761
9829
  if (!id) return passOutput();
9830
+ const denyCredit = Math.min(mcpOutputBytes(id), PER_FILE_COUNTERFACTUAL_CEILING);
9831
+ recordStat("mcp:recall", denyCredit, savedTokensFromBytes(denyCredit));
9762
9832
  return denyOutput(
9763
9833
  "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
9834
  );
@@ -9803,8 +9873,9 @@ function postMcpHandler(event) {
9803
9873
  const redactedBody = redactSecrets(compressed).text;
9804
9874
  const notice = `[token-goat: compressed, full via mcp-output ${id}]
9805
9875
  `;
9876
+ const originalBytes = Buffer.byteLength(resultText, "utf-8");
9806
9877
  const worthwhile = isRewriteWorthwhile({
9807
- originalBytes: Buffer.byteLength(resultText, "utf-8"),
9878
+ originalBytes,
9808
9879
  rewrittenBytes: Buffer.byteLength(redactedBody, "utf-8"),
9809
9880
  noticeBytes: Buffer.byteLength(notice, "utf-8"),
9810
9881
  minNetSavingsBytes: resolveMinNetSavingsBytes()
@@ -9812,7 +9883,8 @@ function postMcpHandler(event) {
9812
9883
  if (worthwhile) {
9813
9884
  return emitRewrite(
9814
9885
  injectionMatches.length > 0 ? `${notice}${fenceUntrustedContent(redactedBody, injectionMatches, UNTRUSTED_TOOL_TAG)}` : `${notice}${redactedBody}`,
9815
- "mcp"
9886
+ "mcp",
9887
+ { kind: "mcp:compress", originalBytes }
9816
9888
  );
9817
9889
  }
9818
9890
  }
@@ -9934,13 +10006,15 @@ async function shrinkImageBlock(block) {
9934
10006
  const mediaType = typeof source?.media_type === "string" ? source.media_type : "image/png";
9935
10007
  const data = typeof source?.data === "string" ? source.data : "";
9936
10008
  const originalDataUrl = `data:${mediaType};base64,${data}`;
9937
- if (source?.type !== "base64" || data === "") return { text: originalDataUrl, changed: false, savedBytes: 0 };
10009
+ if (source?.type !== "base64" || data === "") return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
9938
10010
  let buffer;
9939
10011
  try {
9940
10012
  buffer = Buffer.from(data, "base64");
9941
10013
  } catch {
9942
- return { text: originalDataUrl, changed: false, savedBytes: 0 };
10014
+ return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
9943
10015
  }
10016
+ const meta = await probeImageMeta(buffer);
10017
+ const tier = loadConfig().image_shrink.vision_tier;
9944
10018
  const fingerprint = screenshotFingerprint(data);
9945
10019
  const alreadyShown = hasSeenImage(fingerprint);
9946
10020
  recordSeenImage(fingerprint);
@@ -9955,16 +10029,23 @@ async function shrinkImageBlock(block) {
9955
10029
  return {
9956
10030
  text: SCREENSHOT_REPEAT_NOTICE,
9957
10031
  changed: true,
9958
- savedBytes: originalDataUrl.length - SCREENSHOT_REPEAT_NOTICE.length
10032
+ // 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.
10033
+ savedBytes: buffer.length - Buffer.byteLength(SCREENSHOT_REPEAT_NOTICE, "utf-8"),
10034
+ // The whole image is withheld here rather than resized, so the visual tokens saved are its
10035
+ // entire billed cost less the notice standing in for it. Billed cost, not raw patch count:
10036
+ // an oversized screenshot would have been capped by the API's own downscale, so pricing the
10037
+ // untouched dimensions would credit a bill that was never going to be sent.
10038
+ savedTokens: Math.max(0, visionTokens(meta?.width ?? 0, meta?.height ?? 0, tier) - savedTokensFromBytes(Buffer.byteLength(SCREENSHOT_REPEAT_NOTICE, "utf-8")))
9959
10039
  };
9960
10040
  }
9961
10041
  }
9962
10042
  const result = await imageQualifiesForShrink(buffer) ? await shrinkImage(buffer, { sizeThresholdBytes: 0 }) : null;
9963
- if (result === null) return { text: originalDataUrl, changed: false, savedBytes: 0 };
10043
+ if (result === null) return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
9964
10044
  const saved = result.originalBytes - result.shrunkBytes;
9965
10045
  const { summary, dataUrl } = formatShrinkSummary(result, "an inline browser screenshot");
10046
+ const savedTokens = visionTokensSaved(result.originalWidth, result.originalHeight, result.width, result.height, tier);
9966
10047
  return { text: `${summary}
9967
- ${dataUrl}`, changed: true, savedBytes: saved };
10048
+ ${dataUrl}`, changed: true, savedBytes: saved, savedTokens };
9968
10049
  }
9969
10050
  var TAB_CONTEXT_UNCHANGED_NOTICE = "(tabs unchanged since last check)";
9970
10051
  function dedupTabContext(text) {
@@ -9992,15 +10073,19 @@ async function postBrowserImageHandler(event) {
9992
10073
  const parts = [];
9993
10074
  for (const block of blocks) {
9994
10075
  if (block.type === "image") {
9995
- const { text, changed, savedBytes } = await shrinkImageBlock(block);
10076
+ const { text, changed, savedBytes, savedTokens } = await shrinkImageBlock(block);
9996
10077
  if (changed) {
9997
10078
  anyChanged = true;
9998
- recordStat("image_shrink", savedBytes, Math.round(savedBytes / 4), void 0, toolName);
10079
+ recordStat("image_shrink", savedBytes, savedTokens, void 0, toolName);
9999
10080
  }
10000
10081
  parts.push(text);
10001
10082
  } else if (block.type === "text" && typeof block.text === "string") {
10002
10083
  const { text, changed } = dedupTabContext(block.text);
10003
- if (changed) anyChanged = true;
10084
+ if (changed) {
10085
+ anyChanged = true;
10086
+ const savedBytes = Buffer.byteLength(block.text, "utf-8") - Buffer.byteLength(text, "utf-8");
10087
+ if (savedBytes > 0) recordStat("browser_tab_dedup", savedBytes, savedTokensFromBytes(savedBytes), void 0, toolName);
10088
+ }
10004
10089
  parts.push(text);
10005
10090
  } else {
10006
10091
  try {
@@ -10011,7 +10096,7 @@ async function postBrowserImageHandler(event) {
10011
10096
  }
10012
10097
  }
10013
10098
  if (!anyChanged) return passOutput();
10014
- return { hookType: "rewriteOutput", updatedOutput: parts.join("\n") };
10099
+ return emitRewrite(parts.join("\n"), toolName);
10015
10100
  } catch {
10016
10101
  return passOutput();
10017
10102
  }
@@ -10020,6 +10105,9 @@ registerHook("post_tool_use", postBrowserImageHandler, { toolPattern: "^mcp__" }
10020
10105
 
10021
10106
  // src/hooks_agent_spawn.ts
10022
10107
  import { createHash as createHash2 } from "node:crypto";
10108
+ import * as fs from "node:fs";
10109
+ import * as os from "node:os";
10110
+ import * as path2 from "node:path";
10023
10111
  var BRIEFING_TARGET_TOKENS = 550;
10024
10112
  function buildSubagentBriefing() {
10025
10113
  try {
@@ -10091,8 +10179,11 @@ function findDuplicateOutstandingPrompt(prompt) {
10091
10179
  function truncateForWarning(text, max) {
10092
10180
  return text.length > max ? text.slice(0, max) + "..." : text;
10093
10181
  }
10182
+ function isAgentTool(toolName) {
10183
+ return toolName === "Agent" || toolName === "task" || toolName === "Task";
10184
+ }
10094
10185
  function preAgentHandler(event) {
10095
- if (event.toolName !== "Agent") return passOutput();
10186
+ if (!isAgentTool(event.toolName)) return passOutput();
10096
10187
  const toolInput = event.toolInput;
10097
10188
  const prompt = toolInput["prompt"];
10098
10189
  if (typeof prompt !== "string" || prompt.trim() === "") return passOutput();
@@ -10114,7 +10205,7 @@ function preAgentHandler(event) {
10114
10205
  return passOutput();
10115
10206
  }
10116
10207
  }
10117
- var FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
10208
+ var FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})([^\n]*)$/;
10118
10209
  function findFencedBlockLines(lines) {
10119
10210
  const blocks = [];
10120
10211
  let fenceStart = -1;
@@ -10214,9 +10305,100 @@ function collapseBlankRunsInFences(text) {
10214
10305
  out.push(...lines.slice(cursor));
10215
10306
  return changedAny ? out.join("\n") : text;
10216
10307
  }
10308
+ var SPAWN_RESTRICT_HINT_KEY = "agent-spawn-restrict-hint";
10309
+ var SPAWN_RESTRICT_MAX_NAMES = 3;
10310
+ var ROSTER_WALK_MAX_DEPTH = 4;
10311
+ var ROSTER_WALK_MAX_FILES = 400;
10312
+ function parseAgentDefinition(text, fallbackName) {
10313
+ const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
10314
+ if (!fmMatch) return null;
10315
+ const fm = fmMatch[1];
10316
+ const nameMatch = /^name:(.*)$/m.exec(fm);
10317
+ const rawName = nameMatch ? nameMatch[1].trim().replace(/^["']|["']$/g, "") : "";
10318
+ const name2 = rawName !== "" ? rawName : fallbackName;
10319
+ const toolsMatch = /^tools:(.*)$/m.exec(fm);
10320
+ if (!toolsMatch) return { name: name2, restricted: false };
10321
+ const inline = toolsMatch[1].trim();
10322
+ if (inline === "*" || inline === '"*"' || inline === "'*'") return { name: name2, restricted: false };
10323
+ if (inline !== "") return { name: name2, restricted: true };
10324
+ const after = fm.slice(toolsMatch.index + toolsMatch[0].length);
10325
+ for (const line of after.split(/\r?\n/)) {
10326
+ if (/^\s+-\s*\S/.test(line)) return { name: name2, restricted: true };
10327
+ if (/^\S/.test(line)) break;
10328
+ }
10329
+ return { name: name2, restricted: false };
10330
+ }
10331
+ function findRestrictedAgentNames(roots) {
10332
+ const scanRoots = roots ?? [path2.join(os.homedir(), ".claude", "agents")];
10333
+ const names = /* @__PURE__ */ new Set();
10334
+ const visited = /* @__PURE__ */ new Set();
10335
+ let filesSeen = 0;
10336
+ const walk = (dir, depth) => {
10337
+ if (depth > ROSTER_WALK_MAX_DEPTH || filesSeen >= ROSTER_WALK_MAX_FILES) return;
10338
+ let real;
10339
+ try {
10340
+ real = fs.realpathSync(dir);
10341
+ } catch {
10342
+ return;
10343
+ }
10344
+ const key = process.platform === "win32" ? real.toLocaleLowerCase() : real;
10345
+ if (visited.has(key)) return;
10346
+ visited.add(key);
10347
+ let entries;
10348
+ try {
10349
+ entries = fs.readdirSync(dir, { withFileTypes: true });
10350
+ } catch {
10351
+ return;
10352
+ }
10353
+ for (const entry of entries) {
10354
+ if (filesSeen >= ROSTER_WALK_MAX_FILES) return;
10355
+ const full = path2.join(dir, entry.name);
10356
+ let stat;
10357
+ try {
10358
+ stat = fs.statSync(full);
10359
+ } catch {
10360
+ continue;
10361
+ }
10362
+ if (stat.isDirectory()) {
10363
+ walk(full, depth + 1);
10364
+ continue;
10365
+ }
10366
+ if (!stat.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
10367
+ filesSeen++;
10368
+ let text;
10369
+ try {
10370
+ text = fs.readFileSync(full, "utf-8");
10371
+ } catch {
10372
+ continue;
10373
+ }
10374
+ const parsed = parseAgentDefinition(text, path2.basename(entry.name, path2.extname(entry.name)));
10375
+ if (parsed !== null && parsed.restricted) names.add(parsed.name);
10376
+ }
10377
+ };
10378
+ for (const root of scanRoots) walk(root, 0);
10379
+ return Array.from(names).sort();
10380
+ }
10381
+ function buildUnrestrictedSpawnAdvisory(toolInput) {
10382
+ try {
10383
+ if (getHarnessName() === "copilot_cli") return "";
10384
+ const rawType = toolInput["subagent_type"];
10385
+ const spawnType = typeof rawType === "string" ? rawType.trim() : "";
10386
+ if (spawnType !== "" && spawnType !== "general-purpose") return "";
10387
+ if (wasHintShown(SPAWN_RESTRICT_HINT_KEY)) return "";
10388
+ const names = findRestrictedAgentNames();
10389
+ if (names.length === 0) return "";
10390
+ markHintShown(SPAWN_RESTRICT_HINT_KEY);
10391
+ recordStat("session_hint", 0, 0, void 0, "agent-spawn-restrict");
10392
+ const shown = names.slice(0, SPAWN_RESTRICT_MAX_NAMES).join(", ");
10393
+ const more = names.length > SPAWN_RESTRICT_MAX_NAMES ? ` and ${names.length - SPAWN_RESTRICT_MAX_NAMES} more` : "";
10394
+ 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.`;
10395
+ } catch {
10396
+ return "";
10397
+ }
10398
+ }
10217
10399
  function postAgentHandler(event) {
10218
10400
  try {
10219
- if (event.toolName !== "Agent" || !event.sessionId) return passOutput();
10401
+ if (!isAgentTool(event.toolName) || !event.sessionId) return passOutput();
10220
10402
  const finishedPrompt = event.toolInput["prompt"];
10221
10403
  if (typeof finishedPrompt === "string" && finishedPrompt !== "") {
10222
10404
  removeOutstandingAgentSpawn(finishedPrompt);
@@ -10224,13 +10406,17 @@ function postAgentHandler(event) {
10224
10406
  const redactedReport = redactSecrets(extractToolResultText(event.raw));
10225
10407
  const resultText = redactedReport.text;
10226
10408
  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();
10409
+ const spawnAdvisory = buildUnrestrictedSpawnAdvisory(event.toolInput);
10410
+ if (!resultText || resultText.length < agentReportCfg.min_bytes) {
10411
+ return spawnAdvisory === "" ? passOutput() : contextOutput(spawnAdvisory);
10412
+ }
10413
+ const id = storeMcpOutput(event.sessionId, event.toolName ?? "Agent", event.toolInput, resultText);
10414
+ if (id === null) return spawnAdvisory === "" ? passOutput() : contextOutput(spawnAdvisory);
10230
10415
  if (redactedReport.count > 0) recordStat("secret_redacted", 0, redactedReport.count, void 0, "agent");
10231
10416
  recordStat("session_hint", 0, 0);
10232
10417
  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}`;
10418
+ const notice = `[token-goat] This subagent report (${toKB(resultText.length)}KB) is cached for later recall: ${recallHint}${spawnAdvisory === "" ? "" : `
10419
+ ${spawnAdvisory}`}`;
10234
10420
  const collapsed = collapseFencedBlocks(resultText, recallHint, agentReportCfg.fence_collapse_min_lines, agentReportCfg.fence_collapse_keep_lines);
10235
10421
  const deduped = dedupeFencedBlocks(collapsed, resultText, recallHint);
10236
10422
  const final = collapseBlankRunsInFences(deduped);
@@ -10247,8 +10433,8 @@ function postAgentHandler(event) {
10247
10433
 
10248
10434
  ${notice}`;
10249
10435
  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 };
10436
+ if (savedBytes > 0) recordStat("agent_report_compact", savedBytes, savedTokensFromBytes(savedBytes));
10437
+ return emitRewrite(updatedOutput, "agent", void 0, "counted-elsewhere");
10252
10438
  }
10253
10439
  recordStat("agent_report_compact_declined", 0, 0);
10254
10440
  }
@@ -10258,7 +10444,11 @@ ${notice}`;
10258
10444
  }
10259
10445
  }
10260
10446
  registerHook("pre_tool_use", preAgentHandler, { toolName: "Agent" });
10447
+ registerHook("pre_tool_use", preAgentHandler, { toolName: "task" });
10448
+ registerHook("pre_tool_use", preAgentHandler, { toolName: "Task" });
10261
10449
  registerHook("post_tool_use", postAgentHandler, { toolName: "Agent" });
10450
+ registerHook("post_tool_use", postAgentHandler, { toolName: "task" });
10451
+ registerHook("post_tool_use", postAgentHandler, { toolName: "Task" });
10262
10452
 
10263
10453
  // src/relay.ts
10264
10454
  function isHookEventName(name2) {
@@ -10274,7 +10464,11 @@ function buildEvent(eventName, payload) {
10274
10464
  const sessionId = typeof rawSession === "string" ? rawSession : "";
10275
10465
  const rawAgentId = obj["agent_id"] ?? obj["agentId"];
10276
10466
  const agentId = typeof rawAgentId === "string" && rawAgentId !== "" ? rawAgentId : void 0;
10277
- return { eventName, toolName, toolInput, sessionId, agentId, raw: obj };
10467
+ const rawTraceparent = obj["traceparent"] ?? obj["traceParent"] ?? process.env["TRACEPARENT"] ?? process.env["traceparent"];
10468
+ const traceparent = typeof rawTraceparent === "string" && rawTraceparent.trim() !== "" ? rawTraceparent.trim() : void 0;
10469
+ const rawTracestate = obj["tracestate"] ?? obj["traceState"] ?? process.env["TRACESTATE"] ?? process.env["tracestate"];
10470
+ const tracestate = typeof rawTracestate === "string" && rawTracestate.trim() !== "" ? rawTracestate.trim() : void 0;
10471
+ return { eventName, toolName, toolInput, sessionId, agentId, traceparent, tracestate, raw: obj };
10278
10472
  }
10279
10473
  function sessionStateKey(event) {
10280
10474
  return event.agentId !== void 0 ? `${event.sessionId}:agent:${event.agentId}` : event.sessionId;
@@ -10285,6 +10479,7 @@ function harnessForNormalization() {
10285
10479
  if (detected === "gemini") return "gemini";
10286
10480
  if (detected === "grok") return "grok";
10287
10481
  if (detected === "kimi") return "kimi";
10482
+ if (detected === "qwen") return "qwen";
10288
10483
  if (detected === "copilot_cli") return "copilot_cli";
10289
10484
  return "claude";
10290
10485
  }
@@ -10299,6 +10494,12 @@ async function relayInProcess(eventName, rawPayload) {
10299
10494
  if (!process.env["CLAUDE_CODE_SESSION_ID"] && event.sessionId) {
10300
10495
  process.env["CLAUDE_CODE_SESSION_ID"] = event.sessionId;
10301
10496
  }
10497
+ if (!process.env["TRACEPARENT"] && event.traceparent) {
10498
+ process.env["TRACEPARENT"] = event.traceparent;
10499
+ }
10500
+ if (!process.env["TRACESTATE"] && event.tracestate) {
10501
+ process.env["TRACESTATE"] = event.tracestate;
10502
+ }
10302
10503
  const stateKey = sessionStateKey(event);
10303
10504
  try {
10304
10505
  loadSessionState(stateKey);