token-goat 2.8.2 → 2.8.3

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.
package/README.md CHANGED
@@ -72,7 +72,7 @@ The fastest way to reduce AI token costs is fixing these five, not writing short
72
72
  | Agent re-reads a file edited mid-session | Unified diff injected as a hint — full Read avoided when the diff covers the change. Docs and source/style/data files (`.md`/`.ts`/`.css`/`.json`/…) by default; set `serve_diff_on_reread = false` to disable source diffs |
73
73
  | Compaction forgets which files were edited | Structured session manifest injected before compact |
74
74
  | Same files re-read from scratch after `/compact` | Recovery hint at SessionStart lists cached snapshot + bash + WebFetch IDs |
75
- | Loaded skill body summarised away by compaction | `### Active Skills` manifest section + `**Skills**:` recovery block list every loaded skill; full body recoverable via `token-goat skill-body <name>` without re-invoking |
75
+ | Loaded skill body summarized away by compaction | `### Active Skills` manifest section + `**Skills**:` recovery block list every loaded skill; full body recoverable via `token-goat skill-body <name>` without re-invoking |
76
76
  | Large skill bodies re-injected each turn (6 active skills = 65k+ tokens) | `<!-- COMPACT_END -->` marker: everything above the marker is the compact form; token-goat detects it on load, caches the compact slice, and injects only that — typically ~400 tokens vs. 10k+ |
77
77
  | Model reads a skill SKILL.md file directly mid-session (burning the full 10k–65k tokens again) | Pre-Read hook intercepts `*/.claude/skills/<name>/SKILL.md` paths; if the skill is already cached this session it emits a `token-goat skill-body <name>` hint instead |
78
78
  | Same large skill invoked twice in a session | PreToolUse hook blocks the reload; serves cached compact (~400 tokens) via `additionalContext` instead of the full 40–65k body. Allows the reload if compaction fired since the last load |
@@ -666,7 +666,7 @@ Missed lookups recover surgically: `read` and `section` print a "Did you mean…
666
666
 
667
667
  ### Skill efficiency — the `<!-- COMPACT_END -->` marker
668
668
 
669
- When Claude Code invokes a skill, it re-injects the full skill body on every subsequent turn. A large skill file (e.g. a 10k-token `/improve` or `/ralph`) can cost 40–65k tokens per session across 6 active skills. The `<!-- COMPACT_END -->` marker solves this: place it in any skill file to split it into a compact form (above the marker, ~400 tokens) and a reference section (below). Token-goat detects the marker the first time the skill fires, caches only the compact slice, and injects that from then on — labelled `--- compact form (N tokens) ---` so the model knows to request the full body only when it needs the detail.
669
+ When Claude Code invokes a skill, it re-injects the full skill body on every subsequent turn. A large skill file (e.g. a 10k-token `/improve` or `/ralph`) can cost 40–65k tokens per session across 6 active skills. The `<!-- COMPACT_END -->` marker solves this: place it in any skill file to split it into a compact form (above the marker, ~400 tokens) and a reference section (below). Token-goat detects the marker the first time the skill fires, caches only the compact slice, and injects that from then on — labeled `--- compact form (N tokens) ---` so the model knows to request the full body only when it needs the detail.
670
670
 
671
671
  To add the marker to a skill, open the file and insert `<!-- COMPACT_END -->` on its own line where the "quick reference ends and the detail begins" — typically after the quick-start table and before step-by-step instructions. The full reference section is still reachable via `token-goat skill-section "<name>::<heading>"` or `token-goat skill-body <name>` when needed.
672
672
 
@@ -756,7 +756,7 @@ $ token-goat waste --copilot
756
756
  Copilot counted 11,548 tok of tool definitions in total, so this is roughly 18.5% of it.
757
757
  ```
758
758
 
759
- The fixed overhead is the largest number in a Copilot session and no hook can reach it: Copilot assembles the system prompt and the tool definitions natively, with nothing between assembly and send. Only configuration moves it. The per-server breakdown exists to make that configuration decision possible, since one aggregate says the tool definitions are expensive without saying which tools. It is read from Copilot's own MCP tool cache, counts only the fields a model is actually sent, and is labelled an estimate throughout: it comes from byte length rather than Copilot's tokeniser, and it deliberately does not add up to Copilot's total, because Copilot's own built-in tools are not cached there.
759
+ The fixed overhead is the largest number in a Copilot session and no hook can reach it: Copilot assembles the system prompt and the tool definitions natively, with nothing between assembly and send. Only configuration moves it. The per-server breakdown exists to make that configuration decision possible, since one aggregate says the tool definitions are expensive without saying which tools. It is read from Copilot's own MCP tool cache, counts only the fields a model is actually sent, and is labeled an estimate throughout: it comes from byte length rather than Copilot's tokeniser, and it deliberately does not add up to Copilot's total, because Copilot's own built-in tools are not cached there.
760
760
 
761
761
  The "Assistant output" section is separate from the tool-call ledger above it: `generatedTokens` is what was actually paid, once, to produce the assistant's own text turns. `resendCeilingTokens` is a cache-unaware upper bound on how much re-sending those turns as conversation history on every later request could cost — not real spend, since Claude Code's prompt caching bills a repeated conversation prefix at cache-read rates, a fraction of full input price. Treat it as a ceiling on how bad unbounded verbosity could get, not as a dollar figure.
762
762
 
@@ -778,7 +778,7 @@ $ token-goat recall "eslint warnings"
778
778
 
779
779
  Results are ranked by relevance (BM25 via SQLite FTS5, falling back to a plain substring scan if FTS5 is unavailable), newest indexed entries win ties. `--type bash|web|mcp` narrows to one cache type; `--limit <n>` caps the result count (default 10); `--json` emits `{ id, cacheType, label, snippet, storedAt }[]` instead. The index is built incrementally as entries are cached — there is no separate rebuild step.
780
780
 
781
- Run `token-goat recall` with **no query** to browse instead of search: every cached entry across all three types, newest first, in the same format and honouring the same `--type`/`--limit`/`--json` flags. This is the case where the index matters most — the ids have scrolled out of context and you have no term to search for, so the alternative is running `bash-history`, `web-history`, and `mcp-history` in turn.
781
+ Run `token-goat recall` with **no query** to browse instead of search: every cached entry across all three types, newest first, in the same format and honoring the same `--type`/`--limit`/`--json` flags. This is the case where the index matters most — the ids have scrolled out of context and you have no term to search for, so the alternative is running `bash-history`, `web-history`, and `mcp-history` in turn.
782
782
 
783
783
  ### Hint efficacy tracking
784
784
 
@@ -1066,7 +1066,13 @@ A working install returns `["PreToolUse", "PostToolUse", "PreCompact"]`. Any mis
1066
1066
 
1067
1067
  ## Image support
1068
1068
 
1069
- Token-goat shrinks large images before they reach the model, cutting vision token costs by 60–90%. The pipeline uses [`sharp`](https://sharp.pixelplumbing.com/), a Node.js image processing library that ships prebuilt native binaries for Windows, macOS, Linux, and Alpine.
1069
+ Vision models bill by pixel dimensions, not file size. Anthropic charges one token per 28×28-pixel patch of a Claude image (`⌈width/28⌉ × ⌈height/28⌉` visual tokens, per the [Claude vision docs](https://platform.claude.com/docs/en/build-with-claude/vision#evaluate-image-size)), OpenAI's GPT-5.6 models tile in 32×32-pixel patches with a 1.2x multiplier on top, and Gemini charges a flat 258 tokens under 384×384 pixels and roughly 258 tokens per 768×768 tile above that. A heavily compressed screenshot can still decode to a large pixel count, so a small file on disk is no guarantee of a cheap read.
1070
+
1071
+ Token-goat shrinks an image before it reaches the model whenever either of two independent checks trips: the file is at or above 512 KB, or its longest edge exceeds 1568 pixels (`src/image_shrink.ts`). The byte check is a cheap pre-filter that skips decoding most images outright. The dimension check exists because the byte check alone misses a case: a flat-color screenshot can compress to a few hundred kilobytes on disk and still decode to a resolution well past 1568 pixels on its long edge, and vision models bill on that decoded resolution, not the compressed file. Either trigger routes the image through the same pipeline: downscale to a 1568px long edge and re-encode as WebP. The pipeline uses [`sharp`](https://sharp.pixelplumbing.com/), a Node.js image processing library that ships prebuilt native binaries for Windows, macOS, Linux, and Alpine.
1072
+
1073
+ How much this saves depends on the model, because Claude downscales an oversized image itself before billing and caps the cost rather than charging for every pixel it was sent. By the [published per-tier table](https://platform.claude.com/docs/en/build-with-claude/vision#evaluate-image-size), a 3840x2160 screenshot bills at 4784 visual tokens on Claude 4.7 and later, and 1560 on earlier models. Resizing it to a 1568px long edge first costs 1792 tokens: a 63% cut against the newer models, and nothing against the older ones, whose own cap is already tighter than what this pipeline produces. Other vendors cap differently, so treat 63% as the measured Claude figure rather than a universal rate.
1074
+
1075
+ Two cases are not about cost at all. Screenshots returned to the computer-use and browser-use toolsets are rejected outright when they exceed the model's limits, rather than downscaled, so resizing before the call is what lets it succeed. A request carrying more than 20 images also applies a stricter per-image dimension limit to every image in it, which a 1568px long edge already satisfies.
1070
1076
 
1071
1077
  On most platforms, `npm install -g token-goat` installs sharp without additional steps. npm pulls a prebuilt binary keyed to your Node.js major version and OS — no C++ compiler, libvips, or system codec libraries required.
1072
1078
 
@@ -1278,7 +1284,7 @@ A third case needs no fence, because the danger is the line break rather than th
1278
1284
 
1279
1285
  The MCP tools (`symbol` when given a `file` filter, `read`, `section`, `skeleton`, `outline`, `refs`, `brief`, `grep`, `imports`, `exports`) are confined to the project root, resolving symlinks before the check. Set `mcp.confine_reads_to_project_root = false` (env `TOKEN_GOAT_MCP_CONFINE_READS`) in your global config if you genuinely need cross-root reads from an MCP client; a per-project file cannot set it. The CLI is deliberately unconfined and unchanged. This is defense in depth for one sink, not a sandbox: an agent that can call these tools can usually call its own read tool too.
1280
1286
 
1281
- Note what that flag does and does not cover. It stops a caller traversing *out of* the root it is given; it does not constrain *which* root the caller supplies. Every MCP tool takes an optional `projectRoot`, and it exists for a reason — the server's cwd is often not the workspace root for MCP clients — but tool arguments are model-generated, so that choice is untrusted input like any other. If your deployment treats MCP as the only path to the filesystem, set `mcp.allowed_roots` (env `TOKEN_GOAT_MCP_ALLOWED_ROOTS`, delimiter-separated like `PATH`) to the roots that may legitimately be named; a resolved root outside every entry is then refused. It is empty by default, which keeps the multi-root behaviour above unchanged.
1287
+ Note what that flag does and does not cover. It stops a caller traversing *out of* the root it is given; it does not constrain *which* root the caller supplies. Every MCP tool takes an optional `projectRoot`, and it exists for a reason — the server's cwd is often not the workspace root for MCP clients — but tool arguments are model-generated, so that choice is untrusted input like any other. If your deployment treats MCP as the only path to the filesystem, set `mcp.allowed_roots` (env `TOKEN_GOAT_MCP_ALLOWED_ROOTS`, delimiter-separated like `PATH`) to the roots that may legitimately be named; a resolved root outside every entry is then refused. It is empty by default, which keeps the multi-root behavior above unchanged.
1282
1288
 
1283
1289
  **Restricting what token-goat may fetch.** `webfetch.allow` and `webfetch.deny` (env `TOKEN_GOAT_WEBFETCH_ALLOW` / `TOKEN_GOAT_WEBFETCH_DENY`, comma-separated) are wildcard URL patterns that decide which addresses may be reached. Deny is checked first and wins; a non-empty allow list refuses anything it does not name. Patterns are matched against the address as it will actually be sent, not only as you typed it, so a trailing dot on the host, `..` path segments, a default port written out, and percent-encoded path characters cannot be used to step around a rule. Writing a default port in a pattern (`https://example.com:443/*`) and omitting it are equivalent. Both are empty by default, which permits everything, exactly as before. They apply to the WebFetch call your AI makes, to the fetches token-goat performs itself (`fetch-image`, `gdrive-sections`), and to the headless browser behind `screenshot` (whose page sub-resources are checked too), including every redirect hop, so an allowed site cannot redirect the request on to a denied one.
1284
1290
 
@@ -86,7 +86,7 @@ import {
86
86
  runZipRead,
87
87
  symbolNamesInFile,
88
88
  upsertNote
89
- } from "./token-goat-chunk-LILS6TIU.mjs";
89
+ } from "./token-goat-chunk-TX4JFJTD.mjs";
90
90
  import {
91
91
  BASH_OUTPUT_SUBDIR,
92
92
  GEMINI_TOOL_NAME_MAP,
@@ -113,7 +113,7 @@ import {
113
113
  searchRecall,
114
114
  storeWebOutput,
115
115
  summarizeResidentContext
116
- } from "./token-goat-chunk-5MLXFSRI.mjs";
116
+ } from "./token-goat-chunk-4HIMCBYK.mjs";
117
117
  import {
118
118
  AGENT_SALT_MARKER,
119
119
  CONTEXT_AUTOCOMPACT_TOKENS,
@@ -123,6 +123,7 @@ import {
123
123
  SESSIONS_SUBDIR,
124
124
  SKILLS_OUTPUT_SUBDIR,
125
125
  SKIP_DIRS,
126
+ UNTRUSTED_FILE_TAG,
126
127
  UNTRUSTED_TOOL_TAG,
127
128
  UNTRUSTED_WEB_TAG,
128
129
  WORKER_HEARTBEAT_STALE_MS,
@@ -256,7 +257,7 @@ import {
256
257
  vscodeDecoderConfigured,
257
258
  walkProject,
258
259
  writeCompact
259
- } from "./token-goat-chunk-AM23GDIS.mjs";
260
+ } from "./token-goat-chunk-PWVXXPCC.mjs";
260
261
  import {
261
262
  C,
262
263
  CONFIG_KEY_ENV_OVERRIDES,
@@ -338,7 +339,7 @@ import {
338
339
  withFileLock,
339
340
  withRetryOnLock,
340
341
  writeJsonSettings
341
- } from "./token-goat-chunk-IVCTQPZD.mjs";
342
+ } from "./token-goat-chunk-6ODZ6PZK.mjs";
342
343
  import {
343
344
  __export
344
345
  } from "./token-goat-chunk-AEX54RUZ.mjs";
@@ -3760,7 +3761,7 @@ function attemptedCommandName(argv) {
3760
3761
  // src/cli.ts
3761
3762
  import * as fs27 from "fs";
3762
3763
  import * as path27 from "path";
3763
- import { homedir as homedir11 } from "os";
3764
+ import { homedir as homedir12 } from "os";
3764
3765
 
3765
3766
  // src/walk_index.ts
3766
3767
  import * as fs2 from "node:fs";
@@ -13504,20 +13505,55 @@ function formatSessionSlice(turns) {
13504
13505
 
13505
13506
  // src/cli_mcp_audit.ts
13506
13507
  import * as fs26 from "node:fs";
13508
+ import * as os12 from "node:os";
13507
13509
  import * as path26 from "node:path";
13508
- function readMcpConfig(projectRoot) {
13509
- const configPath2 = path26.join(projectRoot, ".mcp.json");
13510
+ function readMcpJsonFile(configPath2) {
13510
13511
  try {
13511
- if (!fs26.existsSync(configPath2)) {
13512
- return null;
13513
- }
13512
+ if (!fs26.existsSync(configPath2)) return null;
13514
13513
  const content = fs26.readFileSync(configPath2, "utf-8");
13515
13514
  const parsed = JSON.parse(content);
13516
- return parsed.mcpServers || parsed;
13515
+ const servers = parsed && typeof parsed === "object" ? parsed.mcpServers ?? parsed : null;
13516
+ return servers && typeof servers === "object" ? servers : null;
13517
+ } catch {
13518
+ return null;
13519
+ }
13520
+ }
13521
+ function driveLetterCaseVariants(p) {
13522
+ const m = /^([a-zA-Z]:)(.*)$/s.exec(p);
13523
+ if (m === null) return [p];
13524
+ const [, drive, rest] = m;
13525
+ return [`${drive.toLowerCase()}${rest}`, `${drive.toUpperCase()}${rest}`];
13526
+ }
13527
+ function readClaudeJsonConfig(claudeJsonPath, projectRoot) {
13528
+ try {
13529
+ if (!fs26.existsSync(claudeJsonPath)) return null;
13530
+ const parsed = JSON.parse(fs26.readFileSync(claudeJsonPath, "utf-8"));
13531
+ const projects = parsed && typeof parsed === "object" ? parsed.projects : null;
13532
+ if (!projects || typeof projects !== "object") return null;
13533
+ const slashForms = [projectRoot, projectRoot.replace(/\\/g, "/"), projectRoot.replace(/\//g, "\\")];
13534
+ const candidates = [...new Set(slashForms.flatMap(driveLetterCaseVariants))];
13535
+ for (const key of candidates) {
13536
+ const entry = projects[key];
13537
+ if (entry && typeof entry === "object") {
13538
+ const servers = entry["mcpServers"];
13539
+ if (servers && typeof servers === "object") return servers;
13540
+ }
13541
+ }
13542
+ return null;
13517
13543
  } catch {
13518
13544
  return null;
13519
13545
  }
13520
13546
  }
13547
+ function discoverMcpConfig(projectRoot, home) {
13548
+ const mcpJsonPath = path26.join(projectRoot, ".mcp.json");
13549
+ const claudeJsonPath = path26.join(home, ".claude.json");
13550
+ const sourcesChecked = [mcpJsonPath, claudeJsonPath];
13551
+ const fromMcpJson = readMcpJsonFile(mcpJsonPath);
13552
+ if (fromMcpJson !== null) return { servers: fromMcpJson, sourcePath: mcpJsonPath, sourcesChecked };
13553
+ const fromClaudeJson = readClaudeJsonConfig(claudeJsonPath, projectRoot);
13554
+ if (fromClaudeJson !== null) return { servers: fromClaudeJson, sourcePath: claudeJsonPath, sourcesChecked };
13555
+ return { servers: null, sourcePath: null, sourcesChecked };
13556
+ }
13521
13557
  function analyzeMcpCache() {
13522
13558
  const serverMetrics = /* @__PURE__ */ new Map();
13523
13559
  const blobs = listBlobs(BASH_OUTPUT_SUBDIR).filter((b) => b.id.startsWith("mcp_"));
@@ -13539,8 +13575,9 @@ function analyzeMcpCache() {
13539
13575
  }
13540
13576
  return serverMetrics;
13541
13577
  }
13542
- function buildMcpAuditReport(projectRoot) {
13543
- const config = readMcpConfig(projectRoot);
13578
+ function buildMcpAuditReport(projectRoot, home = os12.homedir()) {
13579
+ const discovery = discoverMcpConfig(projectRoot, home);
13580
+ const config = discovery.servers;
13544
13581
  const cacheMetrics = analyzeMcpCache();
13545
13582
  const servers = [];
13546
13583
  let totalCost = 0;
@@ -13574,7 +13611,10 @@ function buildMcpAuditReport(projectRoot) {
13574
13611
  servers.sort((a, b) => b.totalTokens - a.totalTokens);
13575
13612
  return {
13576
13613
  projectRoot,
13577
- configFound: config !== null,
13614
+ configFound: discovery.sourcePath !== null,
13615
+ configSourcePath: discovery.sourcePath,
13616
+ configSourcesChecked: discovery.sourcesChecked,
13617
+ costKnown: discovery.sourcePath !== null || cacheMetrics.size > 0,
13578
13618
  servers,
13579
13619
  totalCost
13580
13620
  };
@@ -13586,11 +13626,12 @@ function printReport3(report) {
13586
13626
  w("\n# token-goat mcp-audit\n");
13587
13627
  w(`Project: ${report.projectRoot}
13588
13628
  `);
13589
- w(`Config found: ${report.configFound ? "yes" : "no"}
13629
+ w(report.configSourcePath !== null ? `Config found: yes (${report.configSourcePath})
13630
+ ` : `Config found: no (checked: ${report.configSourcesChecked.join(", ")})
13590
13631
  `);
13591
13632
  w("\n## MCP servers\n");
13592
13633
  if (report.servers.length === 0) {
13593
- w(" none\n");
13634
+ w(report.costKnown ? " none\n" : " none discovered from a readable config source\n");
13594
13635
  } else {
13595
13636
  w("| Server | Per-Call (tok) | Calls | Total (tok) |\n");
13596
13637
  w("|--------|---|---|---|\n");
@@ -13599,9 +13640,10 @@ function printReport3(report) {
13599
13640
  `);
13600
13641
  }
13601
13642
  }
13602
- w(`
13643
+ w(report.costKnown ? `
13603
13644
  Total cost: ${report.totalCost} tok
13604
- `);
13645
+ ` : "\nTotal cost: unknown -- no readable MCP config was found and no MCP calls have been recorded in this session's cache yet\n");
13646
+ w("\nNote: plugin-provided MCP servers have no on-disk config token-goat can read, so a live session may have MCP servers this audit cannot see or price.\n");
13605
13647
  }
13606
13648
  async function runMcpAuditCommand(opts = {}) {
13607
13649
  const projectRoot = resolveProjectRoot(opts.project !== void 0 ? { project: opts.project } : {});
@@ -13620,6 +13662,20 @@ var RECALL_COMMAND = {
13620
13662
  web: "web-output",
13621
13663
  mcp: "mcp-output"
13622
13664
  };
13665
+ function fenceTagForCacheType(cacheType) {
13666
+ return cacheType === "web" ? UNTRUSTED_WEB_TAG : UNTRUSTED_TOOL_TAG;
13667
+ }
13668
+ function fenceSnippetIfMatched(hit) {
13669
+ let matches2 = [];
13670
+ try {
13671
+ if (loadConfig().injection.enabled) matches2 = scanForInjectionPatterns(hit.snippet);
13672
+ } catch {
13673
+ matches2 = [];
13674
+ }
13675
+ if (matches2.length === 0) return hit.snippet;
13676
+ recordStat("injection_detected", 0, 0, void 0, matches2.join(","));
13677
+ return fenceUntrustedContent(hit.snippet, matches2, fenceTagForCacheType(hit.cacheType));
13678
+ }
13623
13679
  function printHits(query, hits) {
13624
13680
  const w = (text) => {
13625
13681
  process.stdout.write(text);
@@ -13635,7 +13691,7 @@ function printHits(query, hits) {
13635
13691
  `);
13636
13692
  w(` ${label}
13637
13693
  `);
13638
- w(` ${hit.snippet}
13694
+ w(` ${fenceSnippetIfMatched(hit)}
13639
13695
 
13640
13696
  `);
13641
13697
  }
@@ -13648,7 +13704,8 @@ function runRecallCommand(query, opts = {}) {
13648
13704
  };
13649
13705
  const hits = browse ? listRecentRecall(scope) : searchRecall(query, scope);
13650
13706
  if (opts.json === true) {
13651
- process.stdout.write(`${JSON.stringify(hits)}
13707
+ const fenced = hits.map((hit) => ({ ...hit, snippet: fenceSnippetIfMatched(hit) }));
13708
+ process.stdout.write(`${JSON.stringify(fenced)}
13652
13709
  `);
13653
13710
  return;
13654
13711
  }
@@ -13679,11 +13736,12 @@ function printSummary(rows) {
13679
13736
  }
13680
13737
  function printTotals(totals) {
13681
13738
  const spent = totals.spentBytes === null ? "n/a" : String(totals.spentBytes);
13682
- const net = totals.netBytes === null ? "n/a" : String(totals.netBytes);
13683
13739
  const legacyNote = totals.legacyEmissions > 0 ? ` (excludes ${totals.legacyEmissions} legacy emission(s) recorded before spend tracking)` : "";
13684
- process.stdout.write(`
13685
- TOTAL saved=${totals.savedBytes} spent=${spent} net=${net}${legacyNote}
13686
- `);
13740
+ process.stdout.write(
13741
+ `
13742
+ TOTAL saved=${totals.savedBytes} (all-time, every hint kind) spent=${spent} (hint_emissions ledger only)${legacyNote}
13743
+ `
13744
+ );
13687
13745
  }
13688
13746
  function runHintStatsCommand(opts = {}) {
13689
13747
  if (opts.reset === true) {
@@ -14043,7 +14101,7 @@ async function cmdMcpServe() {
14043
14101
  let StdioServerTransport;
14044
14102
  try {
14045
14103
  ;
14046
- ({ createMcpServer } = await import("./token-goat-chunk-AL644JUV.mjs"));
14104
+ ({ createMcpServer } = await import("./token-goat-chunk-C5IL6MJH.mjs"));
14047
14105
  ({ StdioServerTransport } = await import("./token-goat-chunk-324QOJYZ.mjs"));
14048
14106
  } catch (err2) {
14049
14107
  process.stderr.write(
@@ -14068,7 +14126,7 @@ async function cmdHook(event, opts) {
14068
14126
  if (typeof opts.harness === "string" && opts.harness.length > 0) {
14069
14127
  process.env[ENV_KEYS.HARNESS_OVERRIDE] = opts.harness;
14070
14128
  }
14071
- const { relay } = await import("./token-goat-chunk-JU7QESB3.mjs");
14129
+ const { relay } = await import("./token-goat-chunk-DP3NNGGV.mjs");
14072
14130
  await relay(event);
14073
14131
  }
14074
14132
  async function cmdInstall(opts) {
@@ -14181,7 +14239,7 @@ async function cmdInstall(opts) {
14181
14239
  );
14182
14240
  }
14183
14241
  try {
14184
- const skillDir = path27.join(homedir11(), ".claude", "skills");
14242
+ const skillDir = path27.join(homedir12(), ".claude", "skills");
14185
14243
  if (fs27.existsSync(skillDir)) {
14186
14244
  const entries = fs27.readdirSync(skillDir, { withFileTypes: true });
14187
14245
  const skillNames = [];
@@ -14522,6 +14580,17 @@ function _applyFiltersAndPrint(content, opts, fenceUntrusted = false, fenceTag =
14522
14580
  }
14523
14581
  return emit2(result.join("\n"));
14524
14582
  }
14583
+ function fenceFileTextIfMatched(text) {
14584
+ let matches2 = [];
14585
+ try {
14586
+ if (loadConfig().injection.enabled) matches2 = scanForInjectionPatterns(text);
14587
+ } catch {
14588
+ matches2 = [];
14589
+ }
14590
+ if (matches2.length === 0) return text;
14591
+ recordStat("injection_detected", 0, 0, void 0, matches2.join(","));
14592
+ return fenceUntrustedContent(text, matches2, UNTRUSTED_FILE_TAG);
14593
+ }
14525
14594
  function fileSizeOrZero(filePath) {
14526
14595
  try {
14527
14596
  return fs27.statSync(filePath).size;
@@ -14585,7 +14654,7 @@ function cmdMcpOutput(id, opts) {
14585
14654
  }
14586
14655
  async function cmdPdfExtract(file, opts) {
14587
14656
  const text = await runPdfExtractText(file, opts.pages, opts.layout === true);
14588
- const printed = _applyFiltersAndPrint(text, opts);
14657
+ const printed = _applyFiltersAndPrint(text, opts, true, UNTRUSTED_FILE_TAG);
14589
14658
  const fullSourceBytes = fileSizeOrZero(file);
14590
14659
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(printed, "utf8"));
14591
14660
  recordStat("pdf_extract", bytesSaved, Math.round(bytesSaved / 4));
@@ -14598,19 +14667,20 @@ async function cmdPdfLocate(file, pattern, opts) {
14598
14667
  if (opts.context !== void 0) locateOpts.context = requirePositiveInt("--context", opts.context);
14599
14668
  if (opts.pages !== void 0) locateOpts.pages = opts.pages;
14600
14669
  const matches2 = await runPdfLocate(file, pattern, locateOpts);
14601
- const pages = matches2.map((m) => m.page);
14670
+ const fencedMatches = matches2.map((m) => ({ ...m, snippet: fenceFileTextIfMatched(m.snippet) }));
14671
+ const pages = fencedMatches.map((m) => m.page);
14602
14672
  let printed;
14603
14673
  if (opts.json === true) {
14604
- printed = JSON.stringify({ file, pattern, matchCount: matches2.length, pages, matches: matches2 }, null, 2);
14674
+ printed = JSON.stringify({ file, pattern, matchCount: fencedMatches.length, pages, matches: fencedMatches }, null, 2);
14605
14675
  out(printed);
14606
- } else if (matches2.length === 0) {
14676
+ } else if (fencedMatches.length === 0) {
14607
14677
  printed = "(no matches)";
14608
14678
  out(printed);
14609
14679
  } else {
14610
- const lines = matches2.map((m) => `p${m.page}: ${m.snippet}`);
14680
+ const lines = fencedMatches.map((m) => `p${m.page}: ${m.snippet}`);
14611
14681
  printed = `${lines.join("\n")}
14612
14682
 
14613
- ${countNoun(matches2.length, "match", "matches")} across ${countNoun(pages.length, "page")}`;
14683
+ ${countNoun(fencedMatches.length, "match", "matches")} across ${countNoun(pages.length, "page")}`;
14614
14684
  out(printed);
14615
14685
  }
14616
14686
  const fullSourceBytes = fileSizeOrZero(file);
@@ -14627,7 +14697,8 @@ async function cmdPdfOutline(file, opts) {
14627
14697
  }
14628
14698
  return;
14629
14699
  }
14630
- const text = opts.json === true ? JSON.stringify(entries, null, 2) : entries.map((e) => `${" ".repeat(e.level)}${e.title}${e.page !== null ? ` (p.${e.page})` : ""}`).join("\n");
14700
+ const fencedEntries = entries.map((e) => ({ ...e, title: fenceFileTextIfMatched(e.title) }));
14701
+ const text = opts.json === true ? JSON.stringify(fencedEntries, null, 2) : fencedEntries.map((e) => `${" ".repeat(e.level)}${e.title}${e.page !== null ? ` (p.${e.page})` : ""}`).join("\n");
14631
14702
  out(text);
14632
14703
  const fullSourceBytes = fileSizeOrZero(file);
14633
14704
  const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(text, "utf8"));
@@ -14679,12 +14750,16 @@ async function cmdImageText(file, opts = {}) {
14679
14750
  }
14680
14751
  let text;
14681
14752
  if (opts.json === true) {
14682
- text = JSON.stringify(result, null, 2);
14753
+ text = JSON.stringify(
14754
+ result.text === null ? result : { ...result, text: fenceFileTextIfMatched(result.text) },
14755
+ null,
14756
+ 2
14757
+ );
14683
14758
  } else {
14684
14759
  const lines = [`Confidence: ${Math.round(result.confidence)}%`, `Characters: ${result.chars}`];
14685
14760
  text = result.textHeavy && result.text !== null ? `${lines.join("\n")}
14686
14761
 
14687
- ${result.text}` : `${lines.join("\n")}
14762
+ ${fenceFileTextIfMatched(result.text)}` : `${lines.join("\n")}
14688
14763
  (below usefulness threshold; text likely noise, not shown)`;
14689
14764
  }
14690
14765
  out(text);
@@ -14747,19 +14822,20 @@ function recordXlsxStat(kind, file, emitted) {
14747
14822
  }
14748
14823
  async function cmdXlsxSheets(file, opts = {}) {
14749
14824
  const sheets = await listSheets(file);
14750
- const text = opts.json === true ? JSON.stringify(sheets.map((s) => ({ name: s.name, ref: s.ref, rows: s.rows, cols: s.cols })), null, 2) : sheets.map((s) => `${s.name} ${s.ref} (${s.rows} rows x ${s.cols} cols)`).join("\n");
14825
+ const fencedSheets = sheets.map((s) => ({ ...s, name: fenceFileTextIfMatched(s.name) }));
14826
+ const text = opts.json === true ? JSON.stringify(fencedSheets.map((s) => ({ name: s.name, ref: s.ref, rows: s.rows, cols: s.cols })), null, 2) : fencedSheets.map((s) => `${s.name} ${s.ref} (${s.rows} rows x ${s.cols} cols)`).join("\n");
14751
14827
  out(text);
14752
14828
  recordXlsxStat("xlsx_sheets", file, text);
14753
14829
  }
14754
14830
  async function cmdXlsxHead(file, opts) {
14755
14831
  const rows = opts.rows !== void 0 ? requireNonNegativeInt("--rows", opts.rows) : 20;
14756
- const text = await headSheet(file, opts.sheet, rows);
14832
+ const text = fenceFileTextIfMatched(await headSheet(file, opts.sheet, rows));
14757
14833
  out(text);
14758
14834
  recordXlsxStat("xlsx_head", file, text);
14759
14835
  }
14760
14836
  async function cmdXlsxRange(file, opts) {
14761
14837
  const result = await rangeSheet(file, opts.sheet, opts.range, opts.formulas === true);
14762
- const text = formatXlsxRange(result);
14838
+ const text = fenceFileTextIfMatched(formatXlsxRange(result));
14763
14839
  out(text);
14764
14840
  recordXlsxStat("xlsx_range", file, text);
14765
14841
  }
@@ -14771,7 +14847,7 @@ async function cmdXlsxQuery(file, opts) {
14771
14847
  ...wheres !== void 0 ? { wheres } : {},
14772
14848
  ...opts.head !== void 0 ? { head: requireNonNegativeInt("--head", opts.head) } : {}
14773
14849
  });
14774
- const text = formatCsvTable(result);
14850
+ const text = fenceFileTextIfMatched(formatCsvTable(result));
14775
14851
  out(text);
14776
14852
  recordXlsxStat("xlsx_query", file, text);
14777
14853
  }
@@ -14782,20 +14858,21 @@ function recordDocStat(kind, file, emitted) {
14782
14858
  }
14783
14859
  async function cmdPptxOutline(file, opts) {
14784
14860
  const slides = await pptxOutline(file);
14785
- const text = opts.json === true ? JSON.stringify(slides, null, 2) : slides.map((s) => `${s.slide}. ${s.title || "(untitled)"} [${s.bodyChars} body chars${s.hasNotes ? ", has notes" : ""}]`).join("\n");
14861
+ const fencedSlides = slides.map((s) => ({ ...s, title: fenceFileTextIfMatched(s.title) }));
14862
+ const text = opts.json === true ? JSON.stringify(fencedSlides, null, 2) : fencedSlides.map((s) => `${s.slide}. ${s.title || "(untitled)"} [${s.bodyChars} body chars${s.hasNotes ? ", has notes" : ""}]`).join("\n");
14786
14863
  out(text);
14787
14864
  recordDocStat("pptx_outline", file, text);
14788
14865
  }
14789
14866
  async function cmdPptxSlide(file, opts) {
14790
14867
  const n = requireNonNegativeInt("--slide", opts.slide);
14791
- const text = await pptxSlideText(file, n, opts.notes === true);
14868
+ const text = fenceFileTextIfMatched(await pptxSlideText(file, n, opts.notes === true));
14792
14869
  out(text);
14793
14870
  recordDocStat("pptx_slide", file, text);
14794
14871
  }
14795
14872
  async function cmdPptxNotes(file, opts) {
14796
14873
  const n = opts.slide !== void 0 ? requireNonNegativeInt("--slide", opts.slide) : void 0;
14797
14874
  const text = await pptxNotesText(file, n);
14798
- const printed = text.length > 0 ? text : "no speaker notes found";
14875
+ const printed = text.length > 0 ? fenceFileTextIfMatched(text) : "no speaker notes found";
14799
14876
  out(printed);
14800
14877
  recordDocStat("pptx_notes", file, printed);
14801
14878
  }
@@ -14805,7 +14882,7 @@ async function cmdPptxText(file, opts) {
14805
14882
  out("no matches");
14806
14883
  return;
14807
14884
  }
14808
- const text = matches2.map((m) => `Slide ${m.slide}: ...${m.snippet}...`).join("\n");
14885
+ const text = fenceFileTextIfMatched(matches2.map((m) => `Slide ${m.slide}: ...${m.snippet}...`).join("\n"));
14809
14886
  out(text);
14810
14887
  recordDocStat("pptx_text", file, text);
14811
14888
  }
@@ -14819,13 +14896,14 @@ async function cmdDocxOutline(file, opts) {
14819
14896
  }
14820
14897
  return;
14821
14898
  }
14822
- const text = opts.json === true ? JSON.stringify(headings, null, 2) : headings.map((h) => `${" ".repeat(h.level - 1)}${h.text}`).join("\n");
14899
+ const fencedHeadings = headings.map((h) => ({ ...h, text: fenceFileTextIfMatched(h.text) }));
14900
+ const text = opts.json === true ? JSON.stringify(fencedHeadings, null, 2) : fencedHeadings.map((h) => `${" ".repeat(h.level - 1)}${h.text}`).join("\n");
14823
14901
  out(text);
14824
14902
  recordDocStat("docx_outline", file, text);
14825
14903
  }
14826
14904
  async function cmdDocxText(file, opts) {
14827
14905
  const text = await docxText(file);
14828
- const printed = _applyFiltersAndPrint(text, opts);
14906
+ const printed = _applyFiltersAndPrint(text, opts, true, UNTRUSTED_FILE_TAG);
14829
14907
  recordDocStat("docx_text", file, printed);
14830
14908
  }
14831
14909
  function cmdTranscriptOutline(file, opts) {
@@ -14963,7 +15041,7 @@ function emitExtraFileArgsNote(command, first, extras, opts = {}) {
14963
15041
  }
14964
15042
  async function cmdCompress(opts) {
14965
15043
  try {
14966
- const bashRunner = await import("./token-goat-chunk-BTZSUIYA.mjs");
15044
+ const bashRunner = await import("./token-goat-chunk-VAGPOZHO.mjs");
14967
15045
  if (opts.compress === false) {
14968
15046
  process.exitCode = bashRunner.runRaw(opts.cmd, parseTimeout(opts.timeout, bashRunner.DEFAULT_TIMEOUT_SECONDS));
14969
15047
  return;
@@ -15868,8 +15946,19 @@ ${content}`;
15868
15946
  const sections = await getDocSections(fileId, { fresh: false });
15869
15947
  emitted = formatSections(sections);
15870
15948
  }
15871
- out(emitted);
15872
- const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(emitted, "utf8"));
15949
+ let toEmit = emitted;
15950
+ try {
15951
+ if (loadConfig().injection.enabled) {
15952
+ const matches2 = scanForInjectionPatterns(emitted);
15953
+ if (matches2.length > 0) {
15954
+ recordStat("injection_detected", 0, 0, void 0, matches2.join(","));
15955
+ toEmit = fenceUntrustedContent(emitted, matches2, UNTRUSTED_WEB_TAG);
15956
+ }
15957
+ }
15958
+ } catch {
15959
+ }
15960
+ out(toEmit);
15961
+ const bytesSaved = Math.max(1, fullSourceBytes - Buffer.byteLength(toEmit, "utf8"));
15873
15962
  recordStat("gdrive_sections", bytesSaved, Math.round(bytesSaved / 4));
15874
15963
  }
15875
15964
  function expandGlobs(root, patterns, globFnOverride) {
@@ -8,7 +8,7 @@ import {
8
8
  loadBlob,
9
9
  sanitizeFtsQuery,
10
10
  storeBlob
11
- } from "./token-goat-chunk-AM23GDIS.mjs";
11
+ } from "./token-goat-chunk-PWVXXPCC.mjs";
12
12
  import {
13
13
  SYMBOL_BODY_CHAR_CAP,
14
14
  extractErrorMessage,
@@ -18,7 +18,7 @@ import {
18
18
  redactSecrets,
19
19
  runGit,
20
20
  shortFingerprint
21
- } from "./token-goat-chunk-IVCTQPZD.mjs";
21
+ } from "./token-goat-chunk-6ODZ6PZK.mjs";
22
22
  import {
23
23
  registerReset
24
24
  } from "./token-goat-chunk-AO2QD2AG.mjs";
@@ -7,11 +7,11 @@ import {
7
7
  expandGlobs,
8
8
  leftoverIntegrations,
9
9
  run
10
- } from "./token-goat-chunk-2MWF3OGR.mjs";
11
- import "./token-goat-chunk-LILS6TIU.mjs";
12
- import "./token-goat-chunk-5MLXFSRI.mjs";
13
- import "./token-goat-chunk-AM23GDIS.mjs";
14
- import "./token-goat-chunk-IVCTQPZD.mjs";
10
+ } from "./token-goat-chunk-222VPFP2.mjs";
11
+ import "./token-goat-chunk-TX4JFJTD.mjs";
12
+ import "./token-goat-chunk-4HIMCBYK.mjs";
13
+ import "./token-goat-chunk-PWVXXPCC.mjs";
14
+ import "./token-goat-chunk-6ODZ6PZK.mjs";
15
15
  import "./token-goat-chunk-AO2QD2AG.mjs";
16
16
  import "./token-goat-chunk-AEX54RUZ.mjs";
17
17
  export {