token-goat 2.8.4 → 2.9.1
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 +20 -8
- package/dist/{token-goat-chunk-NZFTWBGV.mjs → token-goat-chunk-4KD2N46D.mjs} +28 -27
- package/dist/{token-goat-chunk-CZALRRGN.mjs → token-goat-chunk-4ZDRTNGV.mjs} +9942 -95
- package/dist/{token-goat-chunk-L6YP6FKQ.mjs → token-goat-chunk-AUM22UQ2.mjs} +5 -5
- package/dist/{token-goat-chunk-LZOAPGWR.mjs → token-goat-chunk-CD6U5OD6.mjs} +28 -4
- package/dist/{token-goat-chunk-4OTIB7SB.mjs → token-goat-chunk-FDXINYK4.mjs} +2120 -3658
- package/dist/token-goat-chunk-LHMC5ENL.mjs +79 -0
- package/dist/{token-goat-chunk-IZRXU64B.mjs → token-goat-chunk-MR7MNSDR.mjs} +155 -517
- package/dist/{token-goat-chunk-ZQ3PUOP3.mjs → token-goat-chunk-RRNRPCLX.mjs} +167 -116
- package/dist/{token-goat-chunk-U4UI5F3S.mjs → token-goat-chunk-SQSB5P5P.mjs} +842 -368
- package/dist/{token-goat-chunk-RE7S7H26.mjs → token-goat-chunk-SXSWQMNP.mjs} +512 -206
- package/dist/{token-goat-chunk-HNODKNWO.mjs → token-goat-chunk-V5T3YNYR.mjs} +5 -5
- package/dist/token-goat-hook.mjs +5 -5
- package/dist/token-goat.core.mjs +5 -5
- package/package.json +1 -1
- package/dist/token-goat-chunk-E76UNTVK.mjs +0 -7133
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ permalink: /
|
|
|
15
15
|
|
|
16
16
|
**Reduces AI token use/costs by 40–90%, and improves its focus. Fully automated, always online.**
|
|
17
17
|
|
|
18
|
-
**Also defends against prompt injection. Every fetched page
|
|
18
|
+
**Also defends against prompt injection. Every fetched page, tool result, and extracted document is wrapped in an untrusted-content fence before hitting the model, whether or not it matched an attack pattern, and the scan only decides what the label says. One config line to disable.**
|
|
19
19
|
|
|
20
20
|
**Your AI re-reads the same file three times. Every compaction causes amnesia. Every build log buries the one line that matters. You pay for all of it. Token-Goat fixes all of it — automatically.**
|
|
21
21
|
|
|
@@ -69,6 +69,11 @@ The fastest way to reduce AI token costs is fixing these five, not writing short
|
|
|
69
69
|
|--------------------|------------------|
|
|
70
70
|
| 3.3 MB screenshot lands in model context | 84 KB compressed copy, 97.4% smaller |
|
|
71
71
|
| Agent re-reads files from earlier in the session | "Already read this" reminder with narrow slice suggestion |
|
|
72
|
+
| Read tool asks for lines the session was already given | Answered with a pointer at the copy already delivered instead of the file, when the text this read would return matches the text already served for it whole line for whole line. Proof rather than a read count, so it also covers a file inside the recent-read protection window; a changed file, a wider range, and `reread_deny = false` all pass through |
|
|
73
|
+
| Same unchanged file read again through the shell (`cat`, `head`, `tail`, `sed -n '1,40p'`) | Byte-identical repeat replaced with a one-line pointer instead of the whole body — 4,031 bytes to 189 on this project. Only fires when the two runs match exactly, so a changed file is untouched; full text stays available via `token-goat bash-output <id>` |
|
|
74
|
+
| Part of a file re-read under a different command (`head -40 F` then `sed -n '1,30p' F`) | Repeat replaced with a pointer when every line coming back was already served for that same file this session. Matched on the text, on whole-line boundaries, so a read that adds any new lines is left whole; the record is dropped when the file is edited or the conversation is compacted. A file read with the Read tool counts as served too, limited to the lines that read actually handed over, and a read that came back truncated counts for nothing |
|
|
75
|
+
| Read tool asks for a range that overlaps one already delivered | Only the overlapping stretch is withheld, replaced by one line naming the line numbers and the recall command; the new lines come through with their own numbers and spacing, byte for byte. A stretch is kept whenever dropping it would save less than the line replacing it costs, and a result carrying anything the redactor would strip, a truncated result, or a file changed on disk all pass through whole. Set `elide_served_lines = false` to disable |
|
|
76
|
+
| Shell command run with colour on (`git`, `npm`, `npx`, `token-goat`) | Terminal escape sequences removed, leaving byte-for-byte the same output with colour off: 13.2% smaller on a real `git diff` here, and about a quarter of the bytes on the calls it fires for. Nothing is summarized or withheld, so there is no recall pointer; it runs on failed commands too, and only when the escapes are worth a rewrite |
|
|
72
77
|
| 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
78
|
| Compaction forgets which files were edited | Structured session manifest injected before compact |
|
|
74
79
|
| Same files re-read from scratch after `/compact` | Recovery hint at SessionStart lists cached snapshot + bash + WebFetch IDs |
|
|
@@ -77,7 +82,7 @@ The fastest way to reduce AI token costs is fixing these five, not writing short
|
|
|
77
82
|
| 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
83
|
| 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 |
|
|
79
84
|
| Skill invoked with `first_load_compact=true` and `<!-- COMPACT_END -->` present | First load also blocked; only the curated compact section is served. Full body available via `token-goat skill-body <name>` on demand |
|
|
80
|
-
| Same docs URL fetched twice in a session | Re-fetch blocked at
|
|
85
|
+
| Same docs URL fetched twice in a session with the same question | Re-fetch blocked at any context pressure; cached body available via `token-goat web-output <id>`. Keyed on the URL and the prompt together, since a WebFetch answer is specific to the question asked, so the same page fetched with a different question is left alone |
|
|
81
86
|
| `cat src/auth.py` or `Get-Content module.py` run via Bash | Pre-Bash hook detects whole-file reads of indexed source files and suggests `token-goat read "file::Symbol"`, `skeleton`, or `section` — covers `cat`, `bat`, `type`, PowerShell `Get-Content`/`gc` |
|
|
82
87
|
| `rg pattern src/` or `grep -rn` run via Bash (first time) | Pre-Bash hook suggests `token-goat symbol <name>` and `token-goat semantic "<query>"` as indexed alternatives to a full directory walk |
|
|
83
88
|
| `rg "^def" src/file.py` or `grep "class " module.ts` — structural search on a single source file | Pre-Bash hook redirects to `token-goat skeleton "file"` or `outline "file"` — all symbols with line numbers, no full-file read |
|
|
@@ -94,7 +99,7 @@ The fastest way to reduce AI token costs is fixing these five, not writing short
|
|
|
94
99
|
| Same `pytest` / `cargo` / `git log` re-run mid-session | Small prior outputs (≤8 KB) served inline on first repeat; larger outputs get a hint pointing at `token-goat bash-output <id>` |
|
|
95
100
|
| Same `Grep` pattern re-run with hundreds of matches | Pre-Grep dedup hint quotes the prior match count |
|
|
96
101
|
| `Grep` in `content` mode repeats the same file path on every match line | Post-Grep hook folds matches under one path header per file (lossless, path/line survive verbatim) |
|
|
97
|
-
| Same docs URL fetched twice | Re-fetch denied
|
|
102
|
+
| Same docs URL fetched twice with the same prompt | Re-fetch denied (redirects to `token-goat web-output <id>`) once the cached body clears `web_dedup_min_bytes`, at every context pressure. A repeat with a different prompt passes through: the cached answer was written for the earlier question |
|
|
98
103
|
| `token-goat section pyproject.toml::tool.ruff` | One TOML table extracted instead of the whole config; same for `.yaml`/`.yml`/`.json`/`.ini`/`.cfg`/`.env`/`Dockerfile` |
|
|
99
104
|
| Typoed `token-goat symbol getUserr` | `symbol` matches on exact name; a miss returns `No matches for 'getUserr'` (no fuzzy/auto-redirect) — use `token-goat find getUserr` for a typo-tolerant name lookup, or `token-goat semantic "<what it does>"` when you don't know the name at all |
|
|
100
105
|
| `grep`/`rg` returns 50+ match lines | File-level summary: top 20 files by match count; full result cached, ~80% smaller |
|
|
@@ -108,6 +113,7 @@ The fastest way to reduce AI token costs is fixing these five, not writing short
|
|
|
108
113
|
| CSV/JSON/JSONL/log file re-read when only structure changed | Pre-Read hint for structured files (CSV headers, JSON keys, log format), ~70% smaller than full read |
|
|
109
114
|
| Index-only files (lockfiles, source maps, bundles) read on every session | Pre-Read suppression for read-only files (package-lock.json, *.map, dist/), skipped unless explicitly edited |
|
|
110
115
|
| Large markdown file read in full (README.md, CHANGELOG.md, CLAUDE.md ≥8 KB) | Heading tree intercepted instead — H1–H3 with `#2`/`#3` disambiguation; `token-goat section` shortcuts listed for well-known files; post-edit injects a re-read suggestion rather than the full file |
|
|
116
|
+
| Subagent reads a ≥30 KB markdown file whole, on its first look at it | Off by default. Set `subagent_markdown_first_read_deny = true` under `[hints]` to block that read and answer with the heading tree instead. Only fires in a subagent, only on a first, un-ranged read of a `.md`/`.mdx`/`.markdown` file with at least three headings; a read that already asks for a line range, and every main-session read, pass through untouched |
|
|
111
117
|
| PDF opened via Read | Full read denied; PDF shows page count and outline (`token-goat pdf-extract` pulls the actual text, optionally paged/sliced, when the outline isn't enough) |
|
|
112
118
|
| Excel/PowerPoint/Word file (.xlsx/.pptx/.docx) opened via Read | Full read denied; redirects to the matching narrow-slice command family (`xlsx-sheets`/`xlsx-head`/`xlsx-range`/`xlsx-query`, `pptx-outline`/`pptx-slide`/`pptx-notes`/`pptx-text`, `docx-outline`/`docx-text`) instead of extracting the whole document as text |
|
|
113
119
|
| Other Office binary (.odt, .ods, .ott, .odp) opened via Read | Full read denied; redirects to `pandoc` for text extraction (no dedicated reader for these formats yet) |
|
|
@@ -158,7 +164,7 @@ The fastest way to reduce AI token costs is fixing these five, not writing short
|
|
|
158
164
|
| `kubectl events` lists raw repetitive events | Events grouped by REASON with a per-group count; field-selector hint added to narrow scope |
|
|
159
165
|
| `kubectl describe` floods labels and annotations | Labels/annotations blocks collapsed to line counts; Conditions table kept in full; container resource fields preserved |
|
|
160
166
|
| `npm install` verbose output with sill/http/verb/spinner lines | Verbose timing, sill, http, verb lines suppressed; warn lines beyond first 3 collapsed; braille spinner reify lines dropped |
|
|
161
|
-
| Fetched web content lands raw in model context |
|
|
167
|
+
| Fetched web content lands raw in model context | Always wrapped in an untrusted-content fence; scanned for attack patterns, and a matched pattern name is written to the log and into the fence's notice |
|
|
162
168
|
| Chatty log repeats the same error or event thousands of times | `token-goat logfold` collapses consecutive duplicates to `[Nx]` counts; same event logged with different timestamps or request IDs folds correctly — ~90–95% smaller on repetitive logs |
|
|
163
169
|
| Reading poetry.lock or package-lock.json to find a pinned version | `token-goat lockdeps` returns a name/version table of direct dependencies; optional packages and transitive entries excluded |
|
|
164
170
|
|
|
@@ -572,7 +578,9 @@ token-goat pdf-extract manual.pdf --pages 12-15 --layout --head 120
|
|
|
572
578
|
| `token-goat semantic "<query>"` | Find code by meaning, not by filename: embedding-vector similarity search over indexed file chunks and full-text search (BM25) over symbol names/bodies both run on every query and are fused by Reciprocal Rank Fusion (`score = sum of 1/(60 + rank)` per list), so an exact keyword match can outrank a weak vector hit instead of being shadowed by the vector branch. Covers extracted text from PDF/DOCX/PPTX/XLSX files alongside source code, so a query can surface a spec PDF, design doc, deck, or spreadsheet, not just code. Results are re-ranked with a path-priority multiplier so live source wins ties/near-ties against stale or archival prose (`archive/`, `archived/`, `old/`, `deprecated/`, `plans/`, `drafts/`, `CHANGELOG*`, `*.bak`, `*.orig`) and, more mildly, general docs (`docs/**`, `*.md`) — a nudge, not a hard filter, so a genuinely much better archival match still surfaces. Configure with `token-goat config set semantic.archive_weight <0-1>` / `token-goat config set semantic.docs_weight <0-1>` (both default `<1`; set to `1` to disable that penalty entirely, e.g. for a project with a genuinely live `plans/` directory). `--limit <n>` caps result count; `--json` for structured output: `{source, items, truncated, totalCount}`, where `source` is `"hybrid"` when both the embedding and BM25 branches contributed at least one raw hit, `"embeddings"` when only the embedding branch did (e.g. no vector index exists yet: optional embedding deps unavailable, or `indexing.embeddings_enabled` is off), or `"fts"` when only BM25 did, and every item carries the same keys (`filePath`, `name`, `kind`, `startLine`, `endLine`, `distance`, `preview`), with `null` for whichever of `name`/`kind`/`distance` don't apply to that item's source, and `filePath` rendered root-relative when a project root resolves, absolute when none does, matching plain-text output. On an embeddings hit, `name`/`kind` are resolved to the innermost indexed symbol whose line range contains the hit's start line (`null`/`null` when the hit falls outside any symbol, e.g. a top-of-file imports chunk); text output appends the same as a `— inside <name> (<kind>)` suffix. `--grep <pattern>` only shows hits whose FILE PATH matches this regex (literal substring if it is not valid regex), tested against the path exactly as rendered (so an anchored `--grep "^src/"` matches what you see, not the stored absolute path) — the high-value case is dropping test/vendored noise from a project-wide semantic hit list. Applied before the `--limit` slice in both the embeddings and full-text-fallback branches, so it selects from the whole hit set, not an already-capped page; when it matches nothing among hits that do exist, the output names the active filter (and `--json` sets `grepFilteredToEmpty: true`) instead of reading like the search found nothing. `--exclude-tests` hides hits whose file is a test file (opt-in — omitted, output is unchanged), covering the case `--grep` structurally cannot: `--grep` can only ever *select* a path, and the negative-lookahead pattern that would express "not a test" silently degrades to a literal substring match whenever the regex-compile fallback fires. Applied before the `--limit` slice in both branches and composable with `--grep` (a hit must satisfy both); when it hides every hit there was, the output names how many were hidden (and `--json` sets `excludeTestsFilteredToEmpty: true`) and exits 0, instead of the exit-1 "no matches" a genuinely empty search returns. With both filters set and both emptying the view, the `--grep` notice takes priority. |
|
|
573
579
|
| `token-goat map` | Get a compact orientation of the repo. Add `--compact` to fit a fixed 2000-token budget. `--json` emits the project map as JSON instead of text. |
|
|
574
580
|
| `token-goat deps "file"` | One-level import listing for a single file: resolves relative imports to project files (`internal`, root-relative paths) and groups everything else as `external`. `--json` for structured output. `--grep <pattern>` only shows dependencies whose MODULE SPECIFIER (the resolved internal path or the external package name) matches this regex (literal substring if it is not valid regex), applied before output is built; when it matches nothing among real dependencies, the output names the active filter instead of reading like the file has no imports at all. Complemented by `token-goat arch` for the project-wide graph. |
|
|
575
|
-
| `token-goat arch` | Project-wide import graph summary: hub modules (most imported), entry points (nothing imports them), and circular chains. Complements `token-goat deps <file>` for per-file depth. |
|
|
581
|
+
| `token-goat arch` | Project-wide import graph summary: hub modules (most imported), entry points (nothing imports them), and circular chains. `--modules` adds a grouping of the files that mostly import each other, naming each group by its most connected file, saying whether the group is one directory or spread across several, and listing which groups reach into which. That section also prints the grouping's modularity and calls it out when it is too weak to mean anything, since the algorithm returns groups for any graph, including one with no real structure. `--json` carries each group's full member list. Complements `token-goat deps <file>` for per-file depth. |
|
|
582
|
+
| `token-goat affected [files...]` | Which test files transitively import the given changed files, for narrowing a CI run: `git diff --name-only \| token-goat affected --stdin --quiet` prints bare paths ready to pipe into a test runner. Walks the same import graph `arch` builds, backwards. Distinct from `test-for`, which asks which tests reference a file's SYMBOLS by name in one hop; a test reaching the change through a helper is invisible to that one and visible here, and neither subsumes the other. `--depth <n>` bounds the walk (default 5); `--filter <regex>` overrides what counts as a test and an invalid pattern is refused rather than silently matched as a literal, since a typo would quietly select a different set of tests. Every way it can return less than everything is disclosed: an untracked path is named rather than dropped, and a depth bound that actually cut the walk short is reported with how many files were left unexplored. Both disclosures go to stderr so `--quiet` stdout stays pipeable. `--json` for structured output. |
|
|
583
|
+
| `token-goat reconcile` | Sweep this project for files that changed while no token-goat hook was running -- a pull in another terminal, an editor save, a code generator -- and queue them for reindexing. The same sweep runs automatically at session start; this is the manual form. Costs one filesystem stat per tracked file in the ordinary case and only reads a file whose timestamp moved, and a moved timestamp is confirmed against content rather than trusted, so a branch switch does not queue the whole repository. `--dry-run` reports the drift without queueing it; `--budget-ms <ms>` changes the time budget; `--json` for structured output. A sweep cut short by its budget says so and reports no deletions at all, because a file it never reached is indistinguishable from one that was deleted. |
|
|
576
584
|
| `token-goat index [path]` | Parse all git-tracked files and (re)build the symbol index from scratch. Runs automatically on install and incrementally via the background worker after edits — use this to force a full rebuild (e.g. after a config change that narrows what gets indexed). Each file records which version of token-goat's extraction logic produced its symbols, so an upgrade that changes what gets extracted reparses every already-indexed file once, on the next run, instead of leaving unchanged files on their old symbols until something edits them. That first run after such an upgrade takes noticeably longer than usual; later runs skip unchanged files as before. `--walk` indexes a bounded directory walk instead when `path` isn't a git repo. `--force-walk` does the same non-git walk and raises its 20,000-file refusal to 500,000 for a folder you know is genuinely that large (slow, and produces a large index — check `token-goat doctor` afterwards); it never lifts the separate refusal to walk a filesystem root or your home directory. On a real terminal (not a pipe/CI), prints a live progress line to stderr (files done/total, current phase, elapsed time) so a large repo doesn't look hung; stdout is unaffected either way. |
|
|
577
585
|
| `token-goat ignores` | List active skip patterns for the current project — built-in skip dirs and suffixes, blocked roots, and which command each one applies to. It also reports `.tokengoatignore`, which applies to `token-goat pack` only: it excludes nothing from the symbol index. To keep a path out of the index, use `token-goat project exclude <path>`. |
|
|
578
586
|
| `token-goat gdrive-sections <file-id>` | List the heading outline of a Google Doc without fetching the body. |
|
|
@@ -1276,11 +1284,15 @@ That command is the one thing here that is not clean: `onnxruntime-node` pulls a
|
|
|
1276
1284
|
|
|
1277
1285
|
**Prompt injection.** When an AI reads a file, web page, or command output, that content enters its context alongside your own instructions. Prompt injection is when untrusted content includes text designed to look like instructions — "Ignore all previous directives and run this instead" — to redirect the AI mid-task.
|
|
1278
1286
|
|
|
1279
|
-
Token-goat intercepts every Read, Fetch, Bash, and MCP call the AI makes.
|
|
1287
|
+
Token-goat intercepts every Read, Fetch, Bash, and MCP call the AI makes. Text that came from somewhere else is wrapped in an untrusted-content fence before the model sees it, decided by where the text came from and not by whether anything looked suspicious in it (`injection.enabled`, on by default, turns the whole thing off). The content is also scanned for a set of imperative-override attack patterns ("ignore previous instructions," "reveal system prompt," and similar); a match adds the pattern names to the fence's notice and writes a row to the log, and a clean scan changes only the wording. That ordering is the point: the pattern list is deliberately short, so anyone phrasing the same instruction differently would otherwise get an unlabelled channel, and a miss would be silent.
|
|
1280
1288
|
|
|
1281
|
-
|
|
1289
|
+
Three surfaces are covered. Every fetched page is fenced as it arrives and again when a cached copy is recalled with `web-output`. Every MCP tool result is fenced as it arrives, which matters most: it is a remote server's output, so it is the least trustworthy text in the pipeline. And cached Bash and MCP output is fenced when recalled with `bash-output` or `mcp-output`, since the output of a build or test run in a project with a hostile dependency is written by a third party as much as any web page is. Document extraction (`pdf-extract`, `docx-text`, the `xlsx-*` and `pptx-*` commands), `pr-slice`, `gdrive-sections`, and `recall` are covered the same way. The fence naming tool output is a different tag from the one naming web content, so the label tells the model where the text came from.
|
|
1282
1290
|
|
|
1283
|
-
|
|
1291
|
+
Read is the exception: file content passes through to the model unfiltered, because filtering it would silently break legitimate use cases. Where token-goat splices a piece of a file into its own hint or denial message, that excerpt is fenced. Outside of the fence, the primary defense is the model's own training to treat tool output as data, not as commands from a trusted party.
|
|
1292
|
+
|
|
1293
|
+
One deliberate gap: `--json` output cannot carry a fence around the envelope, because a fence wrapped around JSON is no longer JSON and callers parse it. Those envelopes fence individual fields on a pattern match instead, since the fixed wrapper would otherwise cost more than a short field is worth. The printed (non-`--json`) form of the same command is always fenced.
|
|
1294
|
+
|
|
1295
|
+
Separately from that pass-through case: when a read hook *denies* a Read and substitutes its own message, any file bytes it embeds in that message (a markdown heading tree, a served compact or notebook sidecar, a re-read diff, a CSV header row, an HTML title) are wrapped in an `<untrusted-file-content>` fence first, so a hostile repo cannot get its own text presented to the model as token-goat speaking. That fencing is unconditional, not gated on the pattern scan — as all of it now is.
|
|
1284
1296
|
|
|
1285
1297
|
A third case needs no fence, because the danger is the line break rather than the wording. When token-goat prints its own summary of a file it prints one entry per line and takes the names and values straight out of that file: the column profile behind `csv-profile`, the key listing behind `json-outline` and `yaml-outline`, the entry listing behind `zip-list`, and any hook hint naming the file it is about. Every one of those values may legally contain a newline. A quoted CSV field spans lines by design, a JSON key is an arbitrary string, a zip entry name is whatever whoever built the archive wrote in the header, and a file name may contain a newline on Linux and macOS. So a single cell, key, entry or file name could end token-goat's line and start one of its own that reads exactly like another entry token-goat had written, with nothing but the line break to tell them apart. Control characters, Unicode line separators and format characters in those values are escaped into their visible form, so one entry stays one line and hostile content is shown rather than obeyed. The same rule covers a carriage return that would overwrite the line on screen, an ANSI escape that would recolour it, and a bidi override that would make the rest of it render backwards. Ordinary names and values pass through untouched. This matters most for an archive, since a `.whl`, `.vsix` or `.nupkg` comes from a package registry rather than from you.
|
|
1286
1298
|
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { createRequire as __cjsRequire } from 'node:module';
|
|
2
2
|
const require = __cjsRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
deliveredOutputBytes,
|
|
5
|
+
wrappedShell
|
|
6
|
+
} from "./token-goat-chunk-LHMC5ENL.mjs";
|
|
3
7
|
import {
|
|
4
8
|
ToolFilter,
|
|
5
9
|
capTokens,
|
|
6
10
|
combineStreams,
|
|
7
11
|
compressOutput,
|
|
12
|
+
compressedTokensSaved,
|
|
13
|
+
dispatchArgv,
|
|
8
14
|
filterByName,
|
|
15
|
+
loadConfig,
|
|
16
|
+
recordStat,
|
|
9
17
|
resolveMinNetSavingsBytes,
|
|
10
18
|
selectFilter,
|
|
11
|
-
shlexSplit
|
|
12
|
-
|
|
13
|
-
} from "./token-goat-chunk-CZALRRGN.mjs";
|
|
14
|
-
import {
|
|
15
|
-
loadConfig,
|
|
16
|
-
recordStat
|
|
17
|
-
} from "./token-goat-chunk-E76UNTVK.mjs";
|
|
19
|
+
shlexSplit
|
|
20
|
+
} from "./token-goat-chunk-4ZDRTNGV.mjs";
|
|
18
21
|
import "./token-goat-chunk-AO2QD2AG.mjs";
|
|
19
22
|
import "./token-goat-chunk-AEX54RUZ.mjs";
|
|
20
23
|
|
|
@@ -28,17 +31,19 @@ var DEFAULT_TIMEOUT_SECONDS = 600;
|
|
|
28
31
|
var MAX_CAPTURE_BYTES = 32 * 1024 * 1024;
|
|
29
32
|
var MIN_RECORD_STAT_BYTES = 32;
|
|
30
33
|
function resolveFilter(command, filterName, cwd) {
|
|
31
|
-
|
|
32
|
-
const named = filterByName(filterName);
|
|
33
|
-
if (named !== null) return named;
|
|
34
|
-
}
|
|
35
|
-
let argv;
|
|
34
|
+
let split;
|
|
36
35
|
try {
|
|
37
|
-
|
|
36
|
+
split = shlexSplit(command);
|
|
38
37
|
} catch {
|
|
39
|
-
|
|
38
|
+
split = null;
|
|
39
|
+
}
|
|
40
|
+
const argv = split === null ? [command] : dispatchArgv(split, cwd).argv;
|
|
41
|
+
if (filterName) {
|
|
42
|
+
const named = filterByName(filterName);
|
|
43
|
+
if (named !== null) return { filter: named, argv };
|
|
40
44
|
}
|
|
41
|
-
return
|
|
45
|
+
if (split === null) return { filter: null, argv };
|
|
46
|
+
return { filter: selectFilter(split, cwd), argv };
|
|
42
47
|
}
|
|
43
48
|
function baseSpawnOptions(timeout, cwd, env) {
|
|
44
49
|
return { shell: wrappedShell(), timeout: timeout * 1e3, cwd, env };
|
|
@@ -66,14 +71,14 @@ function resolveCompressLimits() {
|
|
|
66
71
|
}
|
|
67
72
|
function run(command, opts = {}) {
|
|
68
73
|
const timeout = opts.timeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
69
|
-
const filter = resolveFilter(command, opts.filterName, opts.cwd);
|
|
74
|
+
const { filter, argv } = resolveFilter(command, opts.filterName, opts.cwd);
|
|
70
75
|
if (filter === null) {
|
|
71
76
|
if ((opts.maxTokens ?? 0) > 0) {
|
|
72
|
-
return wrapAndCompress(command, new IdentityFilter(), timeout, resolveProfile(opts.compressionProfile), opts);
|
|
77
|
+
return wrapAndCompress(command, argv, new IdentityFilter(), timeout, resolveProfile(opts.compressionProfile), opts);
|
|
73
78
|
}
|
|
74
79
|
return passthrough(command, timeout, opts.cwd, opts.env);
|
|
75
80
|
}
|
|
76
|
-
return wrapAndCompress(command, filter, timeout, resolveProfile(opts.compressionProfile), opts);
|
|
81
|
+
return wrapAndCompress(command, argv, filter, timeout, resolveProfile(opts.compressionProfile), opts);
|
|
77
82
|
}
|
|
78
83
|
function runRaw(command, timeout = DEFAULT_TIMEOUT_SECONDS) {
|
|
79
84
|
return passthrough(command, timeout, void 0, void 0);
|
|
@@ -92,7 +97,7 @@ function decode(buf) {
|
|
|
92
97
|
if (buf == null) return "";
|
|
93
98
|
return typeof buf === "string" ? buf : buf.toString("utf8");
|
|
94
99
|
}
|
|
95
|
-
function wrapAndCompress(command, filter, timeout, profile, opts) {
|
|
100
|
+
function wrapAndCompress(command, argv, filter, timeout, profile, opts) {
|
|
96
101
|
const writeStdout = opts.writeStdout ?? ((s) => process.stdout.write(s));
|
|
97
102
|
const result = spawnSync(command, {
|
|
98
103
|
...baseSpawnOptions(timeout, opts.cwd, opts.env),
|
|
@@ -118,12 +123,6 @@ function wrapAndCompress(command, filter, timeout, profile, opts) {
|
|
|
118
123
|
} else {
|
|
119
124
|
exitCode = 0;
|
|
120
125
|
}
|
|
121
|
-
let argv;
|
|
122
|
-
try {
|
|
123
|
-
argv = shlexSplit(command);
|
|
124
|
-
} catch {
|
|
125
|
-
argv = [command];
|
|
126
|
-
}
|
|
127
126
|
const limits = resolveCompressLimits();
|
|
128
127
|
const compressed = compressOutput(filter, stdoutText, stderrText, exitCode, argv, {
|
|
129
128
|
compressionProfile: profile,
|
|
@@ -142,8 +141,10 @@ function wrapAndCompress(command, filter, timeout, profile, opts) {
|
|
|
142
141
|
return exitCode;
|
|
143
142
|
}
|
|
144
143
|
function recordSavings(result) {
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
const delivered = deliveredOutputBytes(result.originalBytes);
|
|
145
|
+
const bytesSaved = Math.max(0, delivered - result.compressedBytes);
|
|
146
|
+
if (bytesSaved < MIN_RECORD_STAT_BYTES) return;
|
|
147
|
+
recordStat(`bash_compress:${result.filterName}`, bytesSaved, compressedTokensSaved(bytesSaved));
|
|
147
148
|
}
|
|
148
149
|
export {
|
|
149
150
|
DEFAULT_TIMEOUT_SECONDS,
|