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 +12 -6
- package/dist/{token-goat-chunk-2MWF3OGR.mjs → token-goat-chunk-222VPFP2.mjs} +138 -49
- package/dist/{token-goat-chunk-5MLXFSRI.mjs → token-goat-chunk-4HIMCBYK.mjs} +2 -2
- package/dist/{token-goat-chunk-SE6V7LUZ.mjs → token-goat-chunk-4OM2Q2SX.mjs} +5 -5
- package/dist/{token-goat-chunk-IVCTQPZD.mjs → token-goat-chunk-6ODZ6PZK.mjs} +219 -147
- package/dist/{token-goat-chunk-AL644JUV.mjs → token-goat-chunk-C5IL6MJH.mjs} +3 -3
- package/dist/{token-goat-chunk-JU7QESB3.mjs → token-goat-chunk-DP3NNGGV.mjs} +5 -5
- package/dist/{token-goat-chunk-TF5NT3H5.mjs → token-goat-chunk-EFF2XCLB.mjs} +18 -5
- package/dist/{token-goat-chunk-KBVN4ELV.mjs → token-goat-chunk-NKNCHJ4H.mjs} +80 -49
- package/dist/{token-goat-chunk-AM23GDIS.mjs → token-goat-chunk-PWVXXPCC.mjs} +134 -29
- package/dist/{token-goat-chunk-LILS6TIU.mjs → token-goat-chunk-TX4JFJTD.mjs} +74 -17
- package/dist/{token-goat-chunk-BTZSUIYA.mjs → token-goat-chunk-VAGPOZHO.mjs} +2 -2
- package/dist/token-goat-hook.mjs +5 -5
- package/dist/token-goat.core.mjs +5 -5
- package/package.json +1 -1
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
|
|
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 —
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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(
|
|
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
|
|
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:
|
|
14674
|
+
printed = JSON.stringify({ file, pattern, matchCount: fencedMatches.length, pages, matches: fencedMatches }, null, 2);
|
|
14605
14675
|
out(printed);
|
|
14606
|
-
} else if (
|
|
14676
|
+
} else if (fencedMatches.length === 0) {
|
|
14607
14677
|
printed = "(no matches)";
|
|
14608
14678
|
out(printed);
|
|
14609
14679
|
} else {
|
|
14610
|
-
const lines =
|
|
14680
|
+
const lines = fencedMatches.map((m) => `p${m.page}: ${m.snippet}`);
|
|
14611
14681
|
printed = `${lines.join("\n")}
|
|
14612
14682
|
|
|
14613
|
-
${countNoun(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
15872
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
11
|
-
import "./token-goat-chunk-
|
|
12
|
-
import "./token-goat-chunk-
|
|
13
|
-
import "./token-goat-chunk-
|
|
14
|
-
import "./token-goat-chunk-
|
|
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 {
|