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.
@@ -86,7 +86,7 @@ import {
86
86
  runZipRead,
87
87
  symbolNamesInFile,
88
88
  upsertNote
89
- } from "./token-goat-chunk-TX4JFJTD.mjs";
89
+ } from "./token-goat-chunk-RE7S7H26.mjs";
90
90
  import {
91
91
  BASH_OUTPUT_SUBDIR,
92
92
  GEMINI_TOOL_NAME_MAP,
@@ -113,13 +113,15 @@ import {
113
113
  searchRecall,
114
114
  storeWebOutput,
115
115
  summarizeResidentContext
116
- } from "./token-goat-chunk-4HIMCBYK.mjs";
116
+ } from "./token-goat-chunk-IZRXU64B.mjs";
117
117
  import {
118
118
  AGENT_SALT_MARKER,
119
119
  CONTEXT_AUTOCOMPACT_TOKENS,
120
120
  DEFAULT_MAX_AGE_MS,
121
121
  DEFAULT_MAX_COUNT,
122
+ MATERIALIZE_SHRUNK_IMAGE_JS,
122
123
  MAX_FILES_SCANNED,
124
+ PARSER_FINGERPRINT,
123
125
  SESSIONS_SUBDIR,
124
126
  SKILLS_OUTPUT_SUBDIR,
125
127
  SKIP_DIRS,
@@ -159,6 +161,7 @@ import {
159
161
  extractCompactFromMarker,
160
162
  extractNamedSection,
161
163
  fenceUntrustedContent,
164
+ fenceUntrustedOcrText,
162
165
  findClaudeMdFiles,
163
166
  findContentDuplicates,
164
167
  findLatestSessionId,
@@ -257,7 +260,7 @@ import {
257
260
  vscodeDecoderConfigured,
258
261
  walkProject,
259
262
  writeCompact
260
- } from "./token-goat-chunk-PWVXXPCC.mjs";
263
+ } from "./token-goat-chunk-4OTIB7SB.mjs";
261
264
  import {
262
265
  C,
263
266
  CONFIG_KEY_ENV_OVERRIDES,
@@ -324,6 +327,7 @@ import {
324
327
  runGit,
325
328
  safeSlice,
326
329
  saveConfig,
330
+ savedTokensFromBytes,
327
331
  sleepSync,
328
332
  stripAnsi,
329
333
  stripOwnHooksFromMap,
@@ -339,7 +343,7 @@ import {
339
343
  withFileLock,
340
344
  withRetryOnLock,
341
345
  writeJsonSettings
342
- } from "./token-goat-chunk-6ODZ6PZK.mjs";
346
+ } from "./token-goat-chunk-E76UNTVK.mjs";
343
347
  import {
344
348
  __export
345
349
  } from "./token-goat-chunk-AEX54RUZ.mjs";
@@ -3458,9 +3462,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3458
3462
  * @param {string} [path]
3459
3463
  * @return {(string|null|Command)}
3460
3464
  */
3461
- executableDir(path28) {
3462
- if (path28 === void 0) return this._executableDir;
3463
- this._executableDir = path28;
3465
+ executableDir(path29) {
3466
+ if (path29 === void 0) return this._executableDir;
3467
+ this._executableDir = path29;
3464
3468
  return this;
3465
3469
  }
3466
3470
  /**
@@ -3759,9 +3763,9 @@ function attemptedCommandName(argv) {
3759
3763
  }
3760
3764
 
3761
3765
  // src/cli.ts
3762
- import * as fs27 from "fs";
3763
- import * as path27 from "path";
3764
- import { homedir as homedir12 } from "os";
3766
+ import * as fs28 from "fs";
3767
+ import * as path28 from "path";
3768
+ import { homedir as homedir13 } from "os";
3765
3769
 
3766
3770
  // src/walk_index.ts
3767
3771
  import * as fs2 from "node:fs";
@@ -4095,10 +4099,11 @@ import * as os from "node:os";
4095
4099
  import * as path from "node:path";
4096
4100
  import { fileURLToPath, pathToFileURL } from "node:url";
4097
4101
 
4098
- // pi built-in tool names -> token-goat internal tool names
4102
+ // pi built-in tool names -> token-goat internal tool names. powershell is pi's Windows shell twin of bash -- same input schema (PowerShellToolInput = BashToolInput, i.e. command/timeout) and registered under its own name (badlogic/pi-mono packages/coding-agent/src/core/tools/powershell.ts + tools/index.ts's ToolName union). Unmapped, every shell command in a powershell-tool pi session bypassed the Bash hooks entirely, the same shape as the Copilot shim's bash/powershell pairing.
4099
4103
  const TOOL_TO_TG: Record<string, string> = {
4100
4104
  read: "Read",
4101
4105
  bash: "Bash",
4106
+ powershell: "Bash",
4102
4107
  edit: "Edit",
4103
4108
  write: "Write",
4104
4109
  grep: "Grep",
@@ -4109,15 +4114,14 @@ const TOOL_TO_TG: Record<string, string> = {
4109
4114
  const ARGS_TO_TG: Record<string, Record<string, string>> = {
4110
4115
  read: { path: "file_path", offset: "offset", limit: "limit" },
4111
4116
  bash: { command: "command", timeout: "timeout" },
4117
+ powershell: { command: "command", timeout: "timeout" },
4112
4118
  edit: { path: "file_path" },
4113
4119
  write: { path: "file_path" },
4114
4120
  grep: { pattern: "pattern", path: "path" },
4115
4121
  find: { pattern: "pattern", path: "path" },
4116
4122
  };
4117
4123
 
4118
- // Tools that have a pre-hook (read/search/fetch types only). Glob has no
4119
- // pre_tool_use handler in token-goat (only Read/Grep/Bash/WebFetch do), so
4120
- // it's excluded here rather than spawning a hook call that always no-ops.
4124
+ // Tools with a pre-hook whose output shape this extension can act on: deny ({block}), updatedInput (in-place arg rewrite), or the Read image-shrink materialization. Glob's pre handler (preGlobDedupHandler in hooks_glob.ts -- the old claim here that Glob has no pre handler was stale) emits only an advisory contextOutput hint, which pi's tool_call contract has no channel for (see the module docblock), so calling it would cost a hook call per find only to drop the answer; it stays excluded for that reason.
4121
4125
  const PRE_HOOK_TOOLS = new Set(["Read", "Grep", "Bash", "WebFetch"]);
4122
4126
 
4123
4127
  // resolveEntryPath reads a sidecar JSON file (token-goat-entry.json, written by
@@ -4269,6 +4273,30 @@ function extractUpdatedInput(resp: Record<string, unknown>): Record<string, unkn
4269
4273
  // since pi's tool_call handler has no context-injection channel -- only
4270
4274
  // in-place arg mutation. Returns undefined (leaving the read path untouched)
4271
4275
  // if the context isn't a shrink payload or anything goes wrong writing it.
4276
+ // Best-effort sweep of previously materialized shrunk copies in the OS temp dir: typed twin of pruneMaterializedShrinks in shrink_block.ts (MATERIALIZE_SHRUNK_IMAGE_JS), which documents why pi keeps its own copy. The temp file only needs to outlive the single tool call whose path was rewritten to it, so anything older than an hour is finished with; the "token-goat-shrink-" prefix check confines the sweep to this mechanism's own files.
4277
+ const MATERIALIZED_SHRINK_MAX_AGE_MS = 60 * 60 * 1000;
4278
+ let lastMaterializedShrinkSweepAtMs = 0;
4279
+ function pruneMaterializedShrinks(): void {
4280
+ const now = Date.now();
4281
+ if (now - lastMaterializedShrinkSweepAtMs < MATERIALIZED_SHRINK_MAX_AGE_MS) return;
4282
+ lastMaterializedShrinkSweepAtMs = now;
4283
+ try {
4284
+ const dir = os.tmpdir();
4285
+ for (const file of fs.readdirSync(dir)) {
4286
+ if (!file.startsWith("token-goat-shrink-")) continue;
4287
+ const full = path.join(dir, file);
4288
+ try {
4289
+ const st = fs.statSync(full);
4290
+ if (st.isFile() && now - st.mtimeMs > MATERIALIZED_SHRINK_MAX_AGE_MS) fs.unlinkSync(full);
4291
+ } catch {
4292
+ // Best-effort per-file cleanup; one bad stat/unlink must not abort the sweep.
4293
+ }
4294
+ }
4295
+ } catch {
4296
+ // Best-effort; a readdir failure must never break the materialization below.
4297
+ }
4298
+ }
4299
+
4272
4300
  function materializeShrunkImage(context: string | undefined): string | undefined {
4273
4301
  if (typeof context !== "string") return undefined;
4274
4302
  const idx = context.indexOf("data:image/");
@@ -4276,6 +4304,7 @@ function materializeShrunkImage(context: string | undefined): string | undefined
4276
4304
  const match = /^data:image\\/([a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)$/.exec(context.slice(idx).trim());
4277
4305
  if (!match) return undefined;
4278
4306
  try {
4307
+ pruneMaterializedShrinks();
4279
4308
  const buf = Buffer.from(match[2], "base64");
4280
4309
  const name = \`token-goat-shrink-\${process.pid}-\${Date.now()}-\${Math.random().toString(36).slice(2)}.\${match[1]}\`;
4281
4310
  const file = path.join(os.tmpdir(), name);
@@ -4361,13 +4390,35 @@ export default function (pi: ExtensionAPI) {
4361
4390
  .filter((c: { type?: string }) => c && c.type === "text")
4362
4391
  .map((c: { text?: string }) => c.text ?? "")
4363
4392
  .join("\\n");
4364
- await callHook("post_tool_use", {
4393
+ const resp = await callHook("post_tool_use", {
4365
4394
  session_id: sessionId,
4366
4395
  tool_name: tg,
4367
4396
  tool_input: toToolInput(event.toolName, (event.input ?? {}) as Record<string, unknown>),
4368
4397
  cwd,
4369
4398
  tool_response: { output },
4370
4399
  });
4400
+ if (!resp) return;
4401
+
4402
+ // rewriteOutput: replace the tool result the model sees. pi's tool_result handler CAN modify the result: its own types declare "Fired after a tool executes. Can modify result." over ToolResultEvent and a ToolResultEventResult of { content, details, isError, usage }, and the shipped runtime applies it (dist/core/extensions/runner.js emitToolResult assigns handlerResult.content onto the event, and dist/core/agent-session.js afterToolCall returns hookResult.content as the tool result content). Without this, injection fencing, secret redaction and output compression were computed and thrown away on every pi session, the same whitelist-shaped drop fixed for opencode.
4403
+ const hso = resp["hookSpecificOutput"] as Record<string, unknown> | undefined;
4404
+ const updatedToolOutput = hso && typeof hso["updatedToolOutput"] === "string" ? (hso["updatedToolOutput"] as string) : undefined;
4405
+ if (updatedToolOutput === undefined) return;
4406
+
4407
+ // Replace only the text blocks, in place, and keep every non-text block (images) untouched: emitToolResult overwrites content wholesale, so anything not carried here is dropped from what the model sees. details/isError/usage are deliberately not returned, since runner.js only overwrites the fields a handler actually provides.
4408
+ const rewritten: typeof contentBlocks = [];
4409
+ let replaced = false;
4410
+ for (const block of contentBlocks) {
4411
+ const isText = Boolean(block) && (block as { type?: string }).type === "text";
4412
+ if (!isText) {
4413
+ rewritten.push(block);
4414
+ continue;
4415
+ }
4416
+ if (replaced) continue;
4417
+ replaced = true;
4418
+ rewritten.push({ type: "text", text: updatedToolOutput });
4419
+ }
4420
+ if (!replaced) rewritten.push({ type: "text", text: updatedToolOutput });
4421
+ return { content: rewritten };
4371
4422
  });
4372
4423
 
4373
4424
  // Compaction: pi's session_before_compact REPLACES the summary rather than
@@ -4554,7 +4605,7 @@ import os from "node:os"
4554
4605
  import path from "node:path"
4555
4606
  import { fileURLToPath, pathToFileURL } from "node:url"
4556
4607
 
4557
- // opencode built-in tool id -> token-goat canonical tool name.
4608
+ // opencode built-in tool id -> token-goat canonical tool name. websearch/skill/task were re-verified against opencode's own source at the tag matching the installed release (anomalyco/opencode v1.18.16 packages/opencode/src/tool/): WebSearchTool registers as "websearch" (websearch.ts), SkillTool as "skill" (skill.ts), TaskTool as "task" (task.ts, params prompt/subagent_type/description -- the exact keys token-goat's hooks_agent_spawn.ts reads, which registers a lowercase 'task' handler). Unmapped, all three were dead mechanisms here: no WebSearch repeat-search deny/compression, no repeat-skill-load deny, no agent-spawn briefing or report compaction. apply_patch (patchText only, no per-file path) and lsp/plan/question/todo have no token-goat equivalent and stay unmapped.
4558
4609
  const TOOL_TO_TG = {
4559
4610
  read: "Read",
4560
4611
  bash: "Bash",
@@ -4563,15 +4614,15 @@ const TOOL_TO_TG = {
4563
4614
  grep: "Grep",
4564
4615
  glob: "Glob",
4565
4616
  webfetch: "WebFetch",
4617
+ websearch: "WebSearch",
4618
+ skill: "Skill",
4619
+ task: "task",
4566
4620
  }
4567
4621
 
4568
- // Tools with a real pre_tool_use handler registered server-side (hooks_read.ts,
4569
- // image_shrink.ts, hooks_bash.ts, hooks_fetch.ts). Edit/Write have no pre-hook
4570
- // in token-goat at all, so skip the subprocess call for them entirely. Glob
4571
- // has none either, so it's excluded too rather than spawning a no-op call.
4572
- const PRE_HOOK_TOOLS = new Set(["read", "bash", "grep", "webfetch"])
4622
+ // Tools with a pre_tool_use handler server-side whose OUTPUT SHAPE this hook can act on: deny (throw), updatedInput (args rewrite), or the Read image-shrink materialization. websearch (repeat-search deny within the dedup TTL, hooks_websearch.ts), skill (repeat-load deny, hooks_skill.ts) and task (briefing updatedInput rewrite, hooks_agent_spawn.ts) all qualify. Edit/Write have no pre-hook at all. Glob DOES have one (preGlobDedupHandler, hooks_glob.ts) -- the old claim here that it has none was stale -- but its only output is an advisory contextOutput hint, and tool.execute.before has no context channel (see the module docblock), so calling it would spawn a subprocess per glob only to drop the answer; it stays excluded for that reason, not the old one.
4623
+ const PRE_HOOK_TOOLS = new Set(["read", "bash", "grep", "webfetch", "websearch", "skill", "task"])
4573
4624
 
4574
- // opencode tool args (camelCase) -> token-goat snake_case tool_input keys.
4625
+ // opencode tool args (camelCase) -> token-goat snake_case tool_input keys. websearch's query (websearch.ts Parameters), skill's name (skill.ts Parameters; hooks_skill.ts reads tool_input['skill']) and task's prompt/subagent_type/description (task.ts BaseParameterFields) are all from opencode's own schemas at v1.18.16.
4575
4626
  const ARGS_TO_TG = {
4576
4627
  read: { filePath: "file_path", offset: "offset", limit: "limit" },
4577
4628
  bash: { command: "command", timeout: "timeout" },
@@ -4580,6 +4631,9 @@ const ARGS_TO_TG = {
4580
4631
  grep: { pattern: "pattern", path: "path" },
4581
4632
  glob: { pattern: "pattern", path: "path" },
4582
4633
  webfetch: { url: "url" },
4634
+ websearch: { query: "query" },
4635
+ skill: { name: "skill" },
4636
+ task: { prompt: "prompt", subagent_type: "subagent_type", description: "description" },
4583
4637
  }
4584
4638
 
4585
4639
  function reverseArgMap(tool) {
@@ -4615,28 +4669,7 @@ function extractUpdatedInput(resp) {
4615
4669
  return hso && typeof hso === "object" ? hso.updatedInput : undefined
4616
4670
  }
4617
4671
 
4618
- // Decode a token-goat image-shrink additionalContext payload
4619
- // ("<summary>\\ndata:image/<fmt>;base64,<data>") into a real file on disk,
4620
- // since tool.execute.before has no context-injection channel to hand the
4621
- // shrunk image to the model directly -- only output.args mutation. Returns
4622
- // undefined (leaving output.args untouched) if the context isn't a shrink
4623
- // payload or anything goes wrong writing it.
4624
- function materializeShrunkImage(context) {
4625
- if (typeof context !== "string") return undefined
4626
- const idx = context.indexOf("data:image/")
4627
- if (idx === -1) return undefined
4628
- const match = /^data:image\\/([a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)$/.exec(context.slice(idx).trim())
4629
- if (!match) return undefined
4630
- try {
4631
- const buf = Buffer.from(match[2], "base64")
4632
- const name = \`token-goat-shrink-\${process.pid}-\${Date.now()}-\${Math.random().toString(36).slice(2)}.\${match[1]}\`
4633
- const file = path.join(os.tmpdir(), name)
4634
- fs.writeFileSync(file, buf)
4635
- return file
4636
- } catch {
4637
- return undefined
4638
- }
4639
- }
4672
+ ${MATERIALIZE_SHRUNK_IMAGE_JS}
4640
4673
 
4641
4674
  export const TokenGoatPlugin = async ({ directory }) => {
4642
4675
  return {
@@ -4686,6 +4719,14 @@ export const TokenGoatPlugin = async ({ directory }) => {
4686
4719
  })
4687
4720
  if (!resp) return
4688
4721
 
4722
+ // rewriteOutput: replace the tool result wholesale. This is the shape every post_tool_use rewrite producer emits (WebFetch injection fencing and secret redaction, websearch redaction, bash/grep output compression, agent-report compaction), and it went unapplied here for the same whitelist reason the response-contract docblock above used to omit it -- the plugin only handled the shapes its author listed, so opencode sessions got the appended hint channel but never a fenced, redacted or compressed result. Mutating output.output is the exact mechanism the context append below already relies on, so this claims no capability the append did not.
4723
+ const hso = resp.hookSpecificOutput
4724
+ const updatedToolOutput = hso && typeof hso.updatedToolOutput === "string" ? hso.updatedToolOutput : undefined
4725
+ if (updatedToolOutput !== undefined) {
4726
+ output.output = updatedToolOutput
4727
+ return
4728
+ }
4729
+
4689
4730
  const context = extractContext(resp)
4690
4731
  if (context && typeof output.output === "string") {
4691
4732
  output.output += \`\\n\\n[token-goat] \${context}\`
@@ -4740,15 +4781,11 @@ var OPENCLAW_PLUGIN_SCRIPT = `// token-goat bridge plugin for OpenClaw
4740
4781
  // Bridges OpenClaw's tool-call and session hooks to token-goat's subprocess
4741
4782
  // hook protocol. https://github.com/DFKHelper/token-goat
4742
4783
  //
4743
- // NOTE ON PARAMETER SHAPES: OpenClaw's tool-call params are forwarded to
4744
- // token-goat's tool_input unchanged (no key remapping) -- verified against
4745
- // OpenClaw's own event-type source that these keys are already snake_case
4746
- // (file_path, command, etc.), matching token-goat's own convention. The
4747
- // built-in tool NAME list below is broader than any single source confirms,
4748
- // since an unmatched name is a harmless no-op here, not a wrong rewrite.
4784
+ // NOTE ON PARAMETER SHAPES: OpenClaw's bash tool really does use token-goat's own key names (command/timeout), but the path-carrying tools (read/edit/write) send the file path under "path", not "file_path" -- read out of OpenClaw's own tool input schemas (src/agents/sessions/tools/read-tool-contract.ts, edit.ts, bash.ts in openclaw/openclaw). toToolInput below ADDS the canonical file_path alongside the original path so token-goat's getFilePath()-based handlers can see it, without renaming anything. The built-in tool NAME list below is broader than any single source confirms, since an unmatched name is a harmless no-op here, not a wrong rewrite.
4749
4785
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
4750
4786
  import { spawnSync } from "node:child_process";
4751
4787
  import * as fs from "node:fs";
4788
+ import * as os from "node:os";
4752
4789
  import * as path from "node:path";
4753
4790
  import { fileURLToPath, pathToFileURL } from "node:url";
4754
4791
 
@@ -4759,20 +4796,43 @@ const TOOL_TO_TG = {
4759
4796
  edit: "Edit",
4760
4797
  apply_patch: "Edit",
4761
4798
  exec: "Bash",
4799
+ bash: "Bash",
4762
4800
  grep: "Grep",
4763
4801
  glob: "Glob",
4802
+ find: "Glob",
4764
4803
  webfetch: "WebFetch",
4765
4804
  web_search: "WebFetch",
4766
4805
  web_fetch: "WebFetch",
4767
4806
  };
4768
4807
 
4769
- // Tools with a registered pre_tool_use handler in token-goat (Edit/Write have
4770
- // none; Glob has none either -- see pi.ts/opencode.ts's identical
4771
- // PRE_HOOK_TOOLS note -- so it's excluded to avoid a wasted no-op spawn).
4808
+ // Tools with a pre_tool_use handler whose output shape this hook can act on: deny (block/blockReason), updatedInput (params rewrite), or the Read image-shrink materialization. Edit/Write have no pre handler at all. Glob (OpenClaw's find) DOES have one -- preGlobDedupHandler in hooks_glob.ts; the old claim here that it has none was stale -- but its only output is an advisory contextOutput hint and before_tool_call has no context channel, so calling it would spawn a hook subprocess per find only to drop the answer; it stays excluded for that reason (matching pi.ts/opencode.ts's corrected note).
4772
4809
  const PRE_HOOK_TOOLS = new Set(["Read", "Grep", "Bash", "WebFetch"]);
4773
4810
 
4811
+ // OpenClaw tools whose file path travels under "path" (see the parameter-shapes note above). apply_patch is deliberately absent: its input is a patch document, not a single path.
4812
+ const PATH_ARG_TOOLS = new Set(["read", "write", "edit"]);
4813
+
4814
+ // Build token-goat's tool_input from OpenClaw's params: same object, plus the canonical file_path added alongside the original path for the tools that carry one. Never renames or drops a key.
4815
+ function toToolInput(toolName, params) {
4816
+ const p = params || {};
4817
+ if (PATH_ARG_TOOLS.has(toolName) && typeof p.path === "string" && p.file_path === undefined) {
4818
+ return { ...p, file_path: p.path };
4819
+ }
4820
+ return p;
4821
+ }
4822
+
4823
+ // Pull the additionalContext / systemMessage string out of a hook response, whichever shape it came back as (see the response-contract note in this module's header comment).
4824
+ function extractContext(resp) {
4825
+ if (!resp) return undefined;
4826
+ const hso = resp["hookSpecificOutput"];
4827
+ if (hso && typeof hso["additionalContext"] === "string") return hso["additionalContext"];
4828
+ if (typeof resp["systemMessage"] === "string") return resp["systemMessage"];
4829
+ return undefined;
4830
+ }
4831
+
4774
4832
  ${BRIDGE_RELAY_JS}
4775
4833
 
4834
+ ${MATERIALIZE_SHRUNK_IMAGE_JS}
4835
+
4776
4836
  export default definePluginEntry({
4777
4837
  id: "token-goat",
4778
4838
  name: "token-goat",
@@ -4808,7 +4868,7 @@ export default definePluginEntry({
4808
4868
  const resp = await callHook("pre_tool_use", {
4809
4869
  session_id: sid,
4810
4870
  tool_name: tg,
4811
- tool_input: event.params || {},
4871
+ tool_input: toToolInput(event.toolName, event.params),
4812
4872
  cwd: process.cwd(),
4813
4873
  });
4814
4874
  if (!resp) return {};
@@ -4827,6 +4887,12 @@ export default definePluginEntry({
4827
4887
  return { params: { ...(event.params || {}), ...updated } };
4828
4888
  }
4829
4889
 
4890
+ // Image shrink has no context channel here -- translate it into a rewritten path pointing at a materialized shrunk copy instead, via the same params-rewrite channel the updatedInput merge above already relies on. OpenClaw's read tool takes the path under "path" (see the parameter-shapes note above), and the rest of the original params are preserved.
4891
+ if (tg === "Read") {
4892
+ const shrunkPath = materializeShrunkImage(extractContext(resp));
4893
+ if (shrunkPath) return { params: { ...(event.params || {}), path: shrunkPath } };
4894
+ }
4895
+
4830
4896
  return {};
4831
4897
  });
4832
4898
 
@@ -4846,7 +4912,7 @@ export default definePluginEntry({
4846
4912
  await callHook("post_tool_use", {
4847
4913
  session_id: sid,
4848
4914
  tool_name: tg,
4849
- tool_input: event.params || {},
4915
+ tool_input: toToolInput(event.toolName, event.params),
4850
4916
  cwd: process.cwd(),
4851
4917
  tool_response: toolResponse,
4852
4918
  });
@@ -5695,7 +5761,7 @@ function renderTopSessionFilesFromDisk(topN = 5, overrideSessionsDir) {
5695
5761
  var METHODOLOGY = {
5696
5762
  estimate_scope: "Local estimate of content avoided or reduced by token-goat.",
5697
5763
  billing: "They are not GitHub Copilot usage, provider-reported token consumption, or billing data.",
5698
- byte_derived_formula: "Most read, hook, and command entries use Math.round(bytes_saved / 4).",
5764
+ byte_derived_formula: "Most read, hook, and command entries go through savedTokensFromBytes in src/stats.ts, which is Math.round(bytes_saved / 4).",
5699
5765
  filter_estimates: "Output compressors record their filter-calculated delta; image entries use the byte-derived approximation unless the source provides a narrower estimate.",
5700
5766
  advisory_events: "Zero-byte, zero-token advisory events show that guidance fired, not that an agent followed it.",
5701
5767
  audit: "Use stats --full or stats --json for source and command breakdowns; reconcile billing with provider-exported usage data."
@@ -5718,6 +5784,21 @@ function renderMethodology(json = false) {
5718
5784
  ""
5719
5785
  ].join("\n"));
5720
5786
  }
5787
+ function statsJsonPayload(summary) {
5788
+ return {
5789
+ total_events: summary.total_events,
5790
+ total_bytes_saved: summary.total_bytes_saved,
5791
+ total_tokens_saved: summary.total_tokens_saved,
5792
+ by_kind: summary.by_kind,
5793
+ by_day: summary.by_day,
5794
+ by_project: summary.by_project,
5795
+ by_command: summary.by_command,
5796
+ by_source: summary.by_source,
5797
+ by_harness: summary.by_harness,
5798
+ counts: summary.counts,
5799
+ window_days: summary.window_days
5800
+ };
5801
+ }
5721
5802
  function runStats(opts = {}) {
5722
5803
  if (opts.methodology === true) {
5723
5804
  renderMethodology(opts.json === true);
@@ -5726,19 +5807,7 @@ function runStats(opts = {}) {
5726
5807
  const window = opts.windowDays ?? 30;
5727
5808
  const summary = summarize(window, void 0, opts.homeDir);
5728
5809
  if (opts.json === true) {
5729
- const out2 = {
5730
- total_events: summary.total_events,
5731
- total_bytes_saved: summary.total_bytes_saved,
5732
- total_tokens_saved: summary.total_tokens_saved,
5733
- by_kind: summary.by_kind,
5734
- by_day: summary.by_day,
5735
- by_project: summary.by_project,
5736
- by_command: summary.by_command,
5737
- by_source: summary.by_source,
5738
- by_harness: summary.by_harness,
5739
- window_days: summary.window_days
5740
- };
5741
- process.stdout.write(JSON.stringify(out2) + "\n");
5810
+ process.stdout.write(JSON.stringify(statsJsonPayload(summary)) + "\n");
5742
5811
  return;
5743
5812
  }
5744
5813
  const renderOpts = { windowDays: window };
@@ -6287,7 +6356,9 @@ function checkUnmappedTools(dbPath) {
6287
6356
  if (rows.length === 0) {
6288
6357
  return { name, status: "ok", message: "every tool name seen so far reached a handler that wanted it" };
6289
6358
  }
6290
- const nearMisses = rows.filter((r) => r.near_miss !== null && r.near_miss !== void 0);
6359
+ const nearMisses = rows.filter(
6360
+ (r) => r.near_miss !== null && r.near_miss !== void 0 && r.near_miss !== r.tool_name
6361
+ );
6291
6362
  if (nearMisses.length > 0) {
6292
6363
  const shown2 = nearMisses.slice(0, UNMAPPED_TOOL_SAMPLE).map((r) => `${r.harness} sent "${r.tool_name}" where "${r.near_miss}" is handled (${r.event_name}, ${r.hits}x)`);
6293
6364
  const more2 = nearMisses.length > UNMAPPED_TOOL_SAMPLE ? ` (+${nearMisses.length - UNMAPPED_TOOL_SAMPLE} more)` : "";
@@ -9189,12 +9260,12 @@ function formatBudgetText(result, contextK) {
9189
9260
  lines.push(` ${e.rel_path.padEnd(colW, " ")} ${String(e.lines).padStart(6, " ")} ${String(e.tokens).padStart(8, " ")}`);
9190
9261
  }
9191
9262
  lines.push(` ${"-".repeat(colW)} ${"-".repeat(6)} ${"-".repeat(8)}`);
9192
- let pct = "";
9263
+ let pct2 = "";
9193
9264
  if (contextK) {
9194
- pct = ` (${Math.round(result.total_tokens / (contextK * 1e3) * 100)}% of ${contextK}K)`;
9265
+ pct2 = ` (${Math.round(result.total_tokens / (contextK * 1e3) * 100)}% of ${contextK}K)`;
9195
9266
  }
9196
9267
  lines.push(
9197
- ` ${"Total".padEnd(colW, " ")} ${String(result.total_lines).padStart(6, " ")} ${String(result.total_tokens).padStart(8, " ")}${pct}`
9268
+ ` ${"Total".padEnd(colW, " ")} ${String(result.total_lines).padStart(6, " ")} ${String(result.total_tokens).padStart(8, " ")}${pct2}`
9198
9269
  );
9199
9270
  if (result.skipped.length > 0) {
9200
9271
  const skipped = result.skipped.slice(0, 5).join(", ");
@@ -11087,7 +11158,7 @@ function guardDeclarationRows(rows, budgetTokens) {
11087
11158
  function recordDepDocsStat(fullSourceBytes, emittedText, packageName) {
11088
11159
  const emittedBytes = Buffer.byteLength(emittedText, "utf8");
11089
11160
  const bytesSaved = Math.max(1, fullSourceBytes - emittedBytes);
11090
- recordStat("dep_docs", bytesSaved, Math.round(bytesSaved / 4), void 0, packageName);
11161
+ recordStat("dep_docs", bytesSaved, savedTokensFromBytes(bytesSaved), void 0, packageName);
11091
11162
  }
11092
11163
  function runDepDocs(opts) {
11093
11164
  const root = resolveProjectRoot({ project: opts.projectRoot ?? process.cwd() });
@@ -11504,15 +11575,15 @@ function cmdCompactHint(opts) {
11504
11575
  const pressure = getContextPressure(cache ?? void 0);
11505
11576
  const [manifest, eventCount] = sessionId !== null ? buildManifestWithCount(sessionId) : ["", 0];
11506
11577
  const manifestTokens = estimateTokens(manifest);
11507
- const pct = (pressure.fillFraction * 100).toFixed(1);
11578
+ const pct2 = (pressure.fillFraction * 100).toFixed(1);
11508
11579
  if (opts.json === true) {
11509
- const out2 = { tier: pressure.tier, fillFraction: pressure.fillFraction, pct: Number(pct), manifestTokens, eventCount };
11580
+ const out2 = { tier: pressure.tier, fillFraction: pressure.fillFraction, pct: Number(pct2), manifestTokens, eventCount };
11510
11581
  if (sessionId !== null) out2["sessionId"] = sessionId;
11511
11582
  if (opts.trigger !== void 0) out2["trigger"] = opts.trigger;
11512
11583
  process.stdout.write(JSON.stringify(out2, null, 2) + "\n");
11513
11584
  return;
11514
11585
  }
11515
- process.stdout.write(`Compact hint \u2014 context: ${pressure.tier} (${pct}% full)
11586
+ process.stdout.write(`Compact hint \u2014 context: ${pressure.tier} (${pct2}% full)
11516
11587
  `);
11517
11588
  if (sessionId !== null) process.stdout.write(`Session: ${sessionId}
11518
11589
  `);
@@ -11612,6 +11683,19 @@ function indexSizeBytes(dbPath) {
11612
11683
  }
11613
11684
  return total;
11614
11685
  }
11686
+ function comparableSizes(result) {
11687
+ const walInFlight = result.checkpointBusy && result.afterBytes > result.beforeBytes;
11688
+ const before = walInFlight ? result.beforeDbBytes : result.beforeBytes;
11689
+ const after = walInFlight ? result.afterDbBytes : result.afterBytes;
11690
+ return { before, after, freed: before - after };
11691
+ }
11692
+ function dbFileBytes(dbPath) {
11693
+ try {
11694
+ return fs18.statSync(dbPath).size;
11695
+ } catch {
11696
+ return 0;
11697
+ }
11698
+ }
11615
11699
  function tableExists(db, table) {
11616
11700
  const row = db.prepare(`SELECT 1 AS present FROM sqlite_master WHERE type IN ('table','view') AND name = ?`).get(table);
11617
11701
  return row?.present === 1;
@@ -11619,6 +11703,7 @@ function tableExists(db, table) {
11619
11703
  function reclaimIndex(dbPath, opts = {}) {
11620
11704
  const rebuild = opts.rebuild === true;
11621
11705
  const beforeBytes = indexSizeBytes(dbPath);
11706
+ const beforeDbBytes = dbFileBytes(dbPath);
11622
11707
  const db = getDb(dbPath);
11623
11708
  const dropped = {};
11624
11709
  if (rebuild) {
@@ -11643,6 +11728,8 @@ function reclaimIndex(dbPath, opts = {}) {
11643
11728
  return {
11644
11729
  beforeBytes,
11645
11730
  afterBytes: indexSizeBytes(dbPath),
11731
+ beforeDbBytes,
11732
+ afterDbBytes: dbFileBytes(dbPath),
11646
11733
  dropped,
11647
11734
  rebuilt: rebuild,
11648
11735
  checkpointBusy: checkpointBusy || finalCheckpointBusy,
@@ -11678,13 +11765,12 @@ function cmdReclaimIndex(opts) {
11678
11765
  process.stdout.write(JSON.stringify({ dbPath, ...result }, null, 2) + "\n");
11679
11766
  return;
11680
11767
  }
11681
- const freed = result.beforeBytes - result.afterBytes;
11768
+ const { before, after, freed } = comparableSizes(result);
11769
+ const delta = freed >= 0 ? `freed ${mb(freed)}` : `grew ${mb(-freed)}`;
11682
11770
  process.stdout.write(`reclaim-index: ${dbPath}
11683
11771
  `);
11684
- process.stdout.write(
11685
- ` ${mb(result.beforeBytes)} -> ${mb(result.afterBytes)} (freed ${mb(freed)})
11686
- `
11687
- );
11772
+ process.stdout.write(` ${mb(before)} -> ${mb(after)} (${delta})
11773
+ `);
11688
11774
  if (result.rebuilt) {
11689
11775
  for (const [table, n] of Object.entries(result.dropped)) {
11690
11776
  process.stdout.write(` dropped ${n} row(s) from ${table}
@@ -11703,7 +11789,7 @@ function cmdReclaimIndex(opts) {
11703
11789
  }
11704
11790
  if (result.checkpointBusy) {
11705
11791
  process.stdout.write(
11706
- ` note: a concurrent reader blocked WAL truncation, so some space may still be held in ${path18.basename(dbPath)}-wal
11792
+ ` note: a concurrent reader blocked WAL truncation, so some space is still held in ${path18.basename(dbPath)}-wal and the figures above count the main file only. The next checkpoint releases it
11707
11793
  `
11708
11794
  );
11709
11795
  }
@@ -12183,7 +12269,7 @@ function cmdProject(opts) {
12183
12269
  }
12184
12270
  function recordCompactDocStat(fullSourceBytes, emittedText, detail) {
12185
12271
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(emittedText, "utf8"));
12186
- recordStat("compact_doc", bytesSaved, Math.round(bytesSaved / 4), void 0, detail);
12272
+ recordStat("compact_doc", bytesSaved, savedTokensFromBytes(bytesSaved), void 0, detail);
12187
12273
  }
12188
12274
  function cmdCompactDoc(opts) {
12189
12275
  const resolved = path19.resolve(opts.filePath);
@@ -13108,10 +13194,10 @@ function printCopilotReport(report) {
13108
13194
  w(` Conversation: ${conversationTokens.toLocaleString()} tok
13109
13195
  `);
13110
13196
  if (total > 0) {
13111
- const pct = (fixed / total * 100).toFixed(1);
13197
+ const pct2 = (fixed / total * 100).toFixed(1);
13112
13198
  w(` ${fixed.toLocaleString()} tok of system prompt and tool definitions ships with every
13113
13199
  `);
13114
- w(` request; at shutdown that was ${pct}% of the context. No hook can reach it: Copilot
13200
+ w(` request; at shutdown that was ${pct2}% of the context. No hook can reach it: Copilot
13115
13201
  `);
13116
13202
  w(" assembles both natively, with nothing between assembly and send. The levers are all\n");
13117
13203
  w(" config: fewer MCP servers and custom tools, and --excluded-tools / --available-tools /\n");
@@ -13171,9 +13257,9 @@ function printCopilotReport(report) {
13171
13257
  `);
13172
13258
  } else {
13173
13259
  for (const block of report.blocks) {
13174
- const pct = block.bytes > 0 ? (block.repeatBytes / block.bytes * 100).toFixed(1) : "0.0";
13260
+ const pct2 = block.bytes > 0 ? (block.repeatBytes / block.bytes * 100).toFixed(1) : "0.0";
13175
13261
  w(` ${block.kind}: ${countNoun(block.count, "injection")}, ${formatBytes(block.bytes)}`);
13176
- w(block.repeatBytes > 0 ? `, ${formatBytes(block.repeatBytes)} re-sent verbatim (${pct}%)
13262
+ w(block.repeatBytes > 0 ? `, ${formatBytes(block.repeatBytes)} re-sent verbatim (${pct2}%)
13177
13263
  ` : "\n");
13178
13264
  }
13179
13265
  }
@@ -13503,14 +13589,650 @@ function formatSessionSlice(turns) {
13503
13589
  return parts.join("\n").trimEnd();
13504
13590
  }
13505
13591
 
13506
- // src/cli_mcp_audit.ts
13592
+ // src/session_audit.ts
13507
13593
  import * as fs26 from "node:fs";
13508
13594
  import * as os12 from "node:os";
13509
13595
  import * as path26 from "node:path";
13596
+ import * as readline3 from "node:readline";
13597
+ function emptyMeasured() {
13598
+ return { apiCalls: 0, inputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, outputTokens: 0 };
13599
+ }
13600
+ function emptyCategory() {
13601
+ return { count: 0, bytes: 0, estTokens: 0 };
13602
+ }
13603
+ function addCategory(cat, bytes) {
13604
+ cat.count += 1;
13605
+ cat.bytes += bytes;
13606
+ cat.estTokens += estimateTokensFromLength(bytes);
13607
+ }
13608
+ var LOCAL_ONLY_TYPES = /* @__PURE__ */ new Set([
13609
+ "file-history-snapshot",
13610
+ "queue-operation",
13611
+ "mode",
13612
+ "permission-mode",
13613
+ "last-prompt",
13614
+ "bridge-session",
13615
+ "ai-title"
13616
+ ]);
13617
+ var ATTACHMENT_VISIBLE_FIELDS = {
13618
+ hook_success: ["content"],
13619
+ hook_additional_context: ["content"],
13620
+ task_reminder: ["content"],
13621
+ skill_listing: ["content"],
13622
+ agent_listing_delta: ["addedLines"],
13623
+ file: ["content"],
13624
+ queued_command: ["prompt"],
13625
+ plan_file_reference: ["planContent"],
13626
+ edited_text_file: ["snippet"],
13627
+ read_truncation_notice: ["banner"],
13628
+ total_tokens_reminder: ["text"],
13629
+ nested_memory: ["content"],
13630
+ mcp_instructions_delta: ["addedBlocks"],
13631
+ invoked_skills: ["skills"],
13632
+ date_change: ["newDate"],
13633
+ goal_status: ["condition"],
13634
+ task_status: ["deltaSummary", "description"],
13635
+ command_permissions: ["allowedTools"]
13636
+ };
13637
+ var CACHE_WRITE_MULTIPLIER = 1.25;
13638
+ var CACHE_READ_MULTIPLIER = 0.1;
13639
+ var READ_DIVERT_MARKER_RE = /(?:was already read this session|Already read |You've already read|Use `token-goat (?:section|read|bash-output|config-get|skeleton)|token-goat bash-output --file)/;
13640
+ var READ_DIVERT_MAX_BYTES = 2500;
13641
+ var READ_FULL_SERVE_MIN_BYTES = 10240;
13642
+ var BASH_FILTER_MARKER_RE = /\[token-goat[:\]]/;
13643
+ var BASH_SMALL_RESULT_MAX_BYTES = 1024;
13644
+ function commandHead(raw) {
13645
+ let s = raw.trim();
13646
+ for (let guard = 0; guard < 20; guard++) {
13647
+ const stripped = s.replace(/^[(\s]+/, "");
13648
+ const env = /^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+/.exec(stripped);
13649
+ if (env !== null) {
13650
+ s = stripped.slice(env[0].length);
13651
+ continue;
13652
+ }
13653
+ const cd = /^cd\s+(?:"[^"]*"|'[^']*'|[^\s;&]+)[^\S\n]*(?:&&|;|\n)\s*/.exec(stripped);
13654
+ if (cd !== null) {
13655
+ s = stripped.slice(cd[0].length);
13656
+ continue;
13657
+ }
13658
+ if (stripped === s) break;
13659
+ s = stripped;
13660
+ }
13661
+ const m = /^"([^"]+)"|^'([^']+)'|^(\S+)/.exec(s);
13662
+ const tok = m === null ? "" : m[1] ?? m[2] ?? m[3] ?? "";
13663
+ const base = tok.split(/[\\/]/).pop() ?? "";
13664
+ const head = base.replace(/\.exe$/i, "").toLowerCase();
13665
+ return head === "" ? "(none)" : head;
13666
+ }
13667
+ function normalizeReadPath(p) {
13668
+ return p.replace(/\\/g, "/").toLowerCase();
13669
+ }
13670
+ function deepStringBytes(value) {
13671
+ if (typeof value === "string") return Buffer.byteLength(value, "utf8");
13672
+ if (Array.isArray(value)) return value.reduce((acc, v) => acc + deepStringBytes(v), 0);
13673
+ if (value !== null && typeof value === "object") {
13674
+ return Object.values(value).reduce((acc, v) => acc + deepStringBytes(v), 0);
13675
+ }
13676
+ return 0;
13677
+ }
13678
+ var OMISSION_MARKER_RE = /(?:--- (\d+) lines omitted ---|\[token-goat: \+?(\d+) more [a-z ]*lines omitted\]|--- patch: (\d+) lines omitted by token-goat ---|\.\.\. (\d+) lines omitted by token-goat \.\.\.)/g;
13679
+ function toolResultText(content) {
13680
+ if (typeof content === "string") return content;
13681
+ if (Array.isArray(content)) {
13682
+ let text = "";
13683
+ for (const block of content) {
13684
+ if (block !== null && typeof block === "object" && typeof block.text === "string") {
13685
+ text += block.text;
13686
+ }
13687
+ }
13688
+ return text;
13689
+ }
13690
+ return "";
13691
+ }
13692
+ function listCorpusTranscripts(corpusDir) {
13693
+ const found = [];
13694
+ const walk = (dir, entries) => {
13695
+ for (const entry of entries) {
13696
+ const full = path26.join(dir, entry.name);
13697
+ if (entry.name.endsWith(".jsonl")) {
13698
+ found.push(full);
13699
+ } else if (entry.isDirectory()) {
13700
+ let inner;
13701
+ try {
13702
+ inner = fs26.readdirSync(full, { withFileTypes: true });
13703
+ } catch {
13704
+ continue;
13705
+ }
13706
+ walk(full, inner);
13707
+ }
13708
+ }
13709
+ };
13710
+ walk(corpusDir, fs26.readdirSync(corpusDir, { withFileTypes: true }));
13711
+ return found.sort();
13712
+ }
13713
+ function defaultCorpusDir() {
13714
+ return path26.join(os12.homedir(), ".claude", "projects");
13715
+ }
13716
+ async function auditOneFile(filePath, s, toolMap, attachmentMap, hookMap, laneObservations, bashHeadMap) {
13717
+ const isLane = filePath.split(/[\\/]/).includes("subagents");
13718
+ let laneFirstPrefix = null;
13719
+ let laneBriefBytes = -1;
13720
+ const toolNameById = /* @__PURE__ */ new Map();
13721
+ const bashHeadById = /* @__PURE__ */ new Map();
13722
+ let pendingBashReread = [];
13723
+ const readCallById = /* @__PURE__ */ new Map();
13724
+ const readPathEpoch = /* @__PURE__ */ new Map();
13725
+ let compactEpoch = 0;
13726
+ let sawTokenGoatHook = false;
13727
+ let fileRepeats = 0;
13728
+ let fileRepeatsFullNoRange = 0;
13729
+ let fileRepeatsFullNoRangeAfterCompaction = 0;
13730
+ const usageSeenIds = /* @__PURE__ */ new Set();
13731
+ const perCall = [];
13732
+ const laneCalls = [0, 0];
13733
+ let pendingReread = [];
13734
+ const lastVisibleByKind = /* @__PURE__ */ new Map();
13735
+ const flushLane = (lane) => {
13736
+ const kept = [];
13737
+ for (const p of pendingReread) {
13738
+ if (p.lane !== lane) {
13739
+ kept.push(p);
13740
+ continue;
13741
+ }
13742
+ attachmentRollup(attachmentMap, p.kind).rereadTokens += p.tokens * Math.max(0, laneCalls[lane] - p.atCall);
13743
+ }
13744
+ pendingReread = kept;
13745
+ const keptBash = [];
13746
+ for (const p of pendingBashReread) {
13747
+ if (p.lane !== lane) {
13748
+ keptBash.push(p);
13749
+ continue;
13750
+ }
13751
+ s.bashInterception.untouchedRereadTokens += p.tokens * Math.max(0, laneCalls[lane] - p.atCall);
13752
+ }
13753
+ pendingBashReread = keptBash;
13754
+ };
13755
+ const stream = fs26.createReadStream(filePath);
13756
+ const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
13757
+ const streamFailure = new Promise((_, reject) => stream.on("error", reject));
13758
+ const consume = (async () => {
13759
+ for await (const line of rl) {
13760
+ if (line.length === 0) continue;
13761
+ s.lines += 1;
13762
+ const lineBytes = Buffer.byteLength(line, "utf8");
13763
+ s.totalBytes += lineBytes;
13764
+ let obj;
13765
+ try {
13766
+ obj = JSON.parse(line);
13767
+ } catch {
13768
+ s.parseFailedLines += 1;
13769
+ addCategory(s.estimated.otherLocal, lineBytes);
13770
+ continue;
13771
+ }
13772
+ const type = typeof obj["type"] === "string" ? obj["type"] : "(untyped)";
13773
+ const census = s.lineTypes[type] ??= { lines: 0, bytes: 0 };
13774
+ census.lines += 1;
13775
+ census.bytes += lineBytes;
13776
+ if (LOCAL_ONLY_TYPES.has(type)) {
13777
+ addCategory(s.estimated.otherLocal, lineBytes);
13778
+ continue;
13779
+ }
13780
+ const message = obj["message"];
13781
+ if (type === "assistant" && message !== void 0) {
13782
+ const usage = message.usage;
13783
+ if (usage !== void 0 && typeof message.id === "string" && !usageSeenIds.has(message.id)) {
13784
+ usageSeenIds.add(message.id);
13785
+ const num = (k) => typeof usage[k] === "number" ? usage[k] : 0;
13786
+ const input = num("input_tokens");
13787
+ const cacheWrite = num("cache_creation_input_tokens");
13788
+ const cacheRead = num("cache_read_input_tokens");
13789
+ const output = num("output_tokens");
13790
+ s.measured.apiCalls += 1;
13791
+ s.measured.inputTokens += input;
13792
+ s.measured.cacheCreationTokens += cacheWrite;
13793
+ s.measured.cacheReadTokens += cacheRead;
13794
+ s.measured.outputTokens += output;
13795
+ if (obj["isSidechain"] === true) {
13796
+ s.measuredSidechain.apiCalls += 1;
13797
+ s.measuredSidechain.inputTokens += input;
13798
+ s.measuredSidechain.cacheCreationTokens += cacheWrite;
13799
+ s.measuredSidechain.cacheReadTokens += cacheRead;
13800
+ s.measuredSidechain.outputTokens += output;
13801
+ }
13802
+ if (isLane && laneFirstPrefix === null) laneFirstPrefix = input + cacheWrite + cacheRead;
13803
+ perCall.push({ inputTotal: input + cacheWrite + cacheRead, cacheRead, output });
13804
+ const callLane = obj["isSidechain"] === true ? 1 : 0;
13805
+ laneCalls[callLane] = laneCalls[callLane] + 1;
13806
+ }
13807
+ const blocks = Array.isArray(message.content) ? message.content : [];
13808
+ for (const block of blocks) {
13809
+ if (block === null || typeof block !== "object") continue;
13810
+ if (block["type"] === "text" && typeof block["text"] === "string") {
13811
+ addCategory(s.estimated.assistantText, Buffer.byteLength(block["text"], "utf8"));
13812
+ } else if (block["type"] === "thinking" && typeof block["thinking"] === "string") {
13813
+ addCategory(s.estimated.assistantThinking, Buffer.byteLength(block["thinking"], "utf8"));
13814
+ } else if (block["type"] === "tool_use" && typeof block["id"] === "string") {
13815
+ const name = typeof block["name"] === "string" ? block["name"] : "(unnamed)";
13816
+ if (!toolNameById.has(block["id"])) {
13817
+ toolNameById.set(block["id"], name);
13818
+ toolRollup(toolMap, name).calls += 1;
13819
+ const input = block["input"] !== null && typeof block["input"] === "object" ? block["input"] : {};
13820
+ if (name === "Bash" && typeof input["command"] === "string") {
13821
+ bashHeadById.set(block["id"], commandHead(input["command"]));
13822
+ } else if (name === "Read" && typeof input["file_path"] === "string") {
13823
+ const norm = normalizeReadPath(input["file_path"]);
13824
+ const priorEpoch = readPathEpoch.get(norm);
13825
+ readCallById.set(block["id"], { wasSeenBefore: priorEpoch !== void 0, hasRange: input["offset"] !== void 0 || input["limit"] !== void 0, afterCompaction: priorEpoch !== void 0 && compactEpoch > priorEpoch });
13826
+ readPathEpoch.set(norm, compactEpoch);
13827
+ }
13828
+ }
13829
+ let inputBytes;
13830
+ try {
13831
+ inputBytes = Buffer.byteLength(JSON.stringify(block["input"] ?? null), "utf8");
13832
+ } catch {
13833
+ inputBytes = 0;
13834
+ }
13835
+ addCategory(s.estimated.toolUseInputs, inputBytes);
13836
+ }
13837
+ }
13838
+ continue;
13839
+ }
13840
+ if (type === "user" && message !== void 0) {
13841
+ const blocks = Array.isArray(message.content) ? message.content : [];
13842
+ let sawToolResult = false;
13843
+ for (const block of blocks) {
13844
+ if (block === null || typeof block !== "object" || block["type"] !== "tool_result") continue;
13845
+ sawToolResult = true;
13846
+ const text = toolResultText(block["content"]);
13847
+ const bytes = Buffer.byteLength(text, "utf8");
13848
+ const id = typeof block["tool_use_id"] === "string" ? block["tool_use_id"] : "";
13849
+ const name = toolNameById.get(id) ?? "(unknown)";
13850
+ addCategory(s.estimated.toolResults, bytes);
13851
+ const roll = toolRollup(toolMap, name);
13852
+ if (!toolNameById.has(id)) roll.calls += 1;
13853
+ roll.resultBytes += bytes;
13854
+ roll.resultEstTokens += estimateTokensFromLength(bytes);
13855
+ if (name === "Read") {
13856
+ s.readInterception.readResults += 1;
13857
+ if (bytes < READ_DIVERT_MAX_BYTES && READ_DIVERT_MARKER_RE.test(text)) {
13858
+ s.readInterception.divertedByMarker += 1;
13859
+ s.readInterception.divertedBytes += bytes;
13860
+ } else if (bytes >= READ_FULL_SERVE_MIN_BYTES) {
13861
+ s.readInterception.fullServesOver10k += 1;
13862
+ s.readInterception.fullServeBytesOver10k += bytes;
13863
+ const call = readCallById.get(id);
13864
+ if (call === void 0) {
13865
+ s.readInterception.fullServesPathUnknown += 1;
13866
+ } else if (call.wasSeenBefore) {
13867
+ s.readInterception.fullServesRepeat += 1;
13868
+ s.readInterception.repeatBytes += bytes;
13869
+ fileRepeats += 1;
13870
+ if (call.hasRange) {
13871
+ s.readInterception.repeatWithRange += 1;
13872
+ } else {
13873
+ s.readInterception.repeatFullNoRange += 1;
13874
+ fileRepeatsFullNoRange += 1;
13875
+ if (call.afterCompaction) fileRepeatsFullNoRangeAfterCompaction += 1;
13876
+ }
13877
+ } else {
13878
+ s.readInterception.fullServesFirstRead += 1;
13879
+ }
13880
+ }
13881
+ } else if (name === "Bash") {
13882
+ const b = s.bashInterception;
13883
+ b.bashResults += 1;
13884
+ if (BASH_FILTER_MARKER_RE.test(text)) {
13885
+ b.markedByFilter += 1;
13886
+ b.markedBytes += bytes;
13887
+ } else if (bytes < BASH_SMALL_RESULT_MAX_BYTES) {
13888
+ b.smallUntouched += 1;
13889
+ b.smallUntouchedBytes += bytes;
13890
+ } else {
13891
+ b.untouched += 1;
13892
+ b.untouchedBytes += bytes;
13893
+ const tokens = estimateTokensFromLength(bytes);
13894
+ b.untouchedEstTokens += tokens;
13895
+ const lane = obj["isSidechain"] === true ? 1 : 0;
13896
+ pendingBashReread.push({ tokens, atCall: laneCalls[lane], lane });
13897
+ const head = bashHeadById.get(id) ?? "(unknown)";
13898
+ let headRoll = bashHeadMap.get(head);
13899
+ if (headRoll === void 0) {
13900
+ headRoll = { head, results: 0, bytes: 0 };
13901
+ bashHeadMap.set(head, headRoll);
13902
+ }
13903
+ headRoll.results += 1;
13904
+ headRoll.bytes += bytes;
13905
+ }
13906
+ }
13907
+ OMISSION_MARKER_RE.lastIndex = 0;
13908
+ for (const m of text.matchAll(OMISSION_MARKER_RE)) {
13909
+ s.omissionMarkers.fires += 1;
13910
+ s.omissionMarkers.linesOmitted += Number(m[1] ?? m[2] ?? m[3] ?? m[4] ?? 0);
13911
+ }
13912
+ }
13913
+ if (!sawToolResult) {
13914
+ const target = obj["isMeta"] === true ? s.estimated.harnessMeta : s.estimated.userTurns;
13915
+ let contentBytes = 0;
13916
+ if (typeof message.content === "string") {
13917
+ contentBytes = Buffer.byteLength(message.content, "utf8");
13918
+ } else {
13919
+ for (const block of blocks) {
13920
+ if (block !== null && typeof block === "object" && typeof block["text"] === "string") {
13921
+ contentBytes += Buffer.byteLength(block["text"], "utf8");
13922
+ }
13923
+ }
13924
+ }
13925
+ addCategory(target, contentBytes);
13926
+ if (isLane && laneBriefBytes < 0 && obj["isMeta"] !== true) laneBriefBytes = contentBytes;
13927
+ }
13928
+ continue;
13929
+ }
13930
+ if (type === "attachment") {
13931
+ addCategory(s.estimated.attachments, lineBytes);
13932
+ const att = obj["attachment"] !== null && typeof obj["attachment"] === "object" ? obj["attachment"] : {};
13933
+ const kind = typeof att["type"] === "string" ? att["type"] : "(untyped)";
13934
+ const lane = obj["isSidechain"] === true ? 1 : 0;
13935
+ const roll = attachmentRollup(attachmentMap, kind);
13936
+ roll.injections += 1;
13937
+ roll.lineBytes += lineBytes;
13938
+ const fields = ATTACHMENT_VISIBLE_FIELDS[kind];
13939
+ if (fields !== void 0) {
13940
+ const visibleBytes = fields.reduce((acc, f) => acc + deepStringBytes(att[f]), 0);
13941
+ roll.visibleBytes += visibleBytes;
13942
+ const tokens = visibleBytes > 0 ? estimateTokensFromLength(visibleBytes) : 0;
13943
+ roll.estTokens += tokens;
13944
+ if (tokens > 0) pendingReread.push({ kind, tokens, atCall: laneCalls[lane], lane });
13945
+ let serialized;
13946
+ try {
13947
+ serialized = JSON.stringify(fields.map((f) => att[f]));
13948
+ } catch {
13949
+ serialized = "";
13950
+ }
13951
+ const previous = lastVisibleByKind.get(kind);
13952
+ if (previous !== void 0) {
13953
+ if (previous === serialized) roll.repeatedIdentical += 1;
13954
+ else roll.repeatedChanged += 1;
13955
+ }
13956
+ lastVisibleByKind.set(kind, serialized);
13957
+ }
13958
+ if (kind === "hook_success") {
13959
+ const command = typeof att["command"] === "string" ? att["command"] : "";
13960
+ const origin = command.includes("token-goat") || command.includes("token_goat") ? "token-goat" : "other";
13961
+ if (origin === "token-goat") sawTokenGoatHook = true;
13962
+ const event = typeof att["hookEvent"] === "string" ? att["hookEvent"] : "(none)";
13963
+ const hook = hookRollup(hookMap, origin, event);
13964
+ hook.fires += 1;
13965
+ hook.stdoutBytes += typeof att["stdout"] === "string" ? Buffer.byteLength(att["stdout"], "utf8") : 0;
13966
+ hook.contextBytes += typeof att["content"] === "string" ? Buffer.byteLength(att["content"], "utf8") : 0;
13967
+ }
13968
+ continue;
13969
+ }
13970
+ if (type === "system" || type === "summary") {
13971
+ if (obj["subtype"] === "compact_boundary") {
13972
+ flushLane(obj["isSidechain"] === true ? 1 : 0);
13973
+ compactEpoch += 1;
13974
+ }
13975
+ addCategory(s.estimated.system, lineBytes);
13976
+ continue;
13977
+ }
13978
+ addCategory(s.estimated.otherLocal, lineBytes);
13979
+ }
13980
+ })();
13981
+ void consume.catch(() => {
13982
+ });
13983
+ void streamFailure.catch(() => {
13984
+ });
13985
+ try {
13986
+ await Promise.race([consume, streamFailure]);
13987
+ } finally {
13988
+ rl.close();
13989
+ stream.destroy();
13990
+ }
13991
+ flushLane(0);
13992
+ flushLane(1);
13993
+ if (sawTokenGoatHook) {
13994
+ s.readInterception.repeatInHookedSessions += fileRepeats;
13995
+ s.readInterception.repeatFullNoRangeInHookedSessions += fileRepeatsFullNoRange;
13996
+ s.readInterception.repeatFullNoRangeHookedAfterCompaction += fileRepeatsFullNoRangeAfterCompaction;
13997
+ }
13998
+ if (isLane) {
13999
+ let agentType = "(none)";
14000
+ try {
14001
+ const meta = JSON.parse(fs26.readFileSync(filePath.replace(/\.jsonl$/i, ".meta.json"), "utf8"));
14002
+ if (typeof meta["agentType"] === "string" && meta["agentType"] !== "") agentType = meta["agentType"];
14003
+ } catch {
14004
+ }
14005
+ laneObservations.push({ firstPrefixTokens: laneFirstPrefix, calls: usageSeenIds.size, briefBytes: Math.max(0, laneBriefBytes), agentType });
14006
+ }
14007
+ const callCount = perCall.length;
14008
+ for (let i = 0; i < callCount; i++) {
14009
+ const call = perCall[i];
14010
+ const d = s.positionDeciles[Math.min(9, Math.floor(i * 10 / callCount))];
14011
+ d.apiCalls += 1;
14012
+ d.inputTokens += call.inputTotal;
14013
+ d.cacheReadTokens += call.cacheRead;
14014
+ d.outputTokens += call.output;
14015
+ }
14016
+ }
14017
+ function toolRollup(toolMap, name) {
14018
+ let roll = toolMap.get(name);
14019
+ if (roll === void 0) {
14020
+ roll = { name, calls: 0, resultBytes: 0, resultEstTokens: 0 };
14021
+ toolMap.set(name, roll);
14022
+ }
14023
+ return roll;
14024
+ }
14025
+ function attachmentRollup(attachmentMap, kind) {
14026
+ let roll = attachmentMap.get(kind);
14027
+ if (roll === void 0) {
14028
+ roll = { kind, injections: 0, lineBytes: 0, visibleBytes: 0, estTokens: 0, rereadTokens: 0, billedEquivTokens: 0, repeatedIdentical: 0, repeatedChanged: 0 };
14029
+ attachmentMap.set(kind, roll);
14030
+ }
14031
+ return roll;
14032
+ }
14033
+ function hookRollup(hookMap, origin, event) {
14034
+ const key = `${origin}|${event}`;
14035
+ let roll = hookMap.get(key);
14036
+ if (roll === void 0) {
14037
+ roll = { origin, event, fires: 0, stdoutBytes: 0, contextBytes: 0 };
14038
+ hookMap.set(key, roll);
14039
+ }
14040
+ return roll;
14041
+ }
14042
+ async function auditSessionCorpus(opts = {}) {
14043
+ const corpusDir = path26.resolve(opts.dir ?? defaultCorpusDir());
14044
+ if (!fs26.existsSync(corpusDir)) {
14045
+ throw new Error(`session corpus directory not found: ${corpusDir}`);
14046
+ }
14047
+ const files = listCorpusTranscripts(corpusDir);
14048
+ if (files.length === 0) {
14049
+ throw new Error(`no .jsonl session transcripts found under ${corpusDir}`);
14050
+ }
14051
+ const started = Date.now();
14052
+ const summary = {
14053
+ corpusDir,
14054
+ filesScanned: 0,
14055
+ filesFailed: 0,
14056
+ lines: 0,
14057
+ parseFailedLines: 0,
14058
+ totalBytes: 0,
14059
+ runtimeMs: 0,
14060
+ measured: emptyMeasured(),
14061
+ measuredSidechain: emptyMeasured(),
14062
+ estimated: {
14063
+ userTurns: emptyCategory(),
14064
+ toolResults: emptyCategory(),
14065
+ assistantText: emptyCategory(),
14066
+ assistantThinking: emptyCategory(),
14067
+ toolUseInputs: emptyCategory(),
14068
+ attachments: emptyCategory(),
14069
+ harnessMeta: emptyCategory(),
14070
+ system: emptyCategory(),
14071
+ otherLocal: emptyCategory()
14072
+ },
14073
+ tools: [],
14074
+ attachmentKinds: [],
14075
+ hookOutputs: [],
14076
+ positionDeciles: Array.from({ length: 10 }, (_, i) => ({ decile: i + 1, apiCalls: 0, inputTokens: 0, cacheReadTokens: 0, outputTokens: 0 })),
14077
+ sidechainLanes: { laneFiles: 0, lanesWithUsage: 0, meanFirstCallPrefixTokens: 0, medianFirstCallPrefixTokens: 0, p90FirstCallPrefixTokens: 0, meanCallsPerLane: 0, meanBriefBytes: 0, prefixBilledEquivTokens: 0 },
14078
+ laneAgentTypes: [],
14079
+ readInterception: { readResults: 0, divertedByMarker: 0, divertedBytes: 0, fullServesOver10k: 0, fullServeBytesOver10k: 0, fullServesFirstRead: 0, fullServesRepeat: 0, repeatBytes: 0, repeatWithRange: 0, repeatFullNoRange: 0, repeatInHookedSessions: 0, repeatFullNoRangeInHookedSessions: 0, repeatFullNoRangeHookedAfterCompaction: 0, fullServesPathUnknown: 0 },
14080
+ bashInterception: { bashResults: 0, markedByFilter: 0, markedBytes: 0, smallUntouched: 0, smallUntouchedBytes: 0, untouched: 0, untouchedBytes: 0, untouchedEstTokens: 0, untouchedRereadTokens: 0, untouchedBilledEquivTokens: 0, untouchedHeads: [] },
14081
+ lineTypes: {},
14082
+ omissionMarkers: { fires: 0, linesOmitted: 0 }
14083
+ };
14084
+ const toolMap = /* @__PURE__ */ new Map();
14085
+ const attachmentMap = /* @__PURE__ */ new Map();
14086
+ const hookMap = /* @__PURE__ */ new Map();
14087
+ const laneObservations = [];
14088
+ const bashHeadMap = /* @__PURE__ */ new Map();
14089
+ for (const file of files) {
14090
+ try {
14091
+ await auditOneFile(file, summary, toolMap, attachmentMap, hookMap, laneObservations, bashHeadMap);
14092
+ summary.filesScanned += 1;
14093
+ } catch {
14094
+ summary.filesFailed += 1;
14095
+ }
14096
+ }
14097
+ const withUsage = laneObservations.filter((l) => l.firstPrefixTokens !== null);
14098
+ const prefixes = withUsage.map((l) => l.firstPrefixTokens).sort((a, b) => a - b);
14099
+ const mean = (arr) => arr.length === 0 ? 0 : Math.round(arr.reduce((a, b) => a + b, 0) / arr.length);
14100
+ summary.sidechainLanes = {
14101
+ laneFiles: laneObservations.length,
14102
+ lanesWithUsage: withUsage.length,
14103
+ meanFirstCallPrefixTokens: mean(prefixes),
14104
+ medianFirstCallPrefixTokens: prefixes.length === 0 ? 0 : prefixes[Math.floor(prefixes.length / 2)],
14105
+ p90FirstCallPrefixTokens: prefixes.length === 0 ? 0 : prefixes[Math.min(prefixes.length - 1, Math.floor(prefixes.length * 0.9))],
14106
+ meanCallsPerLane: mean(withUsage.map((l) => l.calls)),
14107
+ meanBriefBytes: mean(withUsage.map((l) => l.briefBytes)),
14108
+ prefixBilledEquivTokens: Math.round(withUsage.reduce((acc, l) => acc + l.firstPrefixTokens * (CACHE_WRITE_MULTIPLIER + CACHE_READ_MULTIPLIER * Math.max(0, l.calls - 1)), 0))
14109
+ };
14110
+ const byType = /* @__PURE__ */ new Map();
14111
+ for (const l of laneObservations) {
14112
+ const bucket = byType.get(l.agentType);
14113
+ if (bucket === void 0) byType.set(l.agentType, [l]);
14114
+ else bucket.push(l);
14115
+ }
14116
+ summary.laneAgentTypes = [...byType.entries()].map(([agentType, obs]) => {
14117
+ const typed = obs.filter((l) => l.firstPrefixTokens !== null).map((l) => l.firstPrefixTokens).sort((a, b) => a - b);
14118
+ return { agentType, lanes: obs.length, lanesWithUsage: typed.length, meanFirstCallPrefixTokens: mean(typed), medianFirstCallPrefixTokens: typed.length === 0 ? 0 : typed[Math.floor(typed.length / 2)] };
14119
+ }).sort((a, b) => b.lanes - a.lanes || a.agentType.localeCompare(b.agentType));
14120
+ summary.bashInterception.untouchedBilledEquivTokens = Math.round(CACHE_WRITE_MULTIPLIER * summary.bashInterception.untouchedEstTokens + CACHE_READ_MULTIPLIER * summary.bashInterception.untouchedRereadTokens);
14121
+ summary.bashInterception.untouchedHeads = [...bashHeadMap.values()].sort((a, b) => b.bytes - a.bytes || b.results - a.results || a.head.localeCompare(b.head));
14122
+ summary.tools = [...toolMap.values()].sort((a, b) => b.resultBytes - a.resultBytes || a.name.localeCompare(b.name));
14123
+ for (const roll of attachmentMap.values()) {
14124
+ roll.billedEquivTokens = Math.round(CACHE_WRITE_MULTIPLIER * roll.estTokens + CACHE_READ_MULTIPLIER * roll.rereadTokens);
14125
+ }
14126
+ summary.attachmentKinds = [...attachmentMap.values()].sort((a, b) => b.billedEquivTokens - a.billedEquivTokens || b.injections - a.injections || a.kind.localeCompare(b.kind));
14127
+ summary.hookOutputs = [...hookMap.values()].sort((a, b) => b.contextBytes - a.contextBytes || b.fires - a.fires || a.origin.localeCompare(b.origin) || a.event.localeCompare(b.event));
14128
+ summary.runtimeMs = Date.now() - started;
14129
+ return summary;
14130
+ }
14131
+ function fmt(n) {
14132
+ return n.toLocaleString("en-US");
14133
+ }
14134
+ function pct(part, whole) {
14135
+ return whole === 0 ? "0.0%" : `${(part / whole * 100).toFixed(1)}%`;
14136
+ }
14137
+ function formatSessionAudit(s) {
14138
+ const lines = [];
14139
+ lines.push("# Session corpus audit");
14140
+ lines.push(`Corpus: ${s.corpusDir}`);
14141
+ lines.push(`Files: ${fmt(s.filesScanned)} scanned, ${fmt(s.filesFailed)} unreadable`);
14142
+ lines.push(`Lines: ${fmt(s.lines)} (${fmt(s.parseFailedLines)} unparseable), bytes: ${fmt(s.totalBytes)}`);
14143
+ lines.push(`Runtime: ${(s.runtimeMs / 1e3).toFixed(1)}s`);
14144
+ lines.push("");
14145
+ lines.push("## Measured billed tokens (assistant message.usage, one count per API response)");
14146
+ const m = s.measured;
14147
+ lines.push(`API calls: ${fmt(m.apiCalls)} (sidechain: ${fmt(s.measuredSidechain.apiCalls)})`);
14148
+ lines.push(`Output tokens: ${fmt(m.outputTokens)}`);
14149
+ lines.push(`Input, uncached: ${fmt(m.inputTokens)}`);
14150
+ lines.push(`Input, cache-write: ${fmt(m.cacheCreationTokens)}`);
14151
+ lines.push(`Input, cache-read: ${fmt(m.cacheReadTokens)}`);
14152
+ const totalInput = m.inputTokens + m.cacheCreationTokens + m.cacheReadTokens;
14153
+ lines.push(`Cache-read share of input: ${pct(m.cacheReadTokens, totalInput)}`);
14154
+ lines.push("");
14155
+ lines.push("## Estimated content attribution (chars/3 heuristic; NOT billed units)");
14156
+ const e = s.estimated;
14157
+ const rows = [
14158
+ ["tool results", e.toolResults],
14159
+ ["assistant text", e.assistantText],
14160
+ ["assistant thinking", e.assistantThinking],
14161
+ ["tool call inputs", e.toolUseInputs],
14162
+ ["attachments (harness)", e.attachments],
14163
+ ["user turns", e.userTurns],
14164
+ ["meta user lines", e.harnessMeta],
14165
+ ["system lines", e.system],
14166
+ ["local bookkeeping (never sent)", e.otherLocal]
14167
+ ];
14168
+ const modelVisibleBytes = rows.slice(0, 8).reduce((acc, [, c]) => acc + c.bytes, 0);
14169
+ for (const [label, cat] of rows.sort((a, b) => b[1].bytes - a[1].bytes)) {
14170
+ lines.push(`${label.padEnd(31)} count ${fmt(cat.count).padStart(11)} bytes ${fmt(cat.bytes).padStart(15)} est-tokens ${fmt(cat.estTokens).padStart(13)} ${label === "local bookkeeping (never sent)" ? "(excluded from share)" : pct(cat.bytes, modelVisibleBytes)}`);
14171
+ }
14172
+ lines.push("");
14173
+ lines.push("## Tool results by tool (estimated content size; calls = tool_use invocations)");
14174
+ for (const t of s.tools.slice(0, 25)) {
14175
+ lines.push(`${t.name.padEnd(42)} calls ${fmt(t.calls).padStart(9)} bytes ${fmt(t.resultBytes).padStart(15)} est-tokens ${fmt(t.resultEstTokens).padStart(12)}`);
14176
+ }
14177
+ if (s.tools.length > 25) lines.push(`(${s.tools.length - 25} smaller tools omitted from this table; --json has all)`);
14178
+ lines.push("");
14179
+ lines.push("## Attachment kinds by modeled billed cost (model-visible fields only; NOT billed units)");
14180
+ lines.push("Model: est-tokens x 1.25 cache-write + reread-tokens x 0.1 cache-read; an injection stays in context until the next compact boundary on its lane, or end of transcript.");
14181
+ for (const a of s.attachmentKinds.slice(0, 15)) {
14182
+ lines.push(`${a.kind.padEnd(28)} inj ${fmt(a.injections).padStart(9)} visible-bytes ${fmt(a.visibleBytes).padStart(13)} est-tok ${fmt(a.estTokens).padStart(12)} reread-tok ${fmt(a.rereadTokens).padStart(14)} billed-equiv ${fmt(a.billedEquivTokens).padStart(12)} identical-reinject ${fmt(a.repeatedIdentical).padStart(8)}`);
14183
+ }
14184
+ if (s.attachmentKinds.length > 15) lines.push(`(${s.attachmentKinds.length - 15} smaller kinds omitted from this table; --json has all)`);
14185
+ lines.push("");
14186
+ lines.push("## Hook stdout channel (hook_success attachments; context-bytes is the model-visible share)");
14187
+ for (const h of s.hookOutputs) {
14188
+ lines.push(`${h.origin.padEnd(11)} ${h.event.padEnd(18)} fires ${fmt(h.fires).padStart(9)} stdout-bytes ${fmt(h.stdoutBytes).padStart(13)} context-bytes ${fmt(h.contextBytes).padStart(13)}`);
14189
+ }
14190
+ lines.push("");
14191
+ lines.push("## Measured billed tokens by session position (deciles of each session's API calls)");
14192
+ for (const d of s.positionDeciles) {
14193
+ lines.push(`decile ${d.decile.toString().padStart(2)} calls ${fmt(d.apiCalls).padStart(9)} input ${fmt(d.inputTokens).padStart(15)} cache-read ${fmt(d.cacheReadTokens).padStart(15)} output ${fmt(d.outputTokens).padStart(11)}`);
14194
+ }
14195
+ lines.push("");
14196
+ lines.push("## Subagent lanes (spawn-prefix carriage; same residency model as the attachment census; NOT billed units)");
14197
+ const sl = s.sidechainLanes;
14198
+ lines.push(`Lane files: ${fmt(sl.laneFiles)} (${fmt(sl.lanesWithUsage)} with usage)`);
14199
+ lines.push(`First-call prefix tokens: mean ${fmt(sl.meanFirstCallPrefixTokens)}, median ${fmt(sl.medianFirstCallPrefixTokens)}, p90 ${fmt(sl.p90FirstCallPrefixTokens)}`);
14200
+ lines.push(`Calls per lane: mean ${fmt(sl.meanCallsPerLane)}; task-brief bytes: mean ${fmt(sl.meanBriefBytes)}`);
14201
+ lines.push(`Prefix billed-equiv tokens: ${fmt(sl.prefixBilledEquivTokens)} (write x 1.25, then x 0.1 per later call in the lane)`);
14202
+ for (const t of s.laneAgentTypes.slice(0, 12)) {
14203
+ lines.push(` type ${t.agentType.padEnd(24)} lanes ${fmt(t.lanes).padStart(7)} (${fmt(t.lanesWithUsage)} with usage) prefix mean ${fmt(t.meanFirstCallPrefixTokens).padStart(9)}, median ${fmt(t.medianFirstCallPrefixTokens).padStart(9)}`);
14204
+ }
14205
+ if (s.laneAgentTypes.length > 12) lines.push(` (${s.laneAgentTypes.length - 12} smaller agent types omitted from this table; --json has all)`);
14206
+ lines.push("");
14207
+ lines.push("## Read interception (token-goat divert markers inside Read tool results)");
14208
+ const ri = s.readInterception;
14209
+ lines.push(`Read results: ${fmt(ri.readResults)}; diverted by marker: ${fmt(ri.divertedByMarker)} (${fmt(ri.divertedBytes)} bytes); full serves >=10 KiB: ${fmt(ri.fullServesOver10k)} (${fmt(ri.fullServeBytesOver10k)} bytes)`);
14210
+ lines.push(`Full-serve split (same transcript file; a session's lanes are separate files, so repeats UNDER-count): first read ${fmt(ri.fullServesFirstRead)}, repeat ${fmt(ri.fullServesRepeat)} (${fmt(ri.repeatBytes)} bytes), path unknown ${fmt(ri.fullServesPathUnknown)}`);
14211
+ lines.push(`Repeats: with offset/limit ${fmt(ri.repeatWithRange)} (deliberate paging), whole-file ${fmt(ri.repeatFullNoRange)} (divert-miss candidates), in sessions with a token-goat hook fire ${fmt(ri.repeatInHookedSessions)} (whole-file among them: ${fmt(ri.repeatFullNoRangeInHookedSessions)}, of which post-compaction and so correct by design: ${fmt(ri.repeatFullNoRangeHookedAfterCompaction)})`);
14212
+ lines.push("");
14213
+ lines.push("## Bash filter fire-rate (token-goat in-band markers inside Bash tool results)");
14214
+ const bi = s.bashInterception;
14215
+ lines.push(`Bash results: ${fmt(bi.bashResults)}; marked by a filter: ${fmt(bi.markedByFilter)} (${fmt(bi.markedBytes)} bytes); small unmarked <${fmt(BASH_SMALL_RESULT_MAX_BYTES)} B: ${fmt(bi.smallUntouched)} (${fmt(bi.smallUntouchedBytes)} bytes)`);
14216
+ lines.push(`Untouched >=${fmt(BASH_SMALL_RESULT_MAX_BYTES)} B: ${fmt(bi.untouched)} (${fmt(bi.untouchedBytes)} bytes, est-tokens ${fmt(bi.untouchedEstTokens)}, billed-equiv ${fmt(bi.untouchedBilledEquivTokens)})`);
14217
+ lines.push("A filter that matched but fell under the 100-byte net-savings floor leaves no transcript trace and counts as untouched here.");
14218
+ for (const h of bi.untouchedHeads.slice(0, 15)) {
14219
+ lines.push(` ${h.head.padEnd(28)} results ${fmt(h.results).padStart(9)} bytes ${fmt(h.bytes).padStart(15)}`);
14220
+ }
14221
+ if (bi.untouchedHeads.length > 15) lines.push(` (${bi.untouchedHeads.length - 15} smaller command heads omitted from this table; --json has all)`);
14222
+ lines.push("");
14223
+ lines.push("## Mid-trim omission markers inside tool results");
14224
+ lines.push(`fires: ${fmt(s.omissionMarkers.fires)}, lines discarded: ${fmt(s.omissionMarkers.linesOmitted)}`);
14225
+ return lines.join("\n");
14226
+ }
14227
+
14228
+ // src/cli_mcp_audit.ts
14229
+ import * as fs27 from "node:fs";
14230
+ import * as os13 from "node:os";
14231
+ import * as path27 from "node:path";
13510
14232
  function readMcpJsonFile(configPath2) {
13511
14233
  try {
13512
- if (!fs26.existsSync(configPath2)) return null;
13513
- const content = fs26.readFileSync(configPath2, "utf-8");
14234
+ if (!fs27.existsSync(configPath2)) return null;
14235
+ const content = fs27.readFileSync(configPath2, "utf-8");
13514
14236
  const parsed = JSON.parse(content);
13515
14237
  const servers = parsed && typeof parsed === "object" ? parsed.mcpServers ?? parsed : null;
13516
14238
  return servers && typeof servers === "object" ? servers : null;
@@ -13526,8 +14248,8 @@ function driveLetterCaseVariants(p) {
13526
14248
  }
13527
14249
  function readClaudeJsonConfig(claudeJsonPath, projectRoot) {
13528
14250
  try {
13529
- if (!fs26.existsSync(claudeJsonPath)) return null;
13530
- const parsed = JSON.parse(fs26.readFileSync(claudeJsonPath, "utf-8"));
14251
+ if (!fs27.existsSync(claudeJsonPath)) return null;
14252
+ const parsed = JSON.parse(fs27.readFileSync(claudeJsonPath, "utf-8"));
13531
14253
  const projects = parsed && typeof parsed === "object" ? parsed.projects : null;
13532
14254
  if (!projects || typeof projects !== "object") return null;
13533
14255
  const slashForms = [projectRoot, projectRoot.replace(/\\/g, "/"), projectRoot.replace(/\//g, "\\")];
@@ -13545,8 +14267,8 @@ function readClaudeJsonConfig(claudeJsonPath, projectRoot) {
13545
14267
  }
13546
14268
  }
13547
14269
  function discoverMcpConfig(projectRoot, home) {
13548
- const mcpJsonPath = path26.join(projectRoot, ".mcp.json");
13549
- const claudeJsonPath = path26.join(home, ".claude.json");
14270
+ const mcpJsonPath = path27.join(projectRoot, ".mcp.json");
14271
+ const claudeJsonPath = path27.join(home, ".claude.json");
13550
14272
  const sourcesChecked = [mcpJsonPath, claudeJsonPath];
13551
14273
  const fromMcpJson = readMcpJsonFile(mcpJsonPath);
13552
14274
  if (fromMcpJson !== null) return { servers: fromMcpJson, sourcePath: mcpJsonPath, sourcesChecked };
@@ -13575,7 +14297,7 @@ function analyzeMcpCache() {
13575
14297
  }
13576
14298
  return serverMetrics;
13577
14299
  }
13578
- function buildMcpAuditReport(projectRoot, home = os12.homedir()) {
14300
+ function buildMcpAuditReport(projectRoot, home = os13.homedir()) {
13579
14301
  const discovery = discoverMcpConfig(projectRoot, home);
13580
14302
  const config = discovery.servers;
13581
14303
  const cacheMetrics = analyzeMcpCache();
@@ -13728,9 +14450,9 @@ function printSummary(rows) {
13728
14450
  };
13729
14451
  w(pad("category", 22) + pad("emitted", 9) + pad("acted-on", 10) + pad("efficacy", 10) + pad("suppressed", 17) + pad("manual+", 9) + pad("manual-", 9) + "spent\n");
13730
14452
  for (const row of rows) {
13731
- const pct = row.efficacyPct === null ? "n/a" : `${row.efficacyPct}%`;
14453
+ const pct2 = row.efficacyPct === null ? "n/a" : `${row.efficacyPct}%`;
13732
14454
  w(
13733
- pad(row.category, 22) + pad(String(row.emitted), 9) + pad(String(row.actedOn), 10) + pad(pct, 10) + pad(suppressedCell(row), 17) + pad(String(row.manualEffective), 9) + pad(String(row.manualIneffective), 9) + formatSpentCell(row) + "\n"
14455
+ pad(row.category, 22) + pad(String(row.emitted), 9) + pad(String(row.actedOn), 10) + pad(pct2, 10) + pad(suppressedCell(row), 17) + pad(String(row.manualEffective), 9) + pad(String(row.manualIneffective), 9) + formatSpentCell(row) + "\n"
13734
14456
  );
13735
14457
  }
13736
14458
  }
@@ -13875,10 +14597,10 @@ function readBoundedText(text, file) {
13875
14597
  if (text === void 0 && file === void 0) throw new CliError("provide text or --file");
13876
14598
  let value;
13877
14599
  if (file !== void 0) {
13878
- if (fs27.statSync(file).size > CONTENT_MAX_INPUT_CHARS) {
14600
+ if (fs28.statSync(file).size > CONTENT_MAX_INPUT_CHARS) {
13879
14601
  throw new CliError(`file exceeds the ${CONTENT_MAX_INPUT_CHARS}-byte safety limit`);
13880
14602
  }
13881
- value = fs27.readFileSync(file, "utf8");
14603
+ value = fs28.readFileSync(file, "utf8");
13882
14604
  } else {
13883
14605
  if (text === void 0) throw new CliError("provide text or --file");
13884
14606
  value = text;
@@ -14015,10 +14737,10 @@ async function cmdIndex(pathArg, opts = {}) {
14015
14737
  continue;
14016
14738
  }
14017
14739
  const sha = fingerprintFile(key);
14018
- if (sha === null && !fs27.existsSync(key)) continue;
14740
+ if (sha === null && !fs28.existsSync(key)) continue;
14019
14741
  const entry = sha !== null ? getFileEntry(key, dbPath) : null;
14020
14742
  const spellingStale = entry !== null && indexedPathSpellingIsStale(entry.filePath, key);
14021
- const parseUnchanged = !force && !spellingStale && sha !== null && entry?.sha === sha;
14743
+ const parseUnchanged = !force && !spellingStale && sha !== null && entry?.sha === sha && entry.parserSha === PARSER_FINGERPRINT;
14022
14744
  const embeddingsEnabled = loadConfig().indexing?.embeddings_enabled ?? true;
14023
14745
  const depsAvailable = embeddingsEnabled && embeddingsDepsAvailable(getDb(dbPath));
14024
14746
  const embedUnchanged = !force && parseUnchanged && sha !== null && isEmbedFresh(entry?.embedSha, sha, embeddingsEnabled, depsAvailable);
@@ -14041,7 +14763,7 @@ async function cmdIndex(pathArg, opts = {}) {
14041
14763
  }
14042
14764
  continue;
14043
14765
  }
14044
- if (!fs27.existsSync(key)) continue;
14766
+ if (!fs28.existsSync(key)) continue;
14045
14767
  }
14046
14768
  if (!embedUnchanged) {
14047
14769
  paintProgress("embedding");
@@ -14074,7 +14796,7 @@ function cmdMap(opts) {
14074
14796
  out(text);
14075
14797
  }
14076
14798
  const bytesSaved = mapLookupBytesSaved(map, text);
14077
- recordStat("map_lookup", bytesSaved, Math.round(bytesSaved / 4));
14799
+ recordStat("map_lookup", bytesSaved, savedTokensFromBytes(bytesSaved));
14078
14800
  }
14079
14801
  function cmdBridgesStatus(opts) {
14080
14802
  if (opts.json === true) {
@@ -14101,7 +14823,7 @@ async function cmdMcpServe() {
14101
14823
  let StdioServerTransport;
14102
14824
  try {
14103
14825
  ;
14104
- ({ createMcpServer } = await import("./token-goat-chunk-C5IL6MJH.mjs"));
14826
+ ({ createMcpServer } = await import("./token-goat-chunk-LZOAPGWR.mjs"));
14105
14827
  ({ StdioServerTransport } = await import("./token-goat-chunk-324QOJYZ.mjs"));
14106
14828
  } catch (err2) {
14107
14829
  process.stderr.write(
@@ -14114,8 +14836,8 @@ async function cmdMcpServe() {
14114
14836
  const server = await createMcpServer();
14115
14837
  const transport = new StdioServerTransport();
14116
14838
  await server.connect(transport);
14117
- await new Promise((resolve12) => {
14118
- server.onclose = resolve12;
14839
+ await new Promise((resolve13) => {
14840
+ server.onclose = resolve13;
14119
14841
  });
14120
14842
  }
14121
14843
  function printBridgeVerificationNotice(harness) {
@@ -14126,7 +14848,7 @@ async function cmdHook(event, opts) {
14126
14848
  if (typeof opts.harness === "string" && opts.harness.length > 0) {
14127
14849
  process.env[ENV_KEYS.HARNESS_OVERRIDE] = opts.harness;
14128
14850
  }
14129
- const { relay } = await import("./token-goat-chunk-DP3NNGGV.mjs");
14851
+ const { relay } = await import("./token-goat-chunk-L6YP6FKQ.mjs");
14130
14852
  await relay(event);
14131
14853
  }
14132
14854
  async function cmdInstall(opts) {
@@ -14239,16 +14961,16 @@ async function cmdInstall(opts) {
14239
14961
  );
14240
14962
  }
14241
14963
  try {
14242
- const skillDir = path27.join(homedir12(), ".claude", "skills");
14243
- if (fs27.existsSync(skillDir)) {
14244
- const entries = fs27.readdirSync(skillDir, { withFileTypes: true });
14964
+ const skillDir = path28.join(homedir13(), ".claude", "skills");
14965
+ if (fs28.existsSync(skillDir)) {
14966
+ const entries = fs28.readdirSync(skillDir, { withFileTypes: true });
14245
14967
  const skillNames = [];
14246
14968
  const sessionId = getSessionId();
14247
14969
  for (const entry of entries) {
14248
14970
  if (!entry.isDirectory()) continue;
14249
- const skillFile = path27.join(skillDir, entry.name, "SKILL.md");
14250
- if (fs27.existsSync(skillFile)) {
14251
- const body = fs27.readFileSync(skillFile, "utf-8");
14971
+ const skillFile = path28.join(skillDir, entry.name, "SKILL.md");
14972
+ if (fs28.existsSync(skillFile)) {
14973
+ const body = fs28.readFileSync(skillFile, "utf-8");
14252
14974
  const compact = extractCompactFromMarker(body);
14253
14975
  if (compact === null) continue;
14254
14976
  const sourceSha = contentHash(body);
@@ -14258,10 +14980,10 @@ async function cmdInstall(opts) {
14258
14980
  }
14259
14981
  if (skillNames.length > 0) {
14260
14982
  const dir = skillOutputsDir();
14261
- await fs27.promises.mkdir(dir, { recursive: true });
14262
- const pregenPath = path27.join(dir, "pregen.json");
14983
+ await fs28.promises.mkdir(dir, { recursive: true });
14984
+ const pregenPath = path28.join(dir, "pregen.json");
14263
14985
  const pregenData = { ts: Date.now(), names: skillNames };
14264
- await fs27.promises.writeFile(pregenPath, JSON.stringify(pregenData, null, 2));
14986
+ await fs28.promises.writeFile(pregenPath, JSON.stringify(pregenData, null, 2));
14265
14987
  out(`Pre-generated ${skillNames.length} skill compacts.`);
14266
14988
  }
14267
14989
  }
@@ -14439,6 +15161,15 @@ function cmdWaste(opts = {}) {
14439
15161
  ...opts.copilot === true ? { copilot: true } : {}
14440
15162
  });
14441
15163
  }
15164
+ async function cmdSessionAudit(opts = {}) {
15165
+ let summary;
15166
+ try {
15167
+ summary = await auditSessionCorpus({ ...opts.dir !== void 0 ? { dir: opts.dir } : {} });
15168
+ } catch (err2) {
15169
+ throw new CliError(err2 instanceof Error ? err2.message : String(err2));
15170
+ }
15171
+ out(opts.json === true ? JSON.stringify(summary) : formatSessionAudit(summary));
15172
+ }
14442
15173
  async function cmdSessionOutline(sessionIdOrPath, opts = {}) {
14443
15174
  const transcriptPath = resolveSessionTranscript(sessionIdOrPath, opts.project !== void 0 ? { project: opts.project } : {});
14444
15175
  if (transcriptPath === null) {
@@ -14452,11 +15183,11 @@ ${formatSessionOutline(turns)}`;
14452
15183
  out(text);
14453
15184
  const fullSourceBytes = sessionTranscriptSize(transcriptPath);
14454
15185
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(text, "utf8"));
14455
- recordStat("session_outline", bytesSaved, Math.round(bytesSaved / 4));
15186
+ recordStat("session_outline", bytesSaved, savedTokensFromBytes(bytesSaved));
14456
15187
  }
14457
15188
  function sessionTranscriptSize(transcriptPath) {
14458
15189
  try {
14459
- return fs27.statSync(transcriptPath).size;
15190
+ return fs28.statSync(transcriptPath).size;
14460
15191
  } catch {
14461
15192
  return 0;
14462
15193
  }
@@ -14474,7 +15205,7 @@ async function cmdSessionSlice(sessionIdOrPath, opts) {
14474
15205
  out(text);
14475
15206
  const fullSourceBytes = sessionTranscriptSize(transcriptPath);
14476
15207
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(text, "utf8"));
14477
- recordStat("session_slice", bytesSaved, Math.round(bytesSaved / 4));
15208
+ recordStat("session_slice", bytesSaved, savedTokensFromBytes(bytesSaved));
14478
15209
  }
14479
15210
  function cmdMcpAudit(opts = {}) {
14480
15211
  return runMcpAuditCommand({
@@ -14593,7 +15324,7 @@ function fenceFileTextIfMatched(text) {
14593
15324
  }
14594
15325
  function fileSizeOrZero(filePath) {
14595
15326
  try {
14596
- return fs27.statSync(filePath).size;
15327
+ return fs28.statSync(filePath).size;
14597
15328
  } catch {
14598
15329
  return 0;
14599
15330
  }
@@ -14608,11 +15339,11 @@ function cmdBashOutput(id, opts) {
14608
15339
  }
14609
15340
  let content;
14610
15341
  try {
14611
- const st = fs27.statSync(opts.file);
15342
+ const st = fs28.statSync(opts.file);
14612
15343
  if (st.isFIFO() || st.isSocket()) {
14613
15344
  throw new CliError(`--file '${opts.file}' is a special file (FIFO or socket) \u2014 only regular files are supported`);
14614
15345
  }
14615
- content = redactIfDotenv(opts.file, decodeSource(fs27.readFileSync(opts.file)));
15346
+ content = redactIfDotenv(opts.file, decodeSource(fs28.readFileSync(opts.file)));
14616
15347
  } catch (e) {
14617
15348
  if (e instanceof CliError) throw e;
14618
15349
  throw new CliError(`cannot read file: ${opts.file}`);
@@ -14657,7 +15388,7 @@ async function cmdPdfExtract(file, opts) {
14657
15388
  const printed = _applyFiltersAndPrint(text, opts, true, UNTRUSTED_FILE_TAG);
14658
15389
  const fullSourceBytes = fileSizeOrZero(file);
14659
15390
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(printed, "utf8"));
14660
- recordStat("pdf_extract", bytesSaved, Math.round(bytesSaved / 4));
15391
+ recordStat("pdf_extract", bytesSaved, savedTokensFromBytes(bytesSaved));
14661
15392
  }
14662
15393
  async function cmdPdfLocate(file, pattern, opts) {
14663
15394
  const locateOpts = {
@@ -14685,7 +15416,7 @@ ${countNoun(fencedMatches.length, "match", "matches")} across ${countNoun(pages.
14685
15416
  }
14686
15417
  const fullSourceBytes = fileSizeOrZero(file);
14687
15418
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(printed, "utf8"));
14688
- recordStat("pdf_locate", bytesSaved, Math.round(bytesSaved / 4));
15419
+ recordStat("pdf_locate", bytesSaved, savedTokensFromBytes(bytesSaved));
14689
15420
  }
14690
15421
  async function cmdPdfOutline(file, opts) {
14691
15422
  const entries = await runPdfOutline(file);
@@ -14702,7 +15433,7 @@ async function cmdPdfOutline(file, opts) {
14702
15433
  out(text);
14703
15434
  const fullSourceBytes = fileSizeOrZero(file);
14704
15435
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(text, "utf8"));
14705
- recordStat("pdf_outline", bytesSaved, Math.round(bytesSaved / 4));
15436
+ recordStat("pdf_outline", bytesSaved, savedTokensFromBytes(bytesSaved));
14706
15437
  }
14707
15438
  async function cmdPdfMeta(file, opts = {}) {
14708
15439
  const meta = await runPdfMeta(file);
@@ -14716,7 +15447,7 @@ async function cmdPdfMeta(file, opts = {}) {
14716
15447
  out(text);
14717
15448
  const fullSourceBytes = fileSizeOrZero(file);
14718
15449
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(text, "utf8"));
14719
- recordStat("pdf_meta", bytesSaved, Math.round(bytesSaved / 4));
15450
+ recordStat("pdf_meta", bytesSaved, savedTokensFromBytes(bytesSaved));
14720
15451
  }
14721
15452
  async function cmdImageMeta(file, opts = {}) {
14722
15453
  const meta = await runImageMeta(file);
@@ -14738,7 +15469,17 @@ ${msg}`;
14738
15469
  out(text);
14739
15470
  const fullSourceBytes = fileSizeOrZero(file);
14740
15471
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(text, "utf8"));
14741
- recordStat("image_meta", bytesSaved, Math.round(bytesSaved / 4));
15472
+ recordStat("image_meta", bytesSaved, savedTokensFromBytes(bytesSaved));
15473
+ }
15474
+ function fenceOcrText(text) {
15475
+ try {
15476
+ if (loadConfig().injection.enabled) {
15477
+ const matches2 = scanForInjectionPatterns(text);
15478
+ if (matches2.length > 0) recordStat("injection_detected", 0, 0, void 0, matches2.join(","));
15479
+ }
15480
+ } catch {
15481
+ }
15482
+ return fenceUntrustedOcrText(text);
14742
15483
  }
14743
15484
  async function cmdImageText(file, opts = {}) {
14744
15485
  const result = await runImageText(file);
@@ -14751,7 +15492,7 @@ async function cmdImageText(file, opts = {}) {
14751
15492
  let text;
14752
15493
  if (opts.json === true) {
14753
15494
  text = JSON.stringify(
14754
- result.text === null ? result : { ...result, text: fenceFileTextIfMatched(result.text) },
15495
+ result.text === null ? result : { ...result, text: fenceOcrText(result.text) },
14755
15496
  null,
14756
15497
  2
14757
15498
  );
@@ -14759,13 +15500,13 @@ async function cmdImageText(file, opts = {}) {
14759
15500
  const lines = [`Confidence: ${Math.round(result.confidence)}%`, `Characters: ${result.chars}`];
14760
15501
  text = result.textHeavy && result.text !== null ? `${lines.join("\n")}
14761
15502
 
14762
- ${fenceFileTextIfMatched(result.text)}` : `${lines.join("\n")}
15503
+ ${fenceOcrText(result.text)}` : `${lines.join("\n")}
14763
15504
  (below usefulness threshold; text likely noise, not shown)`;
14764
15505
  }
14765
15506
  out(text);
14766
15507
  const fullSourceBytes = fileSizeOrZero(file);
14767
15508
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(text, "utf8"));
14768
- recordStat("image_text", bytesSaved, Math.round(bytesSaved / 4));
15509
+ recordStat("image_text", bytesSaved, savedTokensFromBytes(bytesSaved));
14769
15510
  }
14770
15511
  function cmdVideoChapters(file) {
14771
15512
  const { chapters, subtitleStreams } = extractVideoChapters(file);
@@ -14791,7 +15532,7 @@ function cmdVideoChapters(file) {
14791
15532
  out(text);
14792
15533
  const fullSourceBytes = fileSizeOrZero(file);
14793
15534
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(text, "utf8"));
14794
- recordStat("video_chapters", bytesSaved, Math.round(bytesSaved / 4));
15535
+ recordStat("video_chapters", bytesSaved, savedTokensFromBytes(bytesSaved));
14795
15536
  }
14796
15537
  function formatVideoTimestamp(totalSeconds) {
14797
15538
  const hours = Math.floor(totalSeconds / 3600);
@@ -14818,7 +15559,7 @@ function cmdSharepointResolve(url) {
14818
15559
  function recordXlsxStat(kind, file, emitted) {
14819
15560
  const fullSourceBytes = fileSizeOrZero(file);
14820
15561
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(emitted, "utf8"));
14821
- recordStat(kind, bytesSaved, Math.round(bytesSaved / 4));
15562
+ recordStat(kind, bytesSaved, savedTokensFromBytes(bytesSaved));
14822
15563
  }
14823
15564
  async function cmdXlsxSheets(file, opts = {}) {
14824
15565
  const sheets = await listSheets(file);
@@ -14847,14 +15588,14 @@ async function cmdXlsxQuery(file, opts) {
14847
15588
  ...wheres !== void 0 ? { wheres } : {},
14848
15589
  ...opts.head !== void 0 ? { head: requireNonNegativeInt("--head", opts.head) } : {}
14849
15590
  });
14850
- const text = fenceFileTextIfMatched(formatCsvTable(result));
15591
+ const text = fenceFileTextIfMatched(formatCsvTable(result, (opts.where ?? []).map((w) => `--where ${w}`)));
14851
15592
  out(text);
14852
15593
  recordXlsxStat("xlsx_query", file, text);
14853
15594
  }
14854
15595
  function recordDocStat(kind, file, emitted) {
14855
15596
  const fullSourceBytes = fileSizeOrZero(file);
14856
15597
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(emitted, "utf8"));
14857
- recordStat(kind, bytesSaved, Math.round(bytesSaved / 4));
15598
+ recordStat(kind, bytesSaved, savedTokensFromBytes(bytesSaved));
14858
15599
  }
14859
15600
  async function cmdPptxOutline(file, opts) {
14860
15601
  const slides = await pptxOutline(file);
@@ -15041,7 +15782,7 @@ function emitExtraFileArgsNote(command, first, extras, opts = {}) {
15041
15782
  }
15042
15783
  async function cmdCompress(opts) {
15043
15784
  try {
15044
- const bashRunner = await import("./token-goat-chunk-VAGPOZHO.mjs");
15785
+ const bashRunner = await import("./token-goat-chunk-NZFTWBGV.mjs");
15045
15786
  if (opts.compress === false) {
15046
15787
  process.exitCode = bashRunner.runRaw(opts.cmd, parseTimeout(opts.timeout, bashRunner.DEFAULT_TIMEOUT_SECONDS));
15047
15788
  return;
@@ -15067,9 +15808,12 @@ async function cmdSkillBody(name, opts) {
15067
15808
  if (filePath === null) {
15068
15809
  throw new CliError(`skill '${name}' not found`);
15069
15810
  }
15070
- const body = decodeSource(fs27.readFileSync(filePath));
15811
+ const body = decodeSource(fs28.readFileSync(filePath));
15071
15812
  if (opts.compact === true) {
15072
- out(extractCompactFromMarker(body) ?? body);
15813
+ const emitted = extractCompactFromMarker(body) ?? body;
15814
+ out(emitted);
15815
+ const bytesSaved = Buffer.byteLength(body, "utf8") - Buffer.byteLength(emitted, "utf8");
15816
+ if (bytesSaved > 0) recordStat("skill_body:compact", bytesSaved, savedTokensFromBytes(bytesSaved), void 0, name);
15073
15817
  } else {
15074
15818
  out(body);
15075
15819
  }
@@ -15081,12 +15825,26 @@ async function cmdSkillCompact(name, opts) {
15081
15825
  const skills = await listSkills(sessionId);
15082
15826
  let regenerated = 0;
15083
15827
  let skipped = 0;
15828
+ let noMarker = 0;
15829
+ let unresolvable = 0;
15084
15830
  for (const skill of skills) {
15085
15831
  const filePath = await getSkillFilePath(skill.name);
15086
- if (!filePath) continue;
15087
- const body2 = decodeSource(fs27.readFileSync(filePath));
15832
+ if (!filePath) {
15833
+ unresolvable++;
15834
+ continue;
15835
+ }
15836
+ let body2;
15837
+ try {
15838
+ body2 = decodeSource(fs28.readFileSync(filePath));
15839
+ } catch {
15840
+ unresolvable++;
15841
+ continue;
15842
+ }
15088
15843
  const compact2 = extractCompactFromMarker(body2);
15089
- if (compact2 === null) continue;
15844
+ if (compact2 === null) {
15845
+ noMarker++;
15846
+ continue;
15847
+ }
15090
15848
  const sourceSha2 = contentHash(body2);
15091
15849
  if (skill.compactStale === false) {
15092
15850
  skipped++;
@@ -15095,7 +15853,13 @@ async function cmdSkillCompact(name, opts) {
15095
15853
  regenerated++;
15096
15854
  }
15097
15855
  }
15098
- out(`Regenerated ${regenerated}, skipped ${skipped} (fresh), total ${skills.length}.`);
15856
+ const parts = [`Regenerated ${regenerated}, skipped ${skipped} (fresh)`];
15857
+ if (noMarker > 0) parts.push(`no marker ${noMarker}`);
15858
+ if (unresolvable > 0) parts.push(`unresolvable ${unresolvable}`);
15859
+ out(`${parts.join(", ")}, total ${skills.length}.`);
15860
+ if (noMarker > 0) {
15861
+ out(`${noMarker} cached skill${noMarker === 1 ? " has" : "s have"} no COMPACT_END marker and cannot be compacted; run \`token-goat skill-size\` for per-skill marker recommendations.`);
15862
+ }
15099
15863
  return;
15100
15864
  }
15101
15865
  let body;
@@ -15105,19 +15869,19 @@ async function cmdSkillCompact(name, opts) {
15105
15869
  if (!opts.path.trim()) {
15106
15870
  throw new CliError("--path cannot be empty");
15107
15871
  }
15108
- if (!fs27.existsSync(opts.path)) {
15872
+ if (!fs28.existsSync(opts.path)) {
15109
15873
  throw new CliError(`skill file not found: ${opts.path}`);
15110
15874
  }
15111
15875
  try {
15112
- body = fs27.readFileSync(opts.path, "utf-8");
15876
+ body = fs28.readFileSync(opts.path, "utf-8");
15113
15877
  } catch (e) {
15114
15878
  if (e.code === "ENOENT") {
15115
15879
  throw new CliError(`skill file not found: ${opts.path}`);
15116
15880
  }
15117
15881
  throw new CliError(`failed to read skill file '${opts.path}': ${extractErrorMessage(e)}`);
15118
15882
  }
15119
- cacheName = name ?? path27.basename(path27.dirname(path27.resolve(opts.path)));
15120
- sourcePath = path27.resolve(opts.path);
15883
+ cacheName = name ?? path28.basename(path28.dirname(path28.resolve(opts.path)));
15884
+ sourcePath = path28.resolve(opts.path);
15121
15885
  } else {
15122
15886
  if (name === void 0 || !name.trim()) {
15123
15887
  throw new CliError("skill-compact requires a <name> or --path <file>");
@@ -15126,7 +15890,7 @@ async function cmdSkillCompact(name, opts) {
15126
15890
  if (filePath === null) {
15127
15891
  throw new CliError(`skill '${name}' not found`);
15128
15892
  }
15129
- body = fs27.readFileSync(filePath, "utf-8");
15893
+ body = fs28.readFileSync(filePath, "utf-8");
15130
15894
  cacheName = name;
15131
15895
  sourcePath = filePath;
15132
15896
  }
@@ -15258,8 +16022,8 @@ async function cmdSkillDiff(name) {
15258
16022
  }
15259
16023
  const newer = versions[0];
15260
16024
  const older = versions[1];
15261
- const newerBody = await fs27.promises.readFile(path27.resolve(dir, `${newer.outputId}.txt`), "utf-8").catch(() => null);
15262
- const olderBody = await fs27.promises.readFile(path27.resolve(dir, `${older.outputId}.txt`), "utf-8").catch(() => null);
16025
+ const newerBody = await fs28.promises.readFile(path28.resolve(dir, `${newer.outputId}.txt`), "utf-8").catch(() => null);
16026
+ const olderBody = await fs28.promises.readFile(path28.resolve(dir, `${older.outputId}.txt`), "utf-8").catch(() => null);
15263
16027
  if (newerBody === null || olderBody === null) {
15264
16028
  out(`a cached version of '${name}' was evicted while diffing -- try again`);
15265
16029
  return;
@@ -15288,7 +16052,7 @@ async function cmdSkillSection(nameHeading, headingArg) {
15288
16052
  if (!filePath) {
15289
16053
  throw new CliError(`skill '${skillName}' not found`);
15290
16054
  }
15291
- const body = decodeSource(fs27.readFileSync(filePath));
16055
+ const body = decodeSource(fs28.readFileSync(filePath));
15292
16056
  const extracted = extractNamedSection(body, heading);
15293
16057
  if (!extracted) {
15294
16058
  const messages = [`Section '${heading}' not found in skill '${skillName}'`];
@@ -15303,7 +16067,7 @@ async function cmdSkillSection(nameHeading, headingArg) {
15303
16067
  }
15304
16068
  function atomicWriteBuffer(dest, data) {
15305
16069
  try {
15306
- if (fs27.statSync(dest).isDirectory()) {
16070
+ if (fs28.statSync(dest).isDirectory()) {
15307
16071
  const e = Object.assign(new Error(`EISDIR: illegal operation on a directory, open '${dest}'`), { code: "EISDIR", path: dest });
15308
16072
  throw e;
15309
16073
  }
@@ -15311,23 +16075,23 @@ function atomicWriteBuffer(dest, data) {
15311
16075
  if (e.code !== "ENOENT") throw e;
15312
16076
  }
15313
16077
  const rnd = randomBytes(4).toString("hex");
15314
- const tmp = path27.join(path27.dirname(path27.resolve(dest)), `.tmp.${process.pid}.${rnd}`);
16078
+ const tmp = path28.join(path28.dirname(path28.resolve(dest)), `.tmp.${process.pid}.${rnd}`);
15315
16079
  try {
15316
- fs27.writeFileSync(tmp, data, { mode: 384 });
16080
+ fs28.writeFileSync(tmp, data, { mode: 384 });
15317
16081
  try {
15318
- const destMode = fs27.statSync(dest).mode;
15319
- fs27.chmodSync(tmp, destMode);
16082
+ const destMode = fs28.statSync(dest).mode;
16083
+ fs28.chmodSync(tmp, destMode);
15320
16084
  } catch (e) {
15321
16085
  if (e.code !== "ENOENT") throw e;
15322
16086
  }
15323
16087
  withRetryOnLock(() => {
15324
16088
  try {
15325
- fs27.renameSync(tmp, dest);
16089
+ fs28.renameSync(tmp, dest);
15326
16090
  } catch (e) {
15327
16091
  if (e.code === "EXDEV") {
15328
- fs27.copyFileSync(tmp, dest);
16092
+ fs28.copyFileSync(tmp, dest);
15329
16093
  try {
15330
- fs27.unlinkSync(tmp);
16094
+ fs28.unlinkSync(tmp);
15331
16095
  } catch (ue) {
15332
16096
  process.stderr.write(`token-goat write-file: warning: could not remove temp file ${tmp}: ${ue.message}
15333
16097
  `);
@@ -15339,7 +16103,7 @@ function atomicWriteBuffer(dest, data) {
15339
16103
  });
15340
16104
  } catch (e) {
15341
16105
  try {
15342
- fs27.unlinkSync(tmp);
16106
+ fs28.unlinkSync(tmp);
15343
16107
  } catch {
15344
16108
  }
15345
16109
  throw e;
@@ -15349,9 +16113,9 @@ function mapFsError(e, src, dest, srcLabel = "source") {
15349
16113
  const fe = e;
15350
16114
  if (fe.code === "ENOENT") {
15351
16115
  const errPath = fe.path ?? "";
15352
- const isSource = src !== void 0 && path27.resolve(errPath) === path27.resolve(src);
16116
+ const isSource = src !== void 0 && path28.resolve(errPath) === path28.resolve(src);
15353
16117
  if (isSource) throw new CliError(`${/\bfile$/i.test(srcLabel) ? srcLabel : `${srcLabel} file`} not found: ${src}`);
15354
- const destDir = dest ? path27.dirname(path27.resolve(dest)) : path27.dirname(path27.resolve(errPath || "."));
16118
+ const destDir = dest ? path28.dirname(path28.resolve(dest)) : path28.dirname(path28.resolve(errPath || "."));
15355
16119
  throw new CliError(`destination directory does not exist: ${destDir}`);
15356
16120
  }
15357
16121
  if (fe.code === "ENOTDIR") {
@@ -15362,7 +16126,7 @@ function mapFsError(e, src, dest, srcLabel = "source") {
15362
16126
  }
15363
16127
  if (fe.code === "EISDIR") {
15364
16128
  const errPath = fe.path ?? "";
15365
- const isSource = src !== void 0 && (errPath === "" || path27.resolve(errPath) === path27.resolve(src));
16129
+ const isSource = src !== void 0 && (errPath === "" || path28.resolve(errPath) === path28.resolve(src));
15366
16130
  if (isSource) throw new CliError(`source is a directory, not a file: ${src}`);
15367
16131
  throw new CliError(`destination is a directory, not a file: ${dest ?? (errPath || "(unknown)")}`);
15368
16132
  }
@@ -15431,7 +16195,7 @@ function validateWritablePath(dest, label) {
15431
16195
  throw new CliError(`${label} path contains a null byte`);
15432
16196
  }
15433
16197
  if (isWindows()) {
15434
- const base = path27.basename(dest);
16198
+ const base = path28.basename(dest);
15435
16199
  const stem = base.replace(/\.[^.]*$/, "").toUpperCase();
15436
16200
  if (WIN_RESERVED.has(stem)) {
15437
16201
  throw new CliError(`${label} '${base}' is a reserved Windows device name`);
@@ -15461,7 +16225,7 @@ function readFileBoundedRaw(filePath, label, allowStdIn = false) {
15461
16225
  throw new CliError(`${label} ${filePath} requires piped input; use ${altLabel} for interactive use`);
15462
16226
  }
15463
16227
  try {
15464
- const st = fs27.statSync(filePath);
16228
+ const st = fs28.statSync(filePath);
15465
16229
  if (st.isFIFO() || st.isSocket()) {
15466
16230
  throw new CliError(`${label} '${filePath}' is a special file (FIFO or socket) \u2014 only regular files are supported`);
15467
16231
  }
@@ -15469,7 +16233,7 @@ function readFileBoundedRaw(filePath, label, allowStdIn = false) {
15469
16233
  if (st.size > maxBytes) {
15470
16234
  throw new CliError(`${label} '${filePath}' exceeds size limit (${Math.round(st.size / 1024 / 1024)} MB); set TOKEN_GOAT_MAX_STDIN_MB to override`);
15471
16235
  }
15472
- return fs27.readFileSync(filePath);
16236
+ return fs28.readFileSync(filePath);
15473
16237
  } catch (e) {
15474
16238
  if (e instanceof CliError) throw e;
15475
16239
  mapFsError(e, filePath, void 0, label);
@@ -15513,7 +16277,7 @@ function cmdNoteAdd(file, opts) {
15513
16277
  throw new CliError("note content must be valid UTF-8 text");
15514
16278
  }
15515
16279
  const resolvedPath = resolveIndexPath(file);
15516
- if (!fs27.existsSync(resolvedPath)) {
16280
+ if (!fs28.existsSync(resolvedPath)) {
15517
16281
  throw new CliError(`File not found: '${resolvedPath}'`);
15518
16282
  }
15519
16283
  healStaleIndex(resolvedPath);
@@ -15572,7 +16336,7 @@ function cmdWriteFile(dest, opts) {
15572
16336
  throw new CliError(`TOKEN_GOAT_MAX_STDIN_MB must be a positive integer; got '${process.env["TOKEN_GOAT_MAX_STDIN_MB"] ?? ""}'`);
15573
16337
  }
15574
16338
  const maxBytes = maxMB * 1024 * 1024;
15575
- return new Promise((resolve12, reject) => {
16339
+ return new Promise((resolve13, reject) => {
15576
16340
  const chunks = [];
15577
16341
  let totalBytes = 0;
15578
16342
  let settled = false;
@@ -15596,7 +16360,7 @@ function cmdWriteFile(dest, opts) {
15596
16360
  try {
15597
16361
  atomicWriteBuffer(dest, Buffer.concat(chunks));
15598
16362
  enqueueDirtyPathSafe(dest);
15599
- resolve12();
16363
+ resolve13();
15600
16364
  } catch (e) {
15601
16365
  try {
15602
16366
  mapFsError(e, void 0, dest);
@@ -15727,14 +16491,14 @@ function writeReplacedBuffer(file, replacedBuf, preWriteStat) {
15727
16491
  const until = process.env["TOKEN_GOAT_TEST_REPLACE_DELAY_UNTIL"];
15728
16492
  const deadline = Date.now() + testDelayMs;
15729
16493
  if (until !== void 0 && until !== "") {
15730
- while (Date.now() < deadline && !fs27.existsSync(until)) sleepSync(5);
16494
+ while (Date.now() < deadline && !fs28.existsSync(until)) sleepSync(5);
15731
16495
  } else {
15732
16496
  sleepSync(testDelayMs);
15733
16497
  }
15734
16498
  }
15735
16499
  let preRenameStat;
15736
16500
  try {
15737
- preRenameStat = fs27.statSync(file);
16501
+ preRenameStat = fs28.statSync(file);
15738
16502
  } catch {
15739
16503
  }
15740
16504
  if (preRenameStat !== void 0 && (preRenameStat.mtimeMs !== preWriteStat.mtimeMs || preRenameStat.size !== preWriteStat.size)) {
@@ -15775,7 +16539,7 @@ function cmdReplace(file, opts) {
15775
16539
  const targetBuf = readFileBoundedRaw(file, "target file", true);
15776
16540
  let preWriteStat;
15777
16541
  try {
15778
- preWriteStat = fs27.statSync(file);
16542
+ preWriteStat = fs28.statSync(file);
15779
16543
  } catch {
15780
16544
  }
15781
16545
  const usingFrom = opts.oldFrom !== void 0 || opts.newFrom !== void 0;
@@ -15869,7 +16633,7 @@ function cmdInsertSection(file, opts) {
15869
16633
  }
15870
16634
  let preWriteStat;
15871
16635
  try {
15872
- preWriteStat = fs27.statSync(file);
16636
+ preWriteStat = fs28.statSync(file);
15873
16637
  } catch {
15874
16638
  }
15875
16639
  const result = readSection(file, opts.after);
@@ -15895,7 +16659,7 @@ function cmdInsertSection(file, opts) {
15895
16659
  }
15896
16660
  let rawBytes;
15897
16661
  try {
15898
- rawBytes = fs27.readFileSync(file);
16662
+ rawBytes = fs28.readFileSync(file);
15899
16663
  } catch (e) {
15900
16664
  mapFsError(e, void 0, file);
15901
16665
  }
@@ -15911,7 +16675,7 @@ function cmdInsertSection(file, opts) {
15911
16675
  if (preWriteStat !== void 0) {
15912
16676
  let preRenameStat;
15913
16677
  try {
15914
- preRenameStat = fs27.statSync(file);
16678
+ preRenameStat = fs28.statSync(file);
15915
16679
  } catch {
15916
16680
  }
15917
16681
  if (preRenameStat !== void 0 && (preRenameStat.mtimeMs !== preWriteStat.mtimeMs || preRenameStat.size !== preWriteStat.size)) {
@@ -15959,29 +16723,29 @@ ${content}`;
15959
16723
  }
15960
16724
  out(toEmit);
15961
16725
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(toEmit, "utf8"));
15962
- recordStat("gdrive_sections", bytesSaved, Math.round(bytesSaved / 4));
16726
+ recordStat("gdrive_sections", bytesSaved, savedTokensFromBytes(bytesSaved));
15963
16727
  }
15964
16728
  function expandGlobs(root, patterns, globFnOverride) {
15965
16729
  const out2 = [];
15966
- const globFn = globFnOverride ?? fs27["globSync"];
16730
+ const globFn = globFnOverride ?? fs28["globSync"];
15967
16731
  for (const p of patterns) {
15968
16732
  if (globFn !== void 0 && (p.includes("*") || p.includes("?") || p.includes("{"))) {
15969
16733
  try {
15970
16734
  const hits = globFn(p, { cwd: root });
15971
- for (const h of hits) out2.push(path27.isAbsolute(h) ? h : path27.join(root, h));
16735
+ for (const h of hits) out2.push(path28.isAbsolute(h) ? h : path28.join(root, h));
15972
16736
  continue;
15973
16737
  } catch {
15974
16738
  }
15975
16739
  }
15976
- out2.push(path27.isAbsolute(p) ? p : path27.join(root, p));
16740
+ out2.push(path28.isAbsolute(p) ? p : path28.join(root, p));
15977
16741
  }
15978
16742
  return out2;
15979
16743
  }
15980
16744
  function readIgnoreFile(root) {
15981
- const ignorePath = path27.join(root, ".tokengoatignore");
16745
+ const ignorePath = path28.join(root, ".tokengoatignore");
15982
16746
  let raw;
15983
16747
  try {
15984
- raw = fs27.readFileSync(ignorePath, "utf8");
16748
+ raw = fs28.readFileSync(ignorePath, "utf8");
15985
16749
  } catch {
15986
16750
  return void 0;
15987
16751
  }
@@ -16022,14 +16786,14 @@ function cmdPack(patterns, opts) {
16022
16786
  }
16023
16787
  let instruction;
16024
16788
  if (opts.instructionFile !== void 0) {
16025
- instruction = fs27.readFileSync(opts.instructionFile, "utf8");
16789
+ instruction = fs28.readFileSync(opts.instructionFile, "utf8");
16026
16790
  }
16027
16791
  const formatted = formatPack(result, style, {
16028
16792
  ...opts.lineNumbers === true ? { line_numbers: true } : {},
16029
16793
  ...instruction !== void 0 ? { instruction } : {}
16030
16794
  });
16031
16795
  if (opts.output !== void 0) {
16032
- fs27.writeFileSync(opts.output, formatted, "utf8");
16796
+ fs28.writeFileSync(opts.output, formatted, "utf8");
16033
16797
  } else {
16034
16798
  out(formatted);
16035
16799
  }
@@ -16047,17 +16811,17 @@ function cmdTokens(patterns, opts) {
16047
16811
  if (opts.tree === true) {
16048
16812
  const dirs = /* @__PURE__ */ new Map();
16049
16813
  for (const e of entries) {
16050
- const dir = path27.dirname(e.rel_path);
16814
+ const dir = path28.dirname(e.rel_path);
16051
16815
  if (!dirs.has(dir)) dirs.set(dir, []);
16052
16816
  dirs.get(dir).push(e);
16053
16817
  }
16054
16818
  const lines2 = [];
16055
16819
  for (const [dir, dirEntries] of dirs) {
16056
16820
  const dirTokens = dirEntries.reduce((s, e) => s + e.tokens, 0);
16057
- const pct = result.total_tokens > 0 ? Math.round(dirTokens / result.total_tokens * 100) : 0;
16058
- lines2.push(`${dir}/ (${dirTokens} tokens, ${pct}%)`);
16821
+ const pct2 = result.total_tokens > 0 ? Math.round(dirTokens / result.total_tokens * 100) : 0;
16822
+ lines2.push(`${dir}/ (${dirTokens} tokens, ${pct2}%)`);
16059
16823
  for (const e of dirEntries) {
16060
- lines2.push(` ${path27.basename(e.rel_path).padEnd(30)} ${String(e.tokens).padStart(8)} tokens`);
16824
+ lines2.push(` ${path28.basename(e.rel_path).padEnd(30)} ${String(e.tokens).padStart(8)} tokens`);
16061
16825
  }
16062
16826
  }
16063
16827
  out(lines2.join("\n"));
@@ -16088,7 +16852,7 @@ function cmdBudget(patterns, opts) {
16088
16852
  }
16089
16853
  }
16090
16854
  function cmdFailures(src, opts) {
16091
- const text = src !== void 0 ? fs27.readFileSync(src, "utf8") : fs27.readFileSync(0, "utf8");
16855
+ const text = src !== void 0 ? fs28.readFileSync(src, "utf8") : fs28.readFileSync(0, "utf8");
16092
16856
  const result = extractFailures(text, opts.runner !== void 0 ? { runner: opts.runner } : {});
16093
16857
  if (opts.delta !== true) {
16094
16858
  out(opts.json === true ? formatFailuresJson(result) : formatFailuresText(result));
@@ -16290,6 +17054,7 @@ function buildProgram() {
16290
17054
  program2.command("waste").description("session spend-ledger: token cost per tool/file from the current Claude Code session transcript, plus waste signals").option("--project <path>", "project root to analyze").option("--transcript <path>", "explicit transcript JSONL path (default: most-recently-modified transcript for this project)").option("--top <n>", "number of top expensive tool calls to show (default: 10)").option("--copilot", "analyze a Copilot CLI session event log instead, reporting Copilot's own token split").option("--json", "output JSON").action(guard(cmdWaste));
16291
17055
  program2.command("session-outline [session-id-or-path]").description("turn-by-turn structure (role, preview, tool calls, approx size) of a Claude Code session JSONL transcript, instead of a raw Read; defaults to the current project's most recent session").option("--project <path>", "project root to resolve the session transcript against").option("--json", "output JSON").action(guard(cmdSessionOutline));
16292
17056
  program2.command("session-slice [session-id-or-path]").description("full content of one turn range from a Claude Code session JSONL transcript (see session-outline for turn numbers), instead of a raw Read").requiredOption("--range <spec>", "turn range, e.g. 5-9 or 12 (see session-outline for turn numbers)").option("--project <path>", "project root to resolve the session transcript against").option("--json", "output JSON").action(guard(cmdSessionSlice));
17057
+ program2.command("session-audit").description("corpus-wide token attribution across every local Claude Code session transcript: measured billed usage, estimated content size by source and by tool, and billed cost by session position (aggregate counts only, never transcript content)").option("--dir <path>", "transcript corpus root to scan (default: ~/.claude/projects)").option("--json", "output JSON").action(guard(cmdSessionAudit));
16293
17058
  program2.command("mcp-audit").description("MCP server schema cost-vs-usage report: estimate per-server token cost from cached tool calls").option("--project <path>", "project root to analyze").option("--json", "output JSON").action(guard(cmdMcpAudit));
16294
17059
  program2.command("recall [query]").description("search across every cached bash-output, web-output, and mcp-output entry (full-text); with no query, list them newest-first").option("--type <type>", "filter to one cache type: bash, web, or mcp").option("--limit <n>", "max results to return (default: 10)").option("--json", "output JSON").action(guard(cmdRecall));
16295
17060
  program2.command("hint-stats").description("per-category efficacy report for token-goat's discretionary hint hooks (emitted/acted-on/suppression)").option("--json", "output JSON").option("--reset", "clear all tracked emissions and manual marks").option("--mark-effective <category>", 'record a manual "effective" vote for a hint category').option("--mark-ineffective <category>", 'record a manual "ineffective" vote for a hint category').action(guard(cmdHintStats));
@@ -16654,7 +17419,7 @@ function buildProgram() {
16654
17419
  )
16655
17420
  );
16656
17421
  program2.command("gdrive-sections <file-id>").description("fetch and list sections from a public Google Doc").option("--heading <name>", "get content of one named section").option("--fresh", "skip the on-disk cache and force a live fetch").action(guard(cmdGdriveSections));
16657
- program2.command("compress").alias("bash").alias("run").description("run a shell command and emit a compressed view of its output").requiredOption("-c, --cmd <command>", "the shell command to run, as one string").option("-f, --filter <name>", "filter name (auto-detected from the command when omitted)").option("--timeout <seconds>", "wall-clock timeout in seconds (0 = built-in default)").option("--no-compress", "stream output raw without compression (debug the wrapper)").option("--profile <name>", "compression profile: aggressive | balanced | minimal").option("--max-tokens <n>", "post-compress token cap (0 = no cap)").action(cmdCompress);
17422
+ program2.command("compress").alias("bash").alias("run").description("run a shell command (under a POSIX shell / bash) and emit a compressed view of its output").requiredOption("-c, --cmd <command>", "the shell command to run, as one string (use / for paths across platforms)").option("-f, --filter <name>", "filter name (auto-detected from the command when omitted)").option("--timeout <seconds>", "wall-clock timeout in seconds (0 = built-in default)").option("--no-compress", "stream output raw without compression (debug the wrapper)").option("--profile <name>", "compression profile: aggressive | balanced | minimal").option("--max-tokens <n>", "post-compress token cap (0 = no cap)").action(cmdCompress);
16658
17423
  program2.command("version").description("print the token-goat version").action(
16659
17424
  guard(() => {
16660
17425
  out(VERSION);