token-goat 2.6.30 → 2.6.31

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.
Files changed (33) hide show
  1. package/README.md +54 -15
  2. package/SECURITY.md +72 -0
  3. package/dist/token-goat-chunk-4WGVGN6W.mjs +54 -0
  4. package/dist/token-goat-chunk-56C3XUQY.mjs +156 -0
  5. package/dist/token-goat-chunk-DCGBS2YZ.mjs +112 -0
  6. package/dist/token-goat-chunk-HPY7XE77.mjs +18311 -0
  7. package/dist/token-goat-chunk-J6JTYE2B.mjs +24 -0
  8. package/dist/token-goat-chunk-N2S7UAVU.mjs +13674 -0
  9. package/dist/token-goat-chunk-OIBTYBVR.mjs +901 -0
  10. package/dist/token-goat-chunk-P7I6LS5H.mjs +9643 -0
  11. package/dist/token-goat-chunk-PWD5XZ6C.mjs +9441 -0
  12. package/dist/token-goat-chunk-Q4N6SQWR.mjs +14860 -0
  13. package/dist/token-goat-chunk-QL2743EW.mjs +707 -0
  14. package/dist/token-goat-chunk-VYC34TBW.mjs +33 -0
  15. package/dist/token-goat-chunk-X7PIWZMB.mjs +5337 -0
  16. package/dist/token-goat-chunk-XEJBPPD7.mjs +15545 -0
  17. package/dist/token-goat-hook-chunk-2K7FV2ME.mjs +9438 -0
  18. package/dist/token-goat-hook-chunk-3OJELLK5.mjs +156 -0
  19. package/dist/{token-goat-hook-chunk-ZRJUZZQE.mjs → token-goat-hook-chunk-5AQYD2SZ.mjs} +1 -2
  20. package/dist/token-goat-hook-chunk-66W63A4Y.mjs +9643 -0
  21. package/dist/token-goat-hook-chunk-JT5RMRIY.mjs +901 -0
  22. package/dist/token-goat-hook-chunk-JVJUBWKT.mjs +13674 -0
  23. package/dist/{token-goat-hook-chunk-FMQ37OQD.mjs → token-goat-hook-chunk-K6HLYRRI.mjs} +25 -23
  24. package/dist/token-goat-hook-chunk-NIVRFAJ4.mjs +5323 -0
  25. package/dist/token-goat-hook-chunk-SJM4BUZQ.mjs +18309 -0
  26. package/dist/token-goat-hook-chunk-ULGSXGZK.mjs +23 -0
  27. package/dist/{token-goat-hook-chunk-XNZLMFQ2.mjs → token-goat-hook-chunk-YHRJMMEF.mjs} +2629 -801
  28. package/dist/token-goat-hook.mjs +5 -2
  29. package/dist/token-goat.core.mjs +14 -89574
  30. package/package.json +5 -4
  31. package/dist/token-goat-hook-chunk-4SM2PAR6.mjs +0 -33590
  32. package/dist/token-goat-hook-chunk-5B3OWRNC.mjs +0 -145
  33. package/dist/token-goat-hook-chunk-VKAQNN2R.mjs +0 -24416
package/README.md CHANGED
@@ -39,6 +39,12 @@ Restart your AI sessions. Run `token-goat stats` a couple of minutes after your
39
39
 
40
40
  ---
41
41
 
42
+ <p align="center">
43
+ <img src="assets/token-goat-comparison.jpg" alt="Side-by-side comparison: a bloated workflow sends whole files and grows context every round, while token-goat sends only the needed lines and stays lean" width="900">
44
+ <br>
45
+ <sub>Same requirements, smarter input: fewer input tokens, shorter answers, and context that stops compounding across rounds</sub>
46
+ </p>
47
+
42
48
  <p align="center">
43
49
  <img src="assets/stats_v180.png" alt="token-goat stats display" width="589">
44
50
  <br>
@@ -137,11 +143,10 @@ The fastest way to reduce AI token costs is fixing these five, not writing short
137
143
  | Recovery hints omit critical paths when space is tight | Skip bash snippet when recall available |
138
144
  | AVIF format not supported despite better compression | AVIF image-shrink via sharp (when libvips is built with libaom); WebP fallback; codec auto-detection in docker |
139
145
  | Token-savings invisible until you run `stats` | Token-savings benchmark (slow-marked test suite) locks in measured wins; `token-goat stats` reports net-positive impact |
140
- | Hook crash leaves agent waiting for response | Fail-soft barrier catches `BaseException`/`MemoryError`/`SystemExit`; hook always returns `{"continue": true}` |
146
+ | Hook crash leaves agent waiting for response | Every way the shim can fail prints `{}` and exits 0, leaving the tool call to proceed untouched: an event name it does not know, stdin it cannot read or parse, an in-process load that throws, a token-goat child that exits non-zero or prints nothing, and a catch around the whole run |
141
147
  | Concurrent edits lose update counts mid-session | Session CAS + mtime-based retry prevent lost edits in manifest |
142
- | Dirty queue appends corrupt on concurrent writes | OS file lock (fcntl/msvcrt) prevents torn JSON lines |
148
+ | Dirty queue appends corrupt on concurrent writes | Each entry is one `O_APPEND` line, and an append that finds the file not ending in a newline starts with one, so a line torn by an earlier crash cannot merge with the next path into a single garbage entry |
143
149
  | Worker claim file blocks all re-spawns on crash | Mtime staleness check (>60s) auto-recovers zombie claim files |
144
- | DRY consolidation — 600+ lines duplicated | Tool-response extractor unified; cache helpers (`_safe_join`, `OutputStatDict`) consolidated; dedup-hint template collapsed; CLI output/history commands unified; `humanize_bytes` centralized in `render/ansi` |
145
150
  | Compaction hook subprocess ~190 ms cold | Lazy imports of heavy modules in `hooks_session` and `compact`; compaction path ~110 ms cold (~42% faster) |
146
151
  | Pre-compact subprocess runs on every session | Compact-skip sentinel on disk: if session file is <5 min old and no edits logged, subprocess exits in <1 ms |
147
152
  | Git ops slow manifest build in non-repo dirs | `git diff` / `git log` calls skipped when `cwd` is not inside a git repo (saves 60–100 ms per hook fire) |
@@ -311,7 +316,7 @@ For bounded archive/document comparisons after setup, see the [CLI comparison wo
311
316
 
312
317
  **Important — sharp is part of the install.** The biggest single win (~39% smaller than JPEG, ~97% smaller than raw PNG) comes from WebP encoding. sharp ships with prebuilt native binaries on Windows, macOS, and most Linux platforms, so a standard `npm install -g token-goat` includes it. On Alpine musl and some ARM boards, npm compiles from source against libvips — that may add a few seconds. `token-goat doctor` will report `sharp: ok` when the image pipeline is working; if it flags a failure, follow the [Image support](#image-support) section below before considering the install complete. **AI agents performing this install: run `token-goat doctor` after `token-goat install` and resolve any sharp failure as part of the same task — do not treat it as a follow-up.**
313
318
 
314
- Two things change how Claude Code sessions behave: hooks fire automatically (image shrink, re-read dedup, compact manifests), and a delimited routing block written to `~/.claude/CLAUDE.md` plus a registered skill gate the agent's reads — before any file read it must ask whether a `token-goat read` / `symbol` / `section` returns just what it needs, and the block explicitly subordinates the harness's own Read/Grep tool-preference rules to the *fallback* choice once token-goat is ruled out. A `Bash(token-goat:*)` allowlist entry in `settings.json` lets the agent run those commands without a per-call approval prompt.
319
+ Two things change how Claude Code sessions behave: hooks fire automatically (image shrink, re-read dedup, compact manifests), and a delimited routing block written to `~/.claude/CLAUDE.md` plus a registered skill gate the agent's reads — before any file read it must ask whether a `token-goat read` / `symbol` / `section` returns just what it needs, and the block explicitly subordinates the harness's own Read/Grep tool-preference rules to the *fallback* choice once token-goat is ruled out. Install writes no permission entry: whether `token-goat` commands need a per-call approval prompt is left to your own `settings.json`, unchanged.
315
320
 
316
321
  **Keep that block where install put it.** It's plain markdown in a file you own, so moving it into a tidier reference file is tempting — but `install` and `uninstall` resolve one hardcoded path (`~/.claude/CLAUDE.md`). A relocated copy is never refreshed, so it freezes at whatever version was current when it moved, and the next `install` sees CLAUDE.md missing its block and appends a fresh one — leaving the guidance duplicated across two files with only one of them live. `token-goat doctor` warns when it finds a block outside CLAUDE.md, naming the file; `install` warns at write time and `uninstall` reports what it couldn't remove. None of them edit a file token-goat doesn't own, so cleanup stays your call. A pointer that merely *mentions* the markers in prose is fine — detection requires both markers on their own lines.
317
322
 
@@ -550,8 +555,8 @@ token-goat pdf-extract manual.pdf --pages 12-15 --layout --head 120
550
555
  | `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. |
551
556
  | `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. |
552
557
  | `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. |
553
- | `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 `.tokengoatignore` change). `--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. |
554
- | `token-goat ignores` | List active skip patterns for the current project — built-in skip dirs and suffixes, plus any patterns from `.tokengoatignore`. |
558
+ | `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). `--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. |
559
+ | `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>`. |
555
560
  | `token-goat gdrive-sections <file-id>` | List the heading outline of a Google Doc without fetching the body. |
556
561
  | `token-goat stats` | See locally estimated savings: total events / bytes saved / tokens saved. Add `--full` for the per-source, per-command, and per-day breakdown, or `--methodology` to explain estimates and their limits. These values are not GitHub Copilot usage or billing data. |
557
562
  | `token-goat cost [--session]` | Estimated tokens saved, session or all-time, broken down by savings source. |
@@ -582,7 +587,7 @@ token-goat pdf-extract manual.pdf --pages 12-15 --layout --head 120
582
587
  | `token-goat skill-diff "<name>"` | Unified diff between the two most recent cached versions of a skill — tracks skill updates across sessions. |
583
588
  | `token-goat compact-hint --session-id <id>` | Inspect the compaction manifest for a session. Add `--trigger auto` to preview the pressure-aware budget the live PreCompact hook would use. |
584
589
  | `token-goat resume <session_id>` | Emit a single post-compact recovery packet — top skills, last two Bash outputs, top edited-file diffs, and `git diff --stat`, capped at ~2000 tokens. Replaces 5-10 round-trips. |
585
- | `token-goat config list / get / set / validate` | Inspect or edit `config.toml` from the CLI. `validate` reports unknown keys with did-you-mean suggestions, plus any project-file or environment value that validation rejected or clamped. A project-root `.token-goat.toml` layers on top of the global config, overriding hint thresholds, indexing settings, etc. for that project only; `config get`/`list`/`set` report which layer a value actually resolved from — the project file, an environment variable, or the global config — and where that layer's value was clamped or rejected they say so, naming what was asked for and what is in effect instead. |
590
+ | `token-goat config list / get / set / validate` | Inspect or edit `config.toml` from the CLI. `validate` reports unknown keys with did-you-mean suggestions, plus any project-file or environment value that validation rejected or clamped. A project-root `.token-goat.toml` layers on top of the global config, overriding hint thresholds, indexing settings, etc. for that project only. It may not set the security sections `injection`, `webfetch`, `gdrive`, or `mcp`, nor `indexing.cross_project_symbols`: that file arrives with the repository, so a cloned project could otherwise switch off prompt-injection fencing or empty the fetch allow list for anyone who opened it. Those settings come from the global config or the environment only, and a project file that tries to set one is ignored with a message naming what was dropped. `config get`/`list`/`set` report which layer a value actually resolved from — the project file, an environment variable, or the global config — and where that layer's value was clamped or rejected they say so, naming what was asked for and what is in effect instead. |
586
591
  | `token-goat config-get <file> <key>` | Look up one key from a config-shaped file (TOML/INI `key = value`, or YAML) without reading the whole thing. On a `.md` file, a leading `---`-fenced YAML frontmatter block (Jekyll/Hugo/SKILL.md style) is checked first and takes precedence over the TOML/INI fallback; a `.md` file with no frontmatter, or an unclosed fence, falls through to the normal lookup unchanged. |
587
592
  | `token-goat pdf-extract <file>` | Extract plain text from a PDF instead of a raw Read. `--pages <spec>` narrows to a page range (e.g. `1-5` or `3`); `--head`/`--tail`/`--grep`/`--max-matches`/`--section` slice the extracted text the same way `bash-output`/`web-output` do. `--layout` heuristically reconstructs column-aware reading order from text-item coordinates instead of raw content-stream order (imperfect on rotated/overlapping text). |
588
593
  | `token-goat pdf-outline <file>` | List a PDF's bookmark/outline tree with page numbers instead of a raw Read. |
@@ -635,7 +640,7 @@ token-goat pdf-extract manual.pdf --pages 12-15 --layout --head 120
635
640
  | `token-goat project exclude <path>` | Add a project root to the blocklist so the worker never indexes it. Writes the resolved absolute path to `[worker] blocked_roots` in `config.toml`; idempotent. Remove the entry from the config to re-enable indexing. |
636
641
  | `token-goat project prune [--dry-run]` | Remove blocked/excluded roots that no longer exist on disk. `--dry-run` previews removals without touching the config file. Useful after deleting or moving projects. |
637
642
  | `token-goat install` | Wire up hooks (and, with the harness flags below, other AI tool integrations). No `--dry-run` or `--verify` flag — run `token-goat doctor` after install to audit the result. |
638
- | `token-goat doctor` | Confirm everything is wired correctly. Surfaces install state, cold-import timing, cache hit rates, compaction-budget telemetry, opt-in flag status, and canonical-root sanity. It read-only audits `~/.copilot/mcp-config.json` for globally configured Chrome DevTools or Playwright `npx` launchers, recommending project scope or removal when inactive; it never prints server configuration or secrets. On Windows, it also reports duplicate Chrome DevTools/Playwright MCP launchers and orphaned Node processes without terminating anything. Pass `--context` to show the **Context footprint** section: a fill bar with severity (ok / warn / high / URGENT), per-component breakdown (skills catalog, loaded skill bodies, CLAUDE.md+MEMORY.md, conversation estimate), session-to-session growth trend with sessions-to-URGENT projection, and tiered compaction recommendations (Tier 0–4) naming the exact commands to run. Auto-shown when fill > 40 % or any loaded skill > 2 K tokens lacks a compact. `--json` emits the check results (one entry per check, with `ok`/`warn`/`fail` status) as JSON instead of text. |
643
+ | `token-goat doctor` | Confirm everything is wired correctly. Surfaces install state, cold-import timing, cache hit rates, compaction-budget telemetry, opt-in flag status, and canonical-root sanity. A **Security** section reports the posture in one place: whether offline mode is on, whether injection scanning is on, whether the Google Drive integration is enabled, whether fetching runs against an allow list or a deny list, whether MCP reads are confined to the project root, and whether the data directory is readable by other local users. It only warns when a protection that ships on has been switched off, so a default install stays quiet. It read-only audits `~/.copilot/mcp-config.json` for globally configured Chrome DevTools or Playwright `npx` launchers, recommending project scope or removal when inactive; it never prints server configuration or secrets. On Windows, it also reports duplicate Chrome DevTools/Playwright MCP launchers and orphaned Node processes without terminating anything. Pass `--context` to show the **Context footprint** section: a fill bar with severity (ok / warn / high / URGENT), per-component breakdown (skills catalog, loaded skill bodies, CLAUDE.md+MEMORY.md, conversation estimate), session-to-session growth trend with sessions-to-URGENT projection, and tiered compaction recommendations (Tier 0–4) naming the exact commands to run. Auto-shown when fill > 40 % or any loaded skill > 2 K tokens lacks a compact. `--json` emits the check results (one entry per check, with `ok`/`warn`/`fail` status) as JSON instead of text. |
639
644
  | `token-goat baseline` | Emit a project map: file count, per-language file counts, the top indexed symbols (by name/kind/location), and the most recently modified files. `--subagent` emits a terser variant (fewer symbols, fewer recent files) for context handed to a freshly spawned subagent; `--json` for the machine-readable form. |
640
645
  | `token-goat compact-doc <path>` | Build an extractive compact sidecar for a large reference doc (`.md`/`.markdown`). The compact is stored in the token-goat data dir as a SHA-keyed sidecar; `pre_read` serves it in place of the full file when it exists and is fresh, saving 80–95% of context tokens. Use `--force` to rebuild, `--sentences N` to control lines per section (default 2), `--show` to print the result. The sidecar is automatically marked stale when you edit the source file. Config: `[hints] stable_doc_compacts = true` (default on). |
641
646
 
@@ -883,13 +888,13 @@ never drift on where `mcp.json` lives or what key name it looks for.
883
888
 
884
889
  ## What gets installed?
885
890
 
886
- `token-goat install` writes the following on your machine — nothing else, anywhere. Every entry is reversed by `token-goat uninstall`. Run `token-goat doctor` at any time to see which of these are currently present.
891
+ `token-goat install` writes the following on your machine — nothing else, anywhere. Every entry is reversed by `token-goat uninstall`. Integrations for other harnesses are additive on the way out as well as in, so a plain uninstall does not touch one you installed with `--codex`, `--copilot`, or a sibling flag: rather than undo something you did not ask about, it names each one still present and the flag that removes it. Run `token-goat doctor` at any time to see which of these are currently present.
887
892
 
888
893
  **Claude Code integration** (`~/.claude/`)
889
894
 
890
895
  | Path | What |
891
896
  |------|------|
892
- | `~/.claude/settings.json` | Hook entries for `SessionStart`, `PreToolUse` (Read/Grep/Bash, Drive/WebFetch), `PostToolUse` (Edit/Write/MultiEdit, Read/Grep/Glob, Bash, WebFetch, Skill), and `PreCompact`. Plus a `Bash(token-goat:*)` permission allowlist entry. Existing hooks are preserved; a timestamped `.bak` is written before any change.<br><br>The `PreToolUse` and `PostToolUse` matchers are narrowed to exactly the tools token-goat handles (plus `^mcp__`), generated from the live hook registry rather than a fixed list, so they can't fall out of date as handlers change. Claude Code starts a new process per matcher hit and most of that cost is process startup, so a catch-all matcher would make every unrelated tool call — `TodoWrite`, `TaskUpdate`, and friends — pay for a hook that has nothing to do. |
897
+ | `~/.claude/settings.json` | Hook entries for `SessionStart`, `PreToolUse` (Read/Grep/Bash, Drive/WebFetch), `PostToolUse` (Edit/Write/MultiEdit, Read/Grep/Glob, Bash, WebFetch, Skill), and `PreCompact`. Hook entries only: install writes nothing under `permissions`, so it never grants the agent unprompted execution of anything. Existing hooks are preserved; a timestamped `.bak` is written before any change.<br><br>The `PreToolUse` and `PostToolUse` matchers are narrowed to exactly the tools token-goat handles (plus `^mcp__`), generated from the live hook registry rather than a fixed list, so they can't fall out of date as handlers change. Claude Code starts a new process per matcher hit and most of that cost is process startup, so a catch-all matcher would make every unrelated tool call — `TodoWrite`, `TaskUpdate`, and friends — pay for a hook that has nothing to do. |
893
898
  | `~/.claude/hooks/token-goat-shim.js` | The hook script those `settings.json` commands invoke (`"<node>" "<shim>" <event> "<entry>"`). It imports the hook library in-process instead of spawning a second process, and naming the node binary directly skips the npm bin wrapper — on Windows a `cmd.exe` layer every hook would otherwise pay for. Measured 480 ms → 324 ms per hook call. Regenerated on every `install` run. Always written here even for a `--project` install, since the command bakes in machine-specific absolute paths; a project-scope `settings.json` just points at this one. |
894
899
  | `~/.claude/CLAUDE.md` | A delimited block (`<!-- token-goat-begin -->` … `<!-- token-goat-end -->`) telling the agent to prefer `token-goat read` / `symbol` / `section` over `Read` / `Grep`. Any existing content is preserved. |
895
900
  | `~/.claude/skills/token-goat/SKILL.md` | The token-goat skill — the same routing guidance in skill form. |
@@ -908,6 +913,10 @@ There is no auto-update mechanism. Updating token-goat is always a manual `npm i
908
913
 
909
914
  Contains the symbol index (`global.db`, per-project `.db` files), session cache, shrunken-image cache, cached skill bodies (5 MB cap, LRU-evicted), logs, locks, and the dirty-file queue. Nothing outside this directory and `~/.claude/` is written.
910
915
 
916
+ **What the index actually holds, in plain terms.** The point of a surgical read is returning a function body without the file around it, which means the database stores those bodies. `symbols.body` holds the source text of every indexed symbol, `symbols.docstring` its doc comment, `refs.context` the line around each reference, and `chunks.text` the passages that semantic search embeds. There is also a full-text index over the bodies and docstrings. So the database is not a list of names and line numbers: it is a substantial copy of your source, sitting in a plain unencrypted SQLite file outside the repository, at the path in the table above.
917
+
918
+ Three things follow, and they are worth knowing before you decide. It never leaves the machine: token-goat sends no telemetry of any kind, and the only outbound requests it makes at all are the ones listed in the security section, none of which carry index content. It is not protected by your repository's access controls any more, so anything on the machine that can read your home directory can read it, and on Linux and macOS that directory sits under a home that backup and sync tools routinely copy. And it outlives an uninstall unless you say otherwise: `token-goat uninstall --purge` deletes both roots and tells you how much it reclaimed.
919
+
911
920
  **With `--codex`** (Codex CLI integration)
912
921
 
913
922
  | Path | What |
@@ -1194,20 +1203,41 @@ Outbound network is reserved to these explicit cases:
1194
1203
  - Image fetches from URLs: either explicit via `token-goat fetch-image <url>`, or when the AI agent issues a WebFetch call that returns image content — the hook intercepts and shrinks the image. The URL always originates from the agent's work, not from token-goat itself.
1195
1204
  - `token-goat screenshot <url>` navigates a headless browser to the URL you give it, subject to the target restrictions described below.
1196
1205
  - The first `token-goat semantic` run on a machine downloads the embedding model from `huggingface.co`, pinned to an immutable commit rather than a mutable branch. Subsequent runs use the local cache and make no network call. Skip the download entirely by setting `indexing.embeddings_enabled = false` (it is on by default), in which case `semantic` falls back to full-text search.
1206
+ - The first optical-character read of an image downloads the English language data (about 4 MB) from `cdn.jsdelivr.net`, at a fixed version path. Subsequent reads use the local cache. This happens for an explicit `token-goat image-text`, and also for the automatic text extraction the image-shrink hook performs when the agent reads a screenshot; turn the automatic one off with `image_shrink.ocr_enabled = false`.
1207
+
1208
+ **One switch for all of it.** Set `network.offline = true` (env `TOKEN_GOAT_OFFLINE`) and every one of the paths above refuses instead of connecting, saying so rather than failing quietly. Anything already cached keeps working: a machine that has the embedding model still runs `semantic`, and one that has the language data still reads text out of images. This is one of the settings a per-project config file may not touch, so cloning a repository cannot switch it back off.
1209
+
1210
+ **A repository cannot reconfigure the security controls.** A project-root `.token-goat.toml` layers on top of your global config, which is what it is for: hint thresholds, indexing settings, compression tuning. But that file arrives with the repository, so whoever wrote the repository wrote it. Five whole sections are therefore off limits to it, plus one individual key, and come from your global config or the environment only: `injection` (prompt-injection fencing), `webfetch` (the fetch allow and deny lists), `gdrive` (the Google Drive integration), `mcp` (root confinement and the allowed-roots list), `network` (offline mode), and the single key `indexing.cross_project_symbols`. A project file that sets one of them is ignored, and token-goat prints a line naming what it dropped. Everything else stays project-overridable.
1197
1211
 
1198
1212
  **Security reports.** See [SECURITY.md](SECURITY.md). Email `token-goat@dfkhelper.com`; do not file as a GitHub issue. Reports are acknowledged within 7 days; coordinated disclosure with a 90-day default window.
1199
1213
 
1214
+ **Dependency advisories.** `npm audit` on the published package is not empty. The residual findings all trace to three packages that have no forward patch, two of which are optional and can be left out with `npm install --omit=optional`. Each one is named, with why it does or does not reach you, under [Dependency advisories](SECURITY.md#dependency-advisories).
1215
+
1216
+ **Verifying what you installed.** Every published version is built and pushed by one pinned workflow when a GitHub release is published, with npm provenance, so `npm audit signatures` verifies the tarball against the commit that produced it. Details in [Verifying what you installed](SECURITY.md#verifying-what-you-installed).
1217
+
1200
1218
  **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.
1201
1219
 
1202
- Token-goat intercepts every Read, Fetch, and Bash call the AI makes. For Read and Bash, it does not filter or sanitize content on the way through to the model; doing so would silently break legitimate use cases. WebFetch is the one exception: every fetched page is scanned for a set of imperative-override attack patterns ("ignore previous instructions," "reveal system prompt," and similar), and a match is wrapped in an untrusted-content fence rather than passed through untouched (`injection.enabled`, on by default). Outside of that scan, the primary defense is the model's own training to treat tool output as data, not as commands from a trusted party.
1220
+ Token-goat intercepts every Read, Fetch, Bash, and MCP call the AI makes. Content is scanned for a set of imperative-override attack patterns ("ignore previous instructions," "reveal system prompt," and similar), and a match is wrapped in an untrusted-content fence rather than passed through untouched (`injection.enabled`, on by default). Three surfaces are covered. Every fetched page is scanned as it arrives and again when a cached copy is recalled with `web-output`. Every MCP tool result is scanned 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 scanned 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. 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.
1221
+
1222
+ 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 unconditionally. A fence only appears on a positive match, so ordinary output is unchanged. Outside of the scan, the primary defense is the model's own training to treat tool output as data, not as commands from a trusted party.
1203
1223
 
1204
1224
  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.
1205
1225
 
1206
- 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`) if you genuinely need cross-root reads from an MCP client. 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.
1226
+ 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.
1207
1227
 
1208
1228
  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.
1209
1229
 
1210
- **Secret redaction in cached content.** Token-goat caches command output, fetched pages, and MCP results so it can serve them back later instead of re-running the work. Anything it writes to those caches is passed through a redactor first, so a credential that appeared in output does not sit on disk in plain text and does not get replayed into a later session. This is unconditional — there is no flag to turn it on, and it applies to cached Bash and Task output (including the command string itself, which is where an inline `--token=...` would otherwise land), fetched web content, MCP tool results and their labels, `compress-text`/`handoff` payloads, and the raw JSON disk cache. Recognized shapes: Anthropic, OpenAI, AWS, GitHub, Slack, Stripe, npm, and Google keys; JWTs; `Authorization: Bearer`/`Basic` headers; PEM private-key blocks; and generic `password=`/`secret=`/`api_key=` assignments in `.env`, connection-string, and query-string shape. A match is replaced by a `[REDACTED:<kind>]` marker naming which pattern fired.
1230
+ **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.
1231
+
1232
+ **Where cached content lives, and who can read it.** Cached command output, fetched pages, MCP results, session state and the source index all sit under one data directory (`~/.local/share/token-goat` on Linux, `~/Library/Application Support/token-goat` on macOS, `%LOCALAPPDATA%\dfk-helper\token-goat` on Windows). On POSIX that directory is created owner-only (mode 0700), and an existing one is tightened on the next run, so other local users on a shared build host cannot read it. Windows uses inherited ACLs instead. Individual JSON blobs are additionally written 0600.
1233
+
1234
+ **Confining `symbol` to one project.** `token-goat symbol` is the one read command that answers from the machine-wide index (`global.db`) rather than the current project, so by default `symbol <name>` and `symbol --grep .` return matching symbols, bodies included, from every project ever indexed on the host. That is deliberate and useful on a personal machine: it is how you find a helper you wrote in another repo. On a shared build host, or under an agent you have confined to one directory, it is a read channel that the directory sandbox does not close, because the answer comes out of the index instead of the filesystem. Set `indexing.cross_project_symbols = false` (env `TOKEN_GOAT_CROSS_PROJECT_SYMBOLS`) and `symbol` only answers from the project it is run in. `--project` and `--file` pointing outside that project are refused rather than honored, so the setting cannot be stepped around from inside the confined process. Every other read command (`read`, `refs`, `callers`, `types`, `dead`, `find`, `semantic`) is already project-scoped and is unaffected.
1235
+
1236
+ **Turning off the Google Drive integration.** `token-goat gdrive-sections` is the only feature that talks to Google. Set `gdrive.enabled = false` (env `TOKEN_GOAT_GDRIVE_ENABLED`) and the command refuses before it opens a connection, and the routing guidance token-goat writes into CLAUDE.md, AGENTS.md, `copilot-instructions.md` and the installed skill stops naming it, so an agent is never told the command exists. Nothing else in token-goat contacts Google Drive, and it holds no Drive credentials: `gdrive-sections` fetches the public export URL of a document id you pass it by hand.
1237
+
1238
+ **Secret redaction in cached content.** Token-goat caches command output, fetched pages, and MCP results so it can serve them back later instead of re-running the work. Anything it writes to those caches is passed through a redactor first, so a credential that appeared in output does not sit on disk in plain text and does not get replayed into a later session. This is unconditional — there is no flag to turn it on, and it applies to cached Bash and Task output (including the command string itself, which is where an inline `--token=...` would otherwise land), fetched web content, MCP tool results and their labels, `compress-text`/`handoff` payloads, and the raw JSON disk cache. Recognized shapes: Anthropic, OpenAI, AWS, GitHub, Slack, Stripe, npm, and Google keys; JWTs; `Authorization: Bearer`/`Basic` headers; PEM private-key blocks; presigned-url signatures (AWS `X-Amz-Signature`, Google Cloud Storage `X-Goog-Signature`, Azure SAS `sig`); and generic `password=`/`secret=`/`api_key=` assignments in `.env`, connection-string, and query-string shape. A match is replaced by a `[REDACTED:<kind>]` marker naming which pattern fired.
1239
+
1240
+ Session state gets the same treatment. The per-session file records which urls were fetched and which `curl -o` downloads landed where, and a url carries credentials as readily as output does. The fetched-url list is redacted, so the compaction manifest can still name what was fetched without naming the key; the download list is keyed by a digest of the url instead, because its only consumer is an exact-match check and a redaction there would make two urls differing only in their key look identical. The fetched-url entry also redacts the prompt that was sent with the page, and carries a digest of the pair so redaction cannot merge two entries that differ only inside the redacted span. An entry written by an older version is rewritten into this shape the first time the file is read, so upgrading clears the credentials an old file was holding rather than keeping them for the life of the session.
1211
1241
 
1212
1242
  Two honest limits. It is a pattern matcher, not a classifier: a credential in a format it does not recognize — an internal token shape, a bare high-entropy string with no `key=` prefix — is cached as-is. And it protects what token-goat *stores*, not what your agent reads in real time; a secret printed to the terminal was already in the model's context before any caching happened. Treat it as damage control on the cache layer, not a reason to relax about printing secrets.
1213
1243
 
@@ -1229,13 +1259,22 @@ Add-MpPreference -ExclusionPath "$env:LOCALAPPDATA\dfk-helper\token-goat"
1229
1259
  token-goat uninstall
1230
1260
  ```
1231
1261
 
1232
- Reverses everything in [What gets installed?](#what-gets-installed): the hook entries in `settings.json`, the `CLAUDE.md` block, the skill directory. Add `--codex`, `--gemini`, `--opencode`, `--pi`, `--hermes`, `--openclaw`, `--copilot`, `--grok`, or `--vscode` to also strip those integrations. There is no `--purge` flag — `uninstall` never deletes the data directory (cache, index, models, logs); remove it by hand if you want it gone. It also does not stop a running worker; use `token-goat worker stop` for that. Nothing else on the system depends on it.
1262
+ Reverses everything in [What gets installed?](#what-gets-installed): the hook entries in `settings.json`, the `CLAUDE.md` block, the skill directory. Add `--codex`, `--gemini`, `--opencode`, `--pi`, `--hermes`, `--openclaw`, `--copilot`, `--grok`, or `--vscode` to also strip those integrations. It does not stop a running worker; use `token-goat worker stop` for that. Nothing else on the system depends on it.
1263
+
1264
+ By default the data directories stay: the index took real time to build and a reinstall wants it back. Add `--purge` to delete them as well, which is what offboarding a machine needs:
1265
+
1266
+ ```
1267
+ token-goat worker stop
1268
+ token-goat uninstall --purge
1269
+ ```
1270
+
1271
+ That removes both roots (the data directory holding the index, caches, models and logs, and the home directory holding session state and the OCR cache), naming each one and how much it reclaimed. It refuses while the worker is running, because the worker would rewrite files under a directory being deleted.
1233
1272
 
1234
1273
  ## About
1235
1274
 
1236
1275
  I built this because long Claude Code and Codex sessions on my machine kept burning context in the same ways: screenshots landing at 2-3 MB, the agent re-reading a file it parsed hours earlier in the same conversation, compactions that forgot which functions were edited. Each felt preventable.
1237
1276
 
1238
- This is a solo project. I use it daily on Windows 11. Tests run on Node.js 20 and 22.
1277
+ This is a solo project. I use it daily on Windows 11. The full test suite runs on Node.js 22 across Windows, macOS, and Linux, and all three must pass before anything merges.
1239
1278
 
1240
1279
  ## Sister project
1241
1280
 
package/SECURITY.md ADDED
@@ -0,0 +1,72 @@
1
+ # Security Policy
2
+
3
+ Token-Goat runs on your machine, registers hooks with two AI coding tools, and reads files those tools open. The attack surface is real and the project treats security reports as a priority.
4
+
5
+ ## Reporting a vulnerability
6
+
7
+ Email token-goat@dfkhelper.com. This is a private inbox, not a public issue tracker. Do not file security reports as GitHub issues; that exposes the finding before a fix ships. PGP key available on request.
8
+
9
+ A useful report contains:
10
+
11
+ - Affected Token-Goat version (`token-goat --version`)
12
+ - Operating system and Node version (`node --version`)
13
+ - Reproduction steps, ideally a minimal command sequence
14
+ - Observed impact and a short severity assessment
15
+ - Suggested fix, if known
16
+
17
+ ## What to expect
18
+
19
+ Reports are acknowledged within 7 calendar days of receipt. If you have not heard back in that window, resend; mail does get lost. After triage, a target fix window is set based on severity and communicated back. Coordinated disclosure is preferred, with a typical 90-day window before public details. Reporters who want public credit are credited in the changelog and the release notes. Reporters who prefer to stay anonymous are kept anonymous.
20
+
21
+ ## In scope
22
+
23
+ The following are treated as security issues:
24
+
25
+ - Privilege escalation through Token-Goat's installer, worker, or hooks
26
+ - Remote code execution via hook payloads, CLI arguments, or cached content
27
+ - Data exfiltration through Token-Goat's database, cache, or session store
28
+ - Injection vulnerabilities in any user-facing command or hook input path
29
+ - Supply-chain concerns affecting the published `token-goat` package
30
+ - Authentication or authorization flaws in token-bearing integrations
31
+
32
+ ## Out of scope
33
+
34
+ The following are not treated as security issues unless paired with a working proof of concept showing actual impact:
35
+
36
+ - Theoretical vulnerabilities without a reproducer
37
+ - Issues in upstream dependencies that do not manifest through Token-Goat's surface
38
+ - Local denial of service via resource exhaustion (memory, disk, CPU) on the user's own machine
39
+ - Social-engineering attacks that require tricking the user into running malicious commands
40
+ - Issues that require an already-compromised local user account
41
+
42
+ ## Dependency advisories
43
+
44
+ `npm audit --omit=dev` on the published package is not empty, and pretending otherwise would waste your review time. Every runtime advisory that remains traces to one of three packages. All three are already at their latest published version, so there is no forward patch to take: the only version npm offers as a "fix" is an older major that drops features Token-Goat uses.
45
+
46
+ | Package | Advisories it carries | Where it loads | Why the advisory does not reach you through Token-Goat |
47
+ | --- | --- | --- | --- |
48
+ | `@xenova/transformers` | [`protobufjs`](https://github.com/advisories/GHSA-xq3m-2v4x-88gg) (critical), `onnx-proto`, `onnxruntime-web`, and its own pinned `sharp` | optional; loaded only when semantic search builds or queries embeddings | not mitigated, so it is the one to weigh. Skip it with `npm install --omit=optional`, or leave `indexing.embeddings_enabled` off, and the code never loads |
49
+ | `exceljs` | [`uuid`](https://github.com/advisories/GHSA-w5hq-g745-h8pq) | optional; loaded only when an `xlsx-*` command opens a workbook | the advisory is a missing bounds check on a caller-supplied `buf` argument; ExcelJS never passes one |
50
+ | `html-to-text` | [`deepmerge-ts`](https://github.com/advisories/GHSA-ggr8-5vv4-36mx) | required, and bundled into `dist/token-goat.mjs`; runs when a fetched page is converted to text | the advisory is stack exhaustion while merging a recursive object graph. The only object merged is the fixed options literal in `extractCleanText`; page content is never merged |
51
+
52
+ `npm install --omit=optional` gives you an install without the first two. The `xlsx-*` commands then say ExcelJS is not installed rather than failing oddly, and `semantic` keeps working on keyword search alone: it is the embedding half that goes away, not the command.
53
+
54
+ Direct dependencies with a forward patch are kept current rather than pinned: `sharp` and `puppeteer-core` were both moved across a major version to clear their advisories.
55
+
56
+ ## Verifying what you installed
57
+
58
+ Every published version is built and pushed by one workflow, [`.github/workflows/publish.yml`](.github/workflows/publish.yml), which runs only when a GitHub release is published (or manually, and then only from `main`). It publishes with npm provenance, so npm holds a signed attestation tying the tarball to the commit and workflow run that produced it. Nothing is ever published from a laptop.
59
+
60
+ To check a copy you already have:
61
+
62
+ ```
63
+ npm audit signatures
64
+ ```
65
+
66
+ Run from a project that depends on token-goat, that command verifies the registry signature and the provenance attestation for every installed package, token-goat included. The package page on npm links the attestation to the exact commit, so you can read the source that produced the bytes you are running.
67
+
68
+ Every action used by that workflow, and by CI, is pinned to a full commit SHA rather than a tag, so a compromised action repository cannot silently change what runs. That is enforced by a test rather than by review: see [tests/guards/workflow_actions_pinned.test.ts](tests/guards/workflow_actions_pinned.test.ts).
69
+
70
+ ## License
71
+
72
+ Token-Goat is source-available under the PolyForm Noncommercial License 1.0.0. Submitting a security report does not grant the reporter any license to Token-Goat's code beyond what PolyForm Noncommercial already permits. See LICENSE for the full terms.
@@ -0,0 +1,54 @@
1
+ import { createRequire as __cjsRequire } from 'node:module';
2
+ const require = __cjsRequire(import.meta.url);
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
10
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
11
+ }) : x)(function(x) {
12
+ if (typeof require !== "undefined") return require.apply(this, arguments);
13
+ throw Error('Dynamic require of "' + x + '" is not supported');
14
+ });
15
+ var __esm = (fn, res) => function __init() {
16
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
17
+ };
18
+ var __commonJS = (cb, mod) => function __require2() {
19
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
20
+ };
21
+ var __export = (target, all) => {
22
+ for (var name in all)
23
+ __defProp(target, name, { get: all[name], enumerable: true });
24
+ };
25
+ var __copyProps = (to, from, except, desc) => {
26
+ if (from && typeof from === "object" || typeof from === "function") {
27
+ for (let key of __getOwnPropNames(from))
28
+ if (!__hasOwnProp.call(to, key) && key !== except)
29
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
30
+ }
31
+ return to;
32
+ };
33
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
34
+ // If the importer is in node compatibility mode or this is not an ESM
35
+ // file that has been converted to a CommonJS file using a Babel-
36
+ // compatible transform (i.e. "__esModule" has not been set), then set
37
+ // "default" to the CommonJS "module.exports" for node compatibility.
38
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
39
+ mod
40
+ ));
41
+
42
+ // <define:import.meta.env>
43
+ var init_define_import_meta_env = __esm({
44
+ "<define:import.meta.env>"() {
45
+ }
46
+ });
47
+
48
+ export {
49
+ __require,
50
+ __commonJS,
51
+ __export,
52
+ __toESM,
53
+ init_define_import_meta_env
54
+ };
@@ -0,0 +1,156 @@
1
+ import { createRequire as __cjsRequire } from 'node:module';
2
+ const require = __cjsRequire(import.meta.url);
3
+ import {
4
+ ToolFilter,
5
+ capTokens,
6
+ combineStreams,
7
+ compressOutput,
8
+ filterByName,
9
+ resolveMinNetSavingsBytes,
10
+ selectFilter,
11
+ shlexSplit,
12
+ wrappedShell
13
+ } from "./token-goat-chunk-N2S7UAVU.mjs";
14
+ import {
15
+ loadConfig,
16
+ recordStat
17
+ } from "./token-goat-chunk-X7PIWZMB.mjs";
18
+ import "./token-goat-chunk-VYC34TBW.mjs";
19
+ import {
20
+ init_define_import_meta_env
21
+ } from "./token-goat-chunk-4WGVGN6W.mjs";
22
+
23
+ // src/bash_runner.ts
24
+ init_define_import_meta_env();
25
+ import { spawnSync } from "node:child_process";
26
+ import * as os from "node:os";
27
+ var IdentityFilter = class extends ToolFilter {
28
+ name = "passthrough";
29
+ };
30
+ var DEFAULT_TIMEOUT_SECONDS = 600;
31
+ var MAX_CAPTURE_BYTES = 32 * 1024 * 1024;
32
+ var MIN_RECORD_STAT_BYTES = 32;
33
+ function resolveFilter(command, filterName, cwd) {
34
+ if (filterName) {
35
+ const named = filterByName(filterName);
36
+ if (named !== null) return named;
37
+ }
38
+ let argv;
39
+ try {
40
+ argv = shlexSplit(command);
41
+ } catch {
42
+ return null;
43
+ }
44
+ return selectFilter(argv, cwd);
45
+ }
46
+ function baseSpawnOptions(timeout, cwd, env) {
47
+ return { shell: wrappedShell(), timeout: timeout * 1e3, cwd, env };
48
+ }
49
+ function signalExitCode(signal) {
50
+ const num = os.constants.signals[signal];
51
+ return 128 + (num ?? 0);
52
+ }
53
+ function resolveProfile(explicit) {
54
+ if (explicit) return explicit;
55
+ try {
56
+ const p = loadConfig().compression.profile;
57
+ return p && p !== "auto" ? p : "balanced";
58
+ } catch {
59
+ return "balanced";
60
+ }
61
+ }
62
+ function resolveCompressLimits() {
63
+ try {
64
+ const bc = loadConfig().bash_compress;
65
+ return { maxLines: bc.max_lines, maxBytes: bc.max_bytes };
66
+ } catch {
67
+ return { maxLines: 1e3, maxBytes: 64 * 1024 };
68
+ }
69
+ }
70
+ function run(command, opts = {}) {
71
+ const timeout = opts.timeout ?? DEFAULT_TIMEOUT_SECONDS;
72
+ const filter = resolveFilter(command, opts.filterName, opts.cwd);
73
+ if (filter === null) {
74
+ if ((opts.maxTokens ?? 0) > 0) {
75
+ return wrapAndCompress(command, new IdentityFilter(), timeout, resolveProfile(opts.compressionProfile), opts);
76
+ }
77
+ return passthrough(command, timeout, opts.cwd, opts.env);
78
+ }
79
+ return wrapAndCompress(command, filter, timeout, resolveProfile(opts.compressionProfile), opts);
80
+ }
81
+ function runRaw(command, timeout = DEFAULT_TIMEOUT_SECONDS) {
82
+ return passthrough(command, timeout, void 0, void 0);
83
+ }
84
+ function passthrough(command, timeout, cwd, env) {
85
+ const result = spawnSync(command, { ...baseSpawnOptions(timeout, cwd, env), stdio: "inherit" });
86
+ if (isTimeout(result.error)) return 124;
87
+ if (result.status !== null) return result.status;
88
+ if (result.signal) return signalExitCode(result.signal);
89
+ return 0;
90
+ }
91
+ function isTimeout(error) {
92
+ return !!error && error.code === "ETIMEDOUT";
93
+ }
94
+ function decode(buf) {
95
+ if (buf == null) return "";
96
+ return typeof buf === "string" ? buf : buf.toString("utf8");
97
+ }
98
+ function wrapAndCompress(command, filter, timeout, profile, opts) {
99
+ const writeStdout = opts.writeStdout ?? ((s) => process.stdout.write(s));
100
+ const result = spawnSync(command, {
101
+ ...baseSpawnOptions(timeout, opts.cwd, opts.env),
102
+ stdio: ["ignore", "pipe", "pipe"],
103
+ maxBuffer: MAX_CAPTURE_BYTES
104
+ });
105
+ const timedOut = isTimeout(result.error);
106
+ const overflowed = !!result.error && result.error.code === "ENOBUFS";
107
+ let stdoutText = decode(result.stdout);
108
+ let stderrText = decode(result.stderr);
109
+ if (overflowed) {
110
+ stdoutText += `
111
+ [token-goat: capture capped at ${MAX_CAPTURE_BYTES / (1024 * 1024)} MiB]`;
112
+ }
113
+ let exitCode;
114
+ if (timedOut) {
115
+ exitCode = 124;
116
+ stderrText = (stderrText ? stderrText + "\n" : "") + `[token-goat: command exceeded ${timeout}s timeout and was killed]`;
117
+ } else if (result.status !== null) {
118
+ exitCode = result.status;
119
+ } else if (result.signal) {
120
+ exitCode = signalExitCode(result.signal);
121
+ } else {
122
+ exitCode = 0;
123
+ }
124
+ let argv;
125
+ try {
126
+ argv = shlexSplit(command);
127
+ } catch {
128
+ argv = [command];
129
+ }
130
+ const limits = resolveCompressLimits();
131
+ const compressed = compressOutput(filter, stdoutText, stderrText, exitCode, argv, {
132
+ compressionProfile: profile,
133
+ maxLines: limits.maxLines,
134
+ maxBytes: limits.maxBytes
135
+ });
136
+ const minNetSavingsBytes = resolveMinNetSavingsBytes();
137
+ const applied = compressed.worthApplying(minNetSavingsBytes);
138
+ let text = applied ? compressed.text : combineStreams(stdoutText, stderrText);
139
+ const maxTokens = opts.maxTokens ?? 0;
140
+ if (maxTokens > 0) text = capTokens(text, maxTokens);
141
+ const marker = applied ? compressed.withMarker(minNetSavingsBytes).slice(compressed.text.length) : "";
142
+ const body = text + marker;
143
+ writeStdout(body.endsWith("\n") ? body : body + "\n");
144
+ if (applied) recordSavings(compressed);
145
+ return exitCode;
146
+ }
147
+ function recordSavings(result) {
148
+ if (result.bytesSaved < MIN_RECORD_STAT_BYTES) return;
149
+ recordStat(`bash_compress:${result.filterName}`, result.bytesSaved, result.tokensSaved);
150
+ }
151
+ export {
152
+ DEFAULT_TIMEOUT_SECONDS,
153
+ MAX_CAPTURE_BYTES,
154
+ run,
155
+ runRaw
156
+ };
@@ -0,0 +1,112 @@
1
+ import { createRequire as __cjsRequire } from 'node:module';
2
+ const require = __cjsRequire(import.meta.url);
3
+ import {
4
+ clearModuleCaches
5
+ } from "./token-goat-chunk-VYC34TBW.mjs";
6
+ import {
7
+ init_define_import_meta_env
8
+ } from "./token-goat-chunk-4WGVGN6W.mjs";
9
+
10
+ // src/batch_serve.ts
11
+ init_define_import_meta_env();
12
+ function swapEnv(next) {
13
+ const before = { ...process.env };
14
+ for (const key of Object.keys(process.env)) if (!(key in next)) delete process.env[key];
15
+ for (const [key, value] of Object.entries(next)) process.env[key] = value;
16
+ return () => {
17
+ for (const key of Object.keys(process.env)) if (!(key in before)) delete process.env[key];
18
+ for (const [key, value] of Object.entries(before)) process.env[key] = value;
19
+ };
20
+ }
21
+ function captureOutput() {
22
+ let out = "";
23
+ let errText = "";
24
+ const realOut = process.stdout.write.bind(process.stdout);
25
+ const realErr = process.stderr.write.bind(process.stderr);
26
+ const sink = (append) => (chunk, enc, cb) => {
27
+ append(typeof chunk === "string" ? chunk : String(chunk));
28
+ const done = typeof enc === "function" ? enc : cb;
29
+ if (typeof done === "function") done();
30
+ return true;
31
+ };
32
+ process.stdout.write = sink((s) => {
33
+ out += s;
34
+ });
35
+ process.stderr.write = sink((s) => {
36
+ errText += s;
37
+ });
38
+ return {
39
+ stdout: () => out,
40
+ stderr: () => errText,
41
+ restore: () => {
42
+ process.stdout.write = realOut;
43
+ process.stderr.write = realErr;
44
+ }
45
+ };
46
+ }
47
+ async function serveOne(req, runFn) {
48
+ const cwdBefore = process.cwd();
49
+ const restoreEnv = swapEnv(req.env ?? { ...process.env });
50
+ const cap = captureOutput();
51
+ let status;
52
+ try {
53
+ if (req.cwd !== void 0) process.chdir(req.cwd);
54
+ process.exitCode = void 0;
55
+ await runFn([process.execPath, "token-goat", ...req.argv]);
56
+ status = typeof process.exitCode === "number" ? process.exitCode : 0;
57
+ } catch (e) {
58
+ cap.restore();
59
+ process.stderr.write("");
60
+ status = 1;
61
+ const captured = { out: cap.stdout(), err: cap.stderr() + String(e instanceof Error ? e.stack ?? e.message : e) + "\n" };
62
+ restoreEnv();
63
+ process.exitCode = void 0;
64
+ try {
65
+ process.chdir(cwdBefore);
66
+ } catch {
67
+ }
68
+ clearModuleCaches();
69
+ return { id: req.id, status, stdout: captured.out, stderr: captured.err };
70
+ }
71
+ cap.restore();
72
+ const stdout = cap.stdout();
73
+ const stderr = cap.stderr();
74
+ restoreEnv();
75
+ process.exitCode = void 0;
76
+ try {
77
+ process.chdir(cwdBefore);
78
+ } catch {
79
+ }
80
+ clearModuleCaches();
81
+ return { id: req.id, status, stdout, stderr };
82
+ }
83
+ function serveBatch(token, runFn) {
84
+ let buffered = "";
85
+ let chain = Promise.resolve();
86
+ process.stdin.setEncoding("utf8");
87
+ process.stdin.on("data", (chunk) => {
88
+ buffered += chunk;
89
+ for (; ; ) {
90
+ const nl = buffered.indexOf("\n");
91
+ if (nl === -1) break;
92
+ const line = buffered.slice(0, nl);
93
+ buffered = buffered.slice(nl + 1);
94
+ if (line.trim() === "") continue;
95
+ const req = JSON.parse(line);
96
+ chain = chain.then(async () => {
97
+ const res = await serveOne(req, runFn);
98
+ process.stdout.write(`${token} ${JSON.stringify(res)}
99
+ `);
100
+ });
101
+ }
102
+ });
103
+ process.stdin.on("end", () => {
104
+ void chain.then(() => {
105
+ process.exitCode = 0;
106
+ });
107
+ });
108
+ }
109
+ export {
110
+ serveBatch,
111
+ serveOne
112
+ };