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.
@@ -8,7 +8,7 @@ import {
8
8
  import { createRequire } from "node:module";
9
9
  function resolveVersion() {
10
10
  if (true) {
11
- return "2.8.3";
11
+ return "2.8.4";
12
12
  }
13
13
  const require2 = createRequire(import.meta.url);
14
14
  const pkg = require2("../package.json");
@@ -263,6 +263,16 @@ function lowercaseDriveLetter(s) {
263
263
  }
264
264
  var EXTENDED_UNC_PREFIX_RE = /^\\\\\?\\UNC\\/i;
265
265
  var EXTENDED_PREFIX_RE = /^\\\\\?\\/;
266
+ function shellMountToWindowsPath(p) {
267
+ const s = p.includes("\\") ? p.replace(/\\/g, "/") : p;
268
+ const m = WSL_PATH_RE.exec(s);
269
+ if (m) return `${m[1].toLowerCase()}:/${m[2].replace(/^\/+/, "")}`;
270
+ if (process.platform === "win32") {
271
+ const g = MSYS_PATH_RE.exec(s);
272
+ if (g) return `${g[1].toLowerCase()}:${g[2] ?? "/"}`;
273
+ }
274
+ return s;
275
+ }
266
276
  function normalizePath(p) {
267
277
  let s = p;
268
278
  if (EXTENDED_UNC_PREFIX_RE.test(s)) {
@@ -270,20 +280,7 @@ function normalizePath(p) {
270
280
  } else if (EXTENDED_PREFIX_RE.test(s)) {
271
281
  s = s.slice(4);
272
282
  }
273
- if (s.includes("\\")) {
274
- s = s.replace(/\\/g, "/");
275
- }
276
- const m = WSL_PATH_RE.exec(s);
277
- if (m) {
278
- const driveLetter = m[1].toLowerCase();
279
- const rest = m[2];
280
- const restStripped = rest.replace(/^\/+/, "");
281
- s = `${driveLetter}:/${restStripped}`;
282
- }
283
- if (process.platform === "win32") {
284
- const g = MSYS_PATH_RE.exec(s);
285
- if (g) s = `${g[1].toLowerCase()}:${g[2] ?? "/"}`;
286
- }
283
+ s = shellMountToWindowsPath(s);
287
284
  s = expandShortPath(s);
288
285
  s = lowercaseDriveLetter(s);
289
286
  s = normalizeDarwinSystemAlias(s);
@@ -297,8 +294,10 @@ function normalizeDarwinSystemAlias(p) {
297
294
  }
298
295
  function resolveIndexPath(file, base = process.cwd()) {
299
296
  const isWindowsAbsolute = (s) => /^[a-zA-Z]:[/\\]/.test(s);
300
- const resolve5 = isWindowsAbsolute(file) || isWindowsAbsolute(base) ? path2.win32.resolve : path2.resolve;
301
- return normalizePath(resolve5(base, file));
297
+ const f = shellMountToWindowsPath(file);
298
+ const b = shellMountToWindowsPath(base);
299
+ const resolve5 = isWindowsAbsolute(f) || isWindowsAbsolute(b) ? path2.win32.resolve : path2.resolve;
300
+ return normalizePath(resolve5(b, f));
302
301
  }
303
302
  function toDisplayPath(root, target) {
304
303
  if (root === void 0) return target;
@@ -759,6 +758,14 @@ function grepFilteredToEmptyNotice(preFilterCount, grep, nounSingular, nounPlura
759
758
  const pronoun = preFilterCount === 1 ? "it" : "them";
760
759
  return ` (all ${preFilterCount} ${noun} ${verb} filtered out by --grep ${grep} -- widen or drop the filter to see ${pronoun})`;
761
760
  }
761
+ function filtersFilteredToEmptyNotice(preFilterCount, activeFilters, nounSingular, nounPlural, reassurance) {
762
+ const noun = preFilterCount === 1 ? nounSingular : nounPlural;
763
+ const cause = activeFilters.length === 0 ? "the active filter" : activeFilters.join(" + ");
764
+ const knob = activeFilters.length > 1 ? "filters" : "filter";
765
+ const verb = preFilterCount === 1 ? "was" : "were";
766
+ const tail = reassurance === void 0 ? "" : `; ${reassurance}`;
767
+ return ` (all ${preFilterCount} ${noun} ${verb} filtered out by ${cause}${tail} -- widen or drop the ${knob} to see them)`;
768
+ }
762
769
  function countNoun(count, singular, plural = `${singular}s`) {
763
770
  return `${count} ${count === 1 ? singular : plural}`;
764
771
  }
@@ -2161,7 +2168,15 @@ var CONFIG_DEFAULTS = {
2161
2168
  // 85+, while a photo with an incidental sign or logo in frame scores much lower and
2162
2169
  // noisier -- padding the threshold below the terminal/code norm still comfortably
2163
2170
  // excludes photographic false positives without needing a second heuristic.
2164
- ocr_min_confidence: 65
2171
+ ocr_min_confidence: 65,
2172
+ // Which resolution tier the model being shown the image is on, which decides what its pixels
2173
+ // cost. 'standard' (1568px long edge, 1568 visual tokens) is every model before Claude 4.7;
2174
+ // 'high' (2576px, 4784 tokens) is 4.7 and later, and bills the same large image up to roughly
2175
+ // three times higher. Only the saving *reported* by `token-goat stats` depends on this -- no
2176
+ // image is encoded differently -- and 'standard' is the default because it is the floor: it
2177
+ // caps the counterfactual at the smaller of the two bills and so can never credit a saving
2178
+ // that was not there. Set it to 'high' on a Claude 4.7+ model to see the larger real figure.
2179
+ vision_tier: "standard"
2165
2180
  },
2166
2181
  screenshot: {
2167
2182
  chrome_path: "",
@@ -2321,6 +2336,9 @@ function validatedFloat(raw, def, min, max) {
2321
2336
  if (!Number.isFinite(n)) return def;
2322
2337
  return Math.max(min, Math.min(max, n));
2323
2338
  }
2339
+ function validatedVisionTier(raw, def) {
2340
+ return raw === "standard" || raw === "high" ? raw : def;
2341
+ }
2324
2342
  function validatedStr(raw, def) {
2325
2343
  return typeof raw === "string" ? raw : def;
2326
2344
  }
@@ -2684,6 +2702,8 @@ function mergeRawConfig(base, override) {
2684
2702
  const baseSection = baseVal !== null && typeof baseVal === "object" && !Array.isArray(baseVal) ? baseVal : {};
2685
2703
  merged[key] = { ...baseSection, ...overrideVal };
2686
2704
  } else {
2705
+ const baseVal = base[key];
2706
+ if (baseVal !== null && typeof baseVal === "object" && !Array.isArray(baseVal)) continue;
2687
2707
  merged[key] = overrideVal;
2688
2708
  }
2689
2709
  }
@@ -2877,8 +2897,10 @@ function _buildConfig(raw, projectRaw = {}) {
2877
2897
  is_cfg.screenshot_redirect = validatedBool(is_raw["screenshot_redirect"], is_cfg.screenshot_redirect);
2878
2898
  is_cfg.ocr_enabled = validatedBool(is_raw["ocr_enabled"], is_cfg.ocr_enabled);
2879
2899
  is_cfg.ocr_min_confidence = validatedInt(is_raw["ocr_min_confidence"], is_cfg.ocr_min_confidence, ...boundsOf("image_shrink.ocr_min_confidence"));
2900
+ is_cfg.vision_tier = validatedVisionTier(is_raw["vision_tier"], is_cfg.vision_tier);
2880
2901
  is_cfg.max_image_pixels = envInt("TOKEN_GOAT_MAX_IMAGE_PIXELS", is_cfg.max_image_pixels, ...boundsOf("image_shrink.max_image_pixels"));
2881
2902
  is_cfg.ocr_enabled = envBool("TOKEN_GOAT_OCR_ENABLED", is_cfg.ocr_enabled);
2903
+ is_cfg.vision_tier = validatedVisionTier(process.env["TOKEN_GOAT_VISION_TIER"], is_cfg.vision_tier);
2882
2904
  const sc_raw = section(raw, "screenshot");
2883
2905
  const sc_cfg = getDefaultConfig("screenshot");
2884
2906
  sc_cfg.chrome_path = validatedStr(sc_raw["chrome_path"], sc_cfg.chrome_path);
@@ -3077,6 +3099,7 @@ var CONFIG_KEY_ENV_OVERRIDES = {
3077
3099
  "skill_preservation.orphan_sweep_enabled": ["TOKEN_GOAT_ORPHAN_SWEEP"],
3078
3100
  "image_shrink.max_image_pixels": ["TOKEN_GOAT_MAX_IMAGE_PIXELS"],
3079
3101
  "image_shrink.ocr_enabled": ["TOKEN_GOAT_OCR_ENABLED"],
3102
+ "image_shrink.vision_tier": ["TOKEN_GOAT_VISION_TIER"],
3080
3103
  "screenshot.block_private_targets": ["TOKEN_GOAT_SCREENSHOT_BLOCK_PRIVATE_TARGETS"],
3081
3104
  "repomap.compact_file_threshold": ["TOKEN_GOAT_REPOMAP_COMPACT_THRESHOLD"],
3082
3105
  "repomap.exclude_tests": ["TOKEN_GOAT_REPOMAP_EXCLUDE_TESTS"],
@@ -3199,7 +3222,8 @@ function saveConfig(config) {
3199
3222
  max_image_pixels: is_cfg.max_image_pixels,
3200
3223
  screenshot_redirect: is_cfg.screenshot_redirect,
3201
3224
  ocr_enabled: is_cfg.ocr_enabled,
3202
- ocr_min_confidence: is_cfg.ocr_min_confidence
3225
+ ocr_min_confidence: is_cfg.ocr_min_confidence,
3226
+ vision_tier: is_cfg.vision_tier
3203
3227
  },
3204
3228
  screenshot: {
3205
3229
  chrome_path: config.screenshot.chrome_path,
@@ -4175,9 +4199,11 @@ function makeSymbolEmitter(symbols, sections, seen, filePath, maxSymbols = 500,
4175
4199
  }
4176
4200
  function assignFlatEndLines(sections, totalLines) {
4177
4201
  for (let i = 0; i < sections.length; i++) {
4178
- const next = sections[i + 1];
4179
4202
  const s = sections[i];
4180
4203
  if (s === void 0) continue;
4204
+ let j = i + 1;
4205
+ while (j < sections.length && (sections[j]?.line ?? 0) <= s.line) j++;
4206
+ const next = sections[j];
4181
4207
  const end = next !== void 0 ? next.line - 1 : totalLines;
4182
4208
  s.endLine = end < s.line ? s.line : end;
4183
4209
  }
@@ -4472,6 +4498,7 @@ function findBlockOpenBrace(content, lineIndex, startLine, lastSearchLine, lineC
4472
4498
  else if (ch === ")" || ch === "]") {
4473
4499
  if (parenDepth > 0) parenDepth--;
4474
4500
  } else if (ch === ";") return null;
4501
+ else if (ch === "}" && parenDepth === 0) return null;
4475
4502
  else if (ch === "{") return i;
4476
4503
  }
4477
4504
  return null;
@@ -4567,8 +4594,9 @@ function extractEnv(content, filePath) {
4567
4594
  if (_lineClosesQuote(line, openQuote)) openQuote = null;
4568
4595
  continue;
4569
4596
  }
4570
- if (!line || line[0] === "#" || line[0] === ";" || line[0] === " " || line[0] === " ") continue;
4571
- const m = ENV_KEY_RE.exec(line);
4597
+ const trimmed = line.replace(/^[ \t]+/, "");
4598
+ if (!trimmed || trimmed[0] === "#" || trimmed[0] === ";") continue;
4599
+ const m = ENV_KEY_RE.exec(trimmed);
4572
4600
  if (m === null) continue;
4573
4601
  const name = m[1]?.trim() ?? "";
4574
4602
  if (!name || name.length > MAX_HEADING_LEN) continue;
@@ -4576,7 +4604,7 @@ function extractEnv(content, filePath) {
4576
4604
  if (seen.has(key)) continue;
4577
4605
  seen.add(key);
4578
4606
  symbols.push(makeLineSymbol(filePath, name, "env_key", i + 1));
4579
- openQuote = _detectOpenQuote(line.slice(m[0].length));
4607
+ openQuote = _detectOpenQuote(trimmed.slice(m[0].length));
4580
4608
  }
4581
4609
  return symbols;
4582
4610
  }
@@ -4955,7 +4983,8 @@ CREATE TABLE IF NOT EXISTS files (
4955
4983
  language TEXT,
4956
4984
  indexed_at REAL,
4957
4985
  embed_sha TEXT,
4958
- retry_count INTEGER NOT NULL DEFAULT 0
4986
+ retry_count INTEGER NOT NULL DEFAULT 0,
4987
+ parser_sha TEXT
4959
4988
  );
4960
4989
  -- Expression index on TG_LOWER(path) -- see pathEqClause (sql_path.ts) and TG_LOWER's
4961
4990
  -- registration above. TG_LOWER is registered { deterministic: true }, which is required for
@@ -5215,7 +5244,7 @@ CREATE TRIGGER IF NOT EXISTS cache_recall_au AFTER UPDATE ON cache_recall BEGIN
5215
5244
  VALUES (new.row_id, new.label, new.content);
5216
5245
  END;
5217
5246
  `;
5218
- var SCHEMA_VERSION = 12;
5247
+ var SCHEMA_VERSION = 13;
5219
5248
  function alterTableIdempotent(conn, sql) {
5220
5249
  try {
5221
5250
  conn.exec(sql);
@@ -5259,7 +5288,9 @@ var MIGRATIONS = {
5259
5288
  // .env would have kept serving its pre-fix chunks indefinitely. Deleting the rows here both
5260
5289
  // removes the stored secrets and, by clearing embed_sha, makes the next drain re-embed the file
5261
5290
  // through the redacting path.
5262
- 10: purgeDotenvEmbeddings
5291
+ 10: purgeDotenvEmbeddings,
5292
+ // v12 -> v13: adds files.parser_sha, the digest of the extraction logic that produced this file's rows, tracked separately from files.sha for the same reason embed_sha is -- content freshness and parse freshness are different questions, and the content sha alone could only ever answer the first. A pre-existing v12 database's `files` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has it from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, same pattern as v1 -> v2 / v2 -> v3 / v8 -> v9 / v9 -> v10 above. Deliberately left NULL for every existing row rather than backfilled with the current fingerprint: NULL is the truthful answer (nobody recorded which parser wrote those rows), and it is also the answer that makes the freshness gates reparse them once, which is exactly what a database indexed by an older parser needs.
5293
+ 12: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN parser_sha TEXT")
5263
5294
  };
5264
5295
  function runMigrations(conn, fromVersion, toVersion) {
5265
5296
  for (let v = fromVersion; v < toVersion; v++) {
@@ -5745,7 +5776,51 @@ var _KIND_GROUPS = [
5745
5776
  "exports",
5746
5777
  "imports",
5747
5778
  "changed_lookup",
5748
- "dep_docs"
5779
+ "dep_docs",
5780
+ // Every other SOURCE_READ kind in stats.ts's KIND_TO_SOURCE: the surgical-read commands over documents, structured data and session/PR state. They were registered and produced but grouped nowhere, so `stats --full` printed the whole family under 'Other', away from the read-savings siblings they are measured against. image_meta/image_text sit here rather than under 'Images' because stats.ts files them as SOURCE_READ: they save read bytes, they do not shrink an image.
5781
+ "brief_view",
5782
+ "conflicts",
5783
+ "coverage_report_gaps",
5784
+ "csv_query",
5785
+ "csv_profile",
5786
+ "compact_doc",
5787
+ "docx_outline",
5788
+ "docx_text",
5789
+ "gdrive_sections",
5790
+ "image_meta",
5791
+ "image_text",
5792
+ "json_query",
5793
+ "json_outline",
5794
+ "note_read",
5795
+ "note_list",
5796
+ "openapi_op",
5797
+ "openapi_outline",
5798
+ "pdf_extract",
5799
+ "pdf_locate",
5800
+ "pdf_outline",
5801
+ "pdf_meta",
5802
+ "pptx_outline",
5803
+ "pptx_slide",
5804
+ "pptx_notes",
5805
+ "pptx_text",
5806
+ "pr_slice",
5807
+ "session_outline",
5808
+ "session_slice",
5809
+ "sqlite_query",
5810
+ "sqlite_schema",
5811
+ "transcript",
5812
+ "transcript_outline",
5813
+ "video_chapters",
5814
+ "xlsx_sheets",
5815
+ "xlsx_head",
5816
+ "xlsx_range",
5817
+ "xlsx_query",
5818
+ "xml_query",
5819
+ "xml_outline",
5820
+ "yaml_query",
5821
+ "yaml_outline",
5822
+ "zip_list",
5823
+ "zip_read"
5749
5824
  ])
5750
5825
  },
5751
5826
  { label: "Lookups", members: /* @__PURE__ */ new Set(["symbol_lookup", "semantic_search", "map_lookup"]) },
@@ -5767,74 +5842,72 @@ var _KIND_GROUPS = [
5767
5842
  "session_hint_overhead",
5768
5843
  "session_hint_suppressed",
5769
5844
  "read_count_deny",
5770
- "read_dedup_hint",
5771
5845
  "grep_dedup_hint",
5772
5846
  "glob_dedup_hint",
5773
5847
  "diff_hint",
5774
5848
  "predictive_prefetch_hit",
5775
- "read_partial_overlap_hint",
5776
5849
  "structured_file_hint",
5777
5850
  "write_rewrite_hint",
5778
5851
  "websearch_dedup_hint",
5779
5852
  "large_file_hint_followed",
5780
- "large_file_hint_ignored"
5781
- ])
5782
- },
5783
- {
5784
- label: "Bash",
5785
- members: /* @__PURE__ */ new Set([
5786
- "bash_dedup_hint",
5787
- "bash_output_cached",
5788
- "bash_output_recall",
5789
- "bash_output_recall_miss",
5790
- "bash_dedup_stale",
5791
- "bash_range_read_hint",
5792
- "bash_streak_hint",
5793
- "bash_poll_hint",
5794
- "env_probe_cache_hit",
5795
- "git_diff_scope_hint",
5796
- "dep_list_cache_hit",
5797
- "bash_read_equiv_already_read",
5798
- "bash_grep_result_cache_hit",
5799
- "git_diff_context_trimmed"
5853
+ "large_file_hint_ignored",
5854
+ "evidence_cache_hit"
5800
5855
  ])
5801
5856
  },
5857
+ // Empty for the same reason as MCP below: every live Bash kind arrives through _kindGroupLabel's `bash_compress:` prefix branch, not through a literal name. The fifteen literal names this set used to carry (bash_output_cached, bash_dedup_hint, env_probe_cache_hit and the rest) came over with the Python port and were never recorded or registered anywhere in this tree, so they grouped rows that could not exist.
5858
+ { label: "Bash", members: /* @__PURE__ */ new Set() },
5802
5859
  {
5803
5860
  label: "Web",
5804
5861
  members: /* @__PURE__ */ new Set([
5805
- "web_dedup_hint",
5806
- "web_output_cached",
5807
- "web_output_recall",
5808
- "web_output_recall_miss",
5809
- "web_dedup_stale",
5810
5862
  "web_fetch",
5811
5863
  "injection_detected"
5812
5864
  ])
5813
5865
  },
5866
+ // Membership comes from _kindGroupLabel's `mcp:` prefix branch, not from this set, which is why
5867
+ // it is empty. The entry still has to exist: _renderByKindSection iterates _KIND_GROUPS' labels
5868
+ // (plus 'Other') to decide what to print, so a label _kindGroupLabel returns but that is missing
5869
+ // here does not fall back to 'Other' -- its rows disappear from the table entirely.
5870
+ { label: "MCP", members: /* @__PURE__ */ new Set() },
5814
5871
  {
5815
5872
  label: "Compact / Skills",
5816
5873
  members: /* @__PURE__ */ new Set([
5817
- "compact_manifest",
5818
- "compact_assist",
5819
- "compact_recovery",
5820
- "skill_body_recall",
5821
- "skill_compact_served",
5822
- "skill_cached",
5823
5874
  "skill_load",
5824
5875
  "skill_oversized_first_load",
5825
- "skill_compact_inlined",
5826
- "resume_packet",
5827
- "decision_log"
5876
+ "skill_compact_inlined"
5877
+ ])
5878
+ },
5879
+ // SOURCE_CONTENT: real rewrites of tool output that remove real bytes (agent report compaction, Grep fold, browser tab dedup, bash/content compression and the handoff pair). The by-source table has shown a 'content' row since the source was added, but the by-kind table had no member set for it, so every one of these kinds printed under 'Other'. The taskoutput: prefix branch in _kindGroupLabel routes here too.
5880
+ {
5881
+ label: "Content",
5882
+ members: /* @__PURE__ */ new Set([
5883
+ "content_compress",
5884
+ "content_retrieve",
5885
+ "agent_report_compact",
5886
+ "agent_report_compact_declined",
5887
+ "browser_tab_dedup",
5888
+ "grep:fold",
5889
+ "handoff_create",
5890
+ "handoff_resolve",
5891
+ "plan_echo_collapse"
5828
5892
  ])
5829
5893
  }
5830
5894
  ];
5831
5895
  function _kindGroupLabel(kind) {
5832
- if (kind.startsWith("bash_compress:")) {
5896
+ if (kind.startsWith("bash_compress:") || kind.startsWith("bashoutput:")) {
5833
5897
  return "Bash";
5834
5898
  }
5835
- if (kind.startsWith("webfetch:")) {
5899
+ if (kind.startsWith("webfetch:") || kind.startsWith("gdrive:")) {
5836
5900
  return "Web";
5837
5901
  }
5902
+ if (kind.startsWith("mcp:")) {
5903
+ return "MCP";
5904
+ }
5905
+ if (kind.startsWith("skill_body:") || kind.startsWith("skill_compact:")) {
5906
+ return "Compact / Skills";
5907
+ }
5908
+ if (kind.startsWith("taskoutput:")) {
5909
+ return "Content";
5910
+ }
5838
5911
  for (const group of _KIND_GROUPS) {
5839
5912
  if (group.members.has(kind)) {
5840
5913
  return group.label;
@@ -6138,6 +6211,10 @@ var SOURCE_SKILL = "skill";
6138
6211
  var SOURCE_CONTENT = "content";
6139
6212
  var SOURCE_OTHER = "other";
6140
6213
  var _BYTES_MODE_ONLY_KINDS = /* @__PURE__ */ new Set(["webfetch_image", "gdrive_image"]);
6214
+ var COUNT_ONLY_KINDS = /* @__PURE__ */ new Set(["secret_redacted"]);
6215
+ function savedTokensFromBytes(bytes) {
6216
+ return Math.round(Math.max(0, bytes) / 4);
6217
+ }
6141
6218
  var KIND_TO_SOURCE = {
6142
6219
  image_shrink: SOURCE_IMAGE,
6143
6220
  image_shrink_cache_hit: SOURCE_IMAGE,
@@ -6238,6 +6315,10 @@ var KIND_TO_SOURCE = {
6238
6315
  // Decline counterpart to agent_report_compact: the fence-collapse net-benefit gate ran and found at least one over-long fence, but declined to rewrite because net savings did not clear the notice cost. Always recorded at (0, 0) -- see the recordStat call site -- so it never contributes to any savings total; it exists purely to make gate hit-rate and near-misses visible instead of the decline being invisible.
6239
6316
  agent_report_compact_declined: SOURCE_CONTENT,
6240
6317
  content_compress: SOURCE_CONTENT,
6318
+ // Verbatim-repeat collapse of a browser tool's "Tab Context:" text block (hooks_browser_image.ts postBrowserImageHandler). SOURCE_CONTENT for the same reason as agent_report_compact above: it is a real rewrite with real bytes removed, not an advisory nudge. Deliberately not SOURCE_IMAGE -- it shares a handler with image_shrink but collapses text, and folding text bytes into the image ledger is the two-units-under-one-label mistake this file's image_shrink entry was just fixed for.
6319
+ browser_tab_dedup: SOURCE_CONTENT,
6320
+ // Collapse of the plan echo in an approved ExitPlanMode result (hooks_exitplanmode.ts). SOURCE_CONTENT for the same reason as agent_report_compact: real bytes removed from a tool result, not an advisory nudge. The handler shipped for releases emitting this rewrite and recording nothing at all, so the mechanism was invisible in `stats` and its net benefit could not be checked against the gate that admits it.
6321
+ plan_echo_collapse: SOURCE_CONTENT,
6241
6322
  // Lossless re-layout of Grep content-mode output (hooks_grep.ts foldGrepContentHandler). SOURCE_CONTENT, not SOURCE_HINT, for the same reason as agent_report_compact above: its sibling grep_dedup_hint is advisory and saves nothing directly, whereas this is a real rewrite with real bytes removed. Filing it under the advisory bucket would silently add non-hint savings to hint_stats.ts's savedBytes (which reads by_source[SOURCE_HINT] wholesale) and overstate the hint ledger's net benefit.
6242
6323
  "grep:fold": SOURCE_CONTENT,
6243
6324
  content_retrieve: SOURCE_CONTENT,
@@ -6366,7 +6447,8 @@ CREATE TABLE IF NOT EXISTS stats (
6366
6447
  tokens_saved INTEGER NOT NULL DEFAULT 0,
6367
6448
  bytes_saved INTEGER NOT NULL DEFAULT 0,
6368
6449
  detail TEXT,
6369
- harness TEXT
6450
+ harness TEXT,
6451
+ traceparent TEXT
6370
6452
  );
6371
6453
  CREATE INDEX IF NOT EXISTS idx_stats_ts ON stats(ts);
6372
6454
  CREATE INDEX IF NOT EXISTS idx_stats_kind ON stats(kind);
@@ -6389,6 +6471,11 @@ function migrateGlobalSchema(db) {
6389
6471
  } catch (err) {
6390
6472
  if (!(err instanceof Error) || !/duplicate column/i.test(err.message)) throw err;
6391
6473
  }
6474
+ try {
6475
+ db.exec("ALTER TABLE stats ADD COLUMN traceparent TEXT");
6476
+ } catch (err) {
6477
+ if (!(err instanceof Error) || !/duplicate column/i.test(err.message)) throw err;
6478
+ }
6392
6479
  }
6393
6480
  var _harnessColumnByDb = /* @__PURE__ */ new WeakMap();
6394
6481
  function statsHasHarnessColumn(db) {
@@ -6405,6 +6492,21 @@ function statsHasHarnessColumn(db) {
6405
6492
  _harnessColumnByDb.set(db, present);
6406
6493
  return present;
6407
6494
  }
6495
+ var _traceparentColumnByDb = /* @__PURE__ */ new WeakMap();
6496
+ function statsHasTraceparentColumn(db) {
6497
+ const cached = _traceparentColumnByDb.get(db);
6498
+ if (cached !== void 0) return cached;
6499
+ let present;
6500
+ try {
6501
+ present = db.prepare("PRAGMA table_info(stats)").all().some(
6502
+ (c) => c.name === "traceparent"
6503
+ );
6504
+ } catch {
6505
+ present = false;
6506
+ }
6507
+ _traceparentColumnByDb.set(db, present);
6508
+ return present;
6509
+ }
6408
6510
  function getGlobalDb(homeDir) {
6409
6511
  const basePath = homeDir ? dataDirForHome(homeDir) : dataDir();
6410
6512
  const dbPath = path8.join(basePath, "global.db");
@@ -6423,11 +6525,18 @@ function noStatsMessage(windowDays, homeDir) {
6423
6525
  if (total === 0) return "No stats recorded yet.";
6424
6526
  return `No stats in the last ${countNoun(windowDays, "day")} (${total} recorded outside this window; use --window-days 0 for all time).`;
6425
6527
  }
6426
- function recordStat(kind, bytesSaved = 0, tokensSaved = 0, _testDb, detail) {
6528
+ function recordStat(kind, bytesSaved = 0, tokensSaved = 0, _testDb, detail, traceparent) {
6427
6529
  try {
6428
6530
  const db = _testDb ?? getGlobalDb();
6429
6531
  const ts = Math.floor(Date.now() / 1e3);
6430
- if (statsHasHarnessColumn(db)) {
6532
+ const tp = traceparent ?? process.env["TRACEPARENT"] ?? process.env["traceparent"] ?? null;
6533
+ const hasHarness = statsHasHarnessColumn(db);
6534
+ const hasTraceparent = statsHasTraceparentColumn(db);
6535
+ if (hasHarness && hasTraceparent) {
6536
+ db.prepare(
6537
+ "INSERT INTO stats (ts, kind, bytes_saved, tokens_saved, detail, harness, traceparent) VALUES (?, ?, ?, ?, ?, ?, ?)"
6538
+ ).run(ts, kind, bytesSaved, tokensSaved, detail ?? null, getHarnessName(), tp);
6539
+ } else if (hasHarness) {
6431
6540
  db.prepare(
6432
6541
  "INSERT INTO stats (ts, kind, bytes_saved, tokens_saved, detail, harness) VALUES (?, ?, ?, ?, ?, ?)"
6433
6542
  ).run(ts, kind, bytesSaved, tokensSaved, detail ?? null, getHarnessName());
@@ -6482,10 +6591,14 @@ function summarize(windowDays = 30, testDb, homeDir) {
6482
6591
  const stmt = db.prepare(query);
6483
6592
  const rows = sinceTs !== null ? stmt.all(sinceTs) : stmt.all();
6484
6593
  const tsToDateCache = {};
6594
+ const counts = {};
6485
6595
  for (const row of rows) {
6486
6596
  const bytesSaved = row.bytes_saved ?? 0;
6487
- const tokensSaved = row.tokens_saved ?? 0;
6597
+ const recorded = row.tokens_saved ?? 0;
6488
6598
  const kind = row.kind;
6599
+ const isCount = COUNT_ONLY_KINDS.has(kind);
6600
+ if (isCount) counts[kind] = (counts[kind] ?? 0) + recorded;
6601
+ const tokensSaved = isCount ? 0 : recorded;
6489
6602
  const tsRaw = row.ts;
6490
6603
  if (tsRaw === void 0) continue;
6491
6604
  const ts = tsRaw;
@@ -6544,6 +6657,7 @@ function summarize(windowDays = 30, testDb, homeDir) {
6544
6657
  by_project: byProjectList,
6545
6658
  by_source: bySourceDict,
6546
6659
  by_harness: byHarness,
6660
+ counts,
6547
6661
  by_command: Object.entries(byCommandDict).map(([command, bucket]) => ({ ...bucket, command })).filter((r) => r.events > 0),
6548
6662
  window_days: windowDays
6549
6663
  };
@@ -6554,6 +6668,10 @@ function _totalsLines(summary) {
6554
6668
  `Total events: ${summary.total_events}`,
6555
6669
  `Bytes saved: ${fmtBytes(summary.total_bytes_saved)}`,
6556
6670
  `Tokens saved: ${summary.total_tokens_saved}`,
6671
+ // Printed on its own line, below the token total and never inside it, because it counts
6672
+ // placeholders rather than tokens. Omitted entirely when nothing was redacted, so the line is
6673
+ // information rather than a permanent zero. See COUNT_ONLY_KINDS.
6674
+ ...summary.counts["secret_redacted"] ? [`Secrets hidden: ${summary.counts["secret_redacted"]} (a count, not tokens)`] : [],
6557
6675
  `Window: ${summary.window_days} days`
6558
6676
  ];
6559
6677
  }
@@ -6872,6 +6990,7 @@ export {
6872
6990
  toKB,
6873
6991
  compileGrepMatcher,
6874
6992
  grepFilteredToEmptyNotice,
6993
+ filtersFilteredToEmptyNotice,
6875
6994
  countNoun,
6876
6995
  excludeTestsHiddenNote,
6877
6996
  countContentLines,
@@ -6959,6 +7078,7 @@ export {
6959
7078
  fg,
6960
7079
  C,
6961
7080
  SOURCE_HINT,
7081
+ savedTokensFromBytes,
6962
7082
  formatLocalTimestamp,
6963
7083
  recordStat,
6964
7084
  recordUnmappedTool,
@@ -7,11 +7,11 @@ import {
7
7
  expandGlobs,
8
8
  leftoverIntegrations,
9
9
  run
10
- } from "./token-goat-chunk-222VPFP2.mjs";
11
- import "./token-goat-chunk-TX4JFJTD.mjs";
12
- import "./token-goat-chunk-4HIMCBYK.mjs";
13
- import "./token-goat-chunk-PWVXXPCC.mjs";
14
- import "./token-goat-chunk-6ODZ6PZK.mjs";
10
+ } from "./token-goat-chunk-U4UI5F3S.mjs";
11
+ import "./token-goat-chunk-RE7S7H26.mjs";
12
+ import "./token-goat-chunk-IZRXU64B.mjs";
13
+ import "./token-goat-chunk-4OTIB7SB.mjs";
14
+ import "./token-goat-chunk-E76UNTVK.mjs";
15
15
  import "./token-goat-chunk-AO2QD2AG.mjs";
16
16
  import "./token-goat-chunk-AEX54RUZ.mjs";
17
17
  export {
@@ -8,7 +8,7 @@ import {
8
8
  loadBlob,
9
9
  sanitizeFtsQuery,
10
10
  storeBlob
11
- } from "./token-goat-chunk-PWVXXPCC.mjs";
11
+ } from "./token-goat-chunk-4OTIB7SB.mjs";
12
12
  import {
13
13
  SYMBOL_BODY_CHAR_CAP,
14
14
  extractErrorMessage,
@@ -18,7 +18,7 @@ import {
18
18
  redactSecrets,
19
19
  runGit,
20
20
  shortFingerprint
21
- } from "./token-goat-chunk-6ODZ6PZK.mjs";
21
+ } from "./token-goat-chunk-E76UNTVK.mjs";
22
22
  import {
23
23
  registerReset
24
24
  } from "./token-goat-chunk-AO2QD2AG.mjs";
@@ -151,8 +151,22 @@ var GROK_TOOL_NAME_MAP = {
151
151
  write: "Write",
152
152
  search_replace: "Edit",
153
153
  run_terminal_command: "Bash",
154
+ // The grok 0.2.93 binary registers its shell tool as `tool.run_terminal_cmd` (tracing-id table in ~/.grok/bin/agent.exe) and its newer embedded hooks doc's PreToolUse example sends `"toolName": "run_terminal_cmd"`, while the older embedded doc revision and a 2026-07-09 live capture on the same version both show `run_terminal_command`. Which spelling arrives is profile/version dependent, so BOTH are mapped: an unmatched extra entry is a harmless no-op, a missing one silently kills every Bash hook (compression, wrap, hints) on that profile.
155
+ run_terminal_cmd: "Bash",
154
156
  grep: "Grep",
155
- list_dir: "Glob"
157
+ list_dir: "Glob",
158
+ // `glob` is a distinct registered grok tool (tool.glob in the 0.2.93 binary's tracing-id table), separate from list_dir; unmapped it arrived as lowercase 'glob' and matched no handler.
159
+ glob: "Glob",
160
+ // web_fetch/web_search were entirely absent from this map, so grok's URL fetches bypassed token-goat's WebFetch pipeline (URL-policy deny included) and its searches bypassed the WebSearch dedup/compression. Verified against the 0.2.93 binary: tool.web_fetch / tool.web_search registered ids, grok_build WebFetchInput's field described as "The URL to fetch content from" (i.e. `url`), grok_build WebSearchInput's fields query/citations/allowed_domains -- all matching the keys hooks_fetch.ts / hooks_websearch.ts already read, so a name map alone revives both.
161
+ web_fetch: "WebFetch",
162
+ web_search: "WebSearch",
163
+ // The Hashline prompt profile registers hashline_read/hashline_edit/hashline_grep (tool.hashline_* in the binary; grok's own hook alias table pairs them with Read/Edit/Grep). Their input key shapes were not individually verified: if one differs, the mapped handler degrades to the same no-op as an unmapped name, never a wrong rewrite. The *_concise twins are the GrokBuildConcise profile's registrations of the same three core tools.
164
+ hashline_read: "Read",
165
+ hashline_edit: "Edit",
166
+ hashline_grep: "Grep",
167
+ read_file_concise: "Read",
168
+ search_replace_concise: "Edit",
169
+ run_terminal_cmd_concise: "Bash"
156
170
  };
157
171
  var GROK_INPUT_KEY_MAP = {
158
172
  Read: { target_file: "file_path" }
@@ -162,10 +176,28 @@ var KIMI_TOOL_NAME_MAP = {
162
176
  ReadMediaFile: "Read"
163
177
  };
164
178
  var KIMI_INPUT_KEY_MAP = {
165
- Read: { path: "file_path" },
179
+ // line_offset/n_lines are Kimi's Read paging arguments (ReadInputSchema in MoonshotAI/kimi-code packages/agent-core-v2/src/agent/tools/os/read/read.ts: `path`, `line_offset` "the line number to start reading from", `n_lines` "the number of lines to read"). Unmapped, every ranged Kimi read looked unbounded to hooks_read.ts's estimateRequestedSlice and was gated on the whole file's size -- a small slice of a big file could draw the large-file deny meant for full reads. line_offset's 1-indexed positive form matches Read's own offset semantics exactly; its negative tail-read form has no token-goat equivalent and is clamped to 1 by estimateRequestedSlice's `offset >= 1` guard, degrading to a window of the right SIZE (which is all the gate consumes). ReadMediaFile shares this map via its Read rename and carries only `path`, so the extra entries never touch it.
180
+ Read: { path: "file_path", line_offset: "offset", n_lines: "limit" },
166
181
  Write: { path: "file_path" },
167
182
  Edit: { path: "file_path" }
168
183
  };
184
+ var QWEN_TOOL_NAME_MAP = {
185
+ read_file: "Read",
186
+ write_file: "Write",
187
+ edit: "Edit",
188
+ replace: "Edit",
189
+ notebook_edit: "NotebookEdit",
190
+ run_shell_command: "Bash",
191
+ grep_search: "Grep",
192
+ search_file_content: "Grep",
193
+ glob: "Glob",
194
+ web_fetch: "WebFetch",
195
+ web_search: "WebSearch",
196
+ list_directory: "Read"
197
+ };
198
+ var QWEN_INPUT_KEY_MAP = {
199
+ Read: { path: "file_path" }
200
+ };
169
201
  function remapInputKeys(input, keyMap) {
170
202
  const newInput = {};
171
203
  for (const [k, v] of Object.entries(input)) {
@@ -209,6 +241,14 @@ function grokToCanonicalWire(obj) {
209
241
  }
210
242
  return wire;
211
243
  }
244
+ function aliasLlmContentToOutput(result) {
245
+ const toolResponse = result["tool_response"];
246
+ if (toolResponse === null || typeof toolResponse !== "object" || Array.isArray(toolResponse)) return;
247
+ const responseRecord = toolResponse;
248
+ if (typeof responseRecord["llmContent"] === "string" && responseRecord["output"] === void 0) {
249
+ result["tool_response"] = { ...responseRecord, output: responseRecord["llmContent"] };
250
+ }
251
+ }
212
252
  function normalizePayload(payload, harness = "claude") {
213
253
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
214
254
  _LOG.warn("normalizePayload: payload is not a dict; received %s", typeof payload);
@@ -246,12 +286,19 @@ function normalizePayload(payload, harness = "claude") {
246
286
  result2["_tg_harness"] = harness;
247
287
  return result2;
248
288
  }
289
+ if (harness === "qwen") {
290
+ const result2 = remapToolName(obj, toolName, QWEN_TOOL_NAME_MAP, QWEN_INPUT_KEY_MAP);
291
+ aliasLlmContentToOutput(result2);
292
+ result2["_tg_harness"] = harness;
293
+ return result2;
294
+ }
249
295
  if (harness === "gemini") {
250
296
  const result2 = remapToolName(obj, toolName, GEMINI_TOOL_NAME_MAP, GEMINI_INPUT_KEY_MAP);
251
297
  if ("functionCallId" in result2 && !("toolUseId" in result2)) {
252
298
  result2["toolUseId"] = result2["functionCallId"];
253
299
  delete result2["functionCallId"];
254
300
  }
301
+ aliasLlmContentToOutput(result2);
255
302
  result2["_tg_harness"] = harness;
256
303
  return result2;
257
304
  }
@@ -4,14 +4,14 @@ import {
4
4
  buildEvent,
5
5
  relay,
6
6
  relayInProcess
7
- } from "./token-goat-chunk-NKNCHJ4H.mjs";
7
+ } from "./token-goat-chunk-ZQ3PUOP3.mjs";
8
8
  import {
9
9
  MAX_STDIN_BYTES,
10
10
  readStdinJson
11
- } from "./token-goat-chunk-4HIMCBYK.mjs";
12
- import "./token-goat-chunk-PWVXXPCC.mjs";
13
- import "./token-goat-chunk-EFF2XCLB.mjs";
14
- import "./token-goat-chunk-6ODZ6PZK.mjs";
11
+ } from "./token-goat-chunk-IZRXU64B.mjs";
12
+ import "./token-goat-chunk-4OTIB7SB.mjs";
13
+ import "./token-goat-chunk-CZALRRGN.mjs";
14
+ import "./token-goat-chunk-E76UNTVK.mjs";
15
15
  import "./token-goat-chunk-AO2QD2AG.mjs";
16
16
  import "./token-goat-chunk-AEX54RUZ.mjs";
17
17
  export {