token-goat 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ permalink: /
9
9
 
10
10
  ![Token-Goat](assets/logo.png)
11
11
 
12
- **85%** smaller reads · **97.4%** image compression · **130+** bash output filters · **94–99%** skill overhead cut · compaction memory · **prompt injection** guard · **3.7 GB** never reached the model · **1.1 Gt** tokens saved
12
+ **85%** smaller reads · **97.4%** image compression · **160+** filter & interception rules · **94–99%** skill overhead cut · compaction memory · **prompt injection** guard · **3.7 GB** never reached the model · **1.1 Gt** tokens saved
13
13
 
14
14
  **Reduces AI token use/costs by 40–90%, and improves its focus. Fully automated, always online.**
15
15
 
@@ -72,6 +72,7 @@ The fastest way to reduce AI token costs is fixing these five, not writing short
72
72
  | Same docs URL fetched twice in a session | Re-fetch blocked at warm+ context pressure; cached body available via `token-goat web-output <id>` |
73
73
  | `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` |
74
74
  | `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 |
75
+ | `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 |
75
76
  | `rg` or `grep` run twice with the same pattern | Pre-Bash dedup hint fires on repeated `rg`/`grep`/`ag` calls the same way it fires on the native Grep tool; repeat searches return a cached match-count hint instead of re-running |
76
77
  | Read tool targets `tool-results/<id>.txt` or `tasks/<id>.output` | Pre-Read hook suggests `token-goat bash-output <id> --tail N` / `--grep PATTERN` / `--section H`; the filename stem is the output ID |
77
78
  | Repeated monitoring command run again (`gh run watch`, `next dev`, `vitest`, `docker logs`) | Pre-bash recall hint: when a prior run is cached and its output exceeds 2 KB, a pointer to `token-goat bash-output <id> --grep PATTERN` is injected instead of re-running the command |
@@ -239,7 +240,7 @@ FAILED tests/test_x.py::test_one
239
240
  [token-goat: pytest filter compressed 4.8 KiB to 0.1 KiB (97% saved)]
240
241
  ```
241
242
 
242
- 130 built-in filters cover the noisiest dev commands: `pytest`, `jest` / `vitest`, `cargo`, `npm` / `pnpm` / `yarn` / `bun`, `docker`, `kubectl` / `helm`, `aws`, `ruff` / `eslint` / `mypy` / `pylint` / `oxlint`, `git`, `make` / `gradle` / `mvn` / `ant` / `bazel`, `go test` / `golangci-lint`, `terraform` / `pulumi` / `cdk`, `pip` / `uv` / `conda`, `python`, `gh`, `ansible`, `pre-commit`, `grep`, `eza` / `ls`, `fd`, `bat`, `jq`, `yq`, `curl` / `wget`, `rsync`, `dotnet`, `cmake` / `ctest`, `swift` / `xcodebuild`, `ruby` / `bundler`, `elixir` / `mix`, `php` / `composer`, `flutter` / `dart`, `rust` / `cargo`, `kotlin` / `ktlint`, `zig`, `crystal`, `haskell` / `cabal`, `nix`, `R`, `c++` (conan / vcpkg / cppcheck / clang-tidy), `wrangler` / `hardhat` / `serverless`, `erlang`, `fly.io`, `forge`, `elm`, `julia`, `tox`, `vault`, `packer`, `nx` / `lerna` / `turbo`, `prettier` / `biome`, `sass`, `wasm-pack`, `deno`, **and AI tool CLIs**: `aider`, `gemini`, `claude`, `gh copilot`, `copilot`, `cursor`, `windsurf` (incl. Cascade), `opencode`, `continue`, `cline`. Each filter strips ANSI escapes, collapses `\r` progress bars, dedupes repeated lines, groups linter issues by rule, keeps every error block verbatim, and caps total output at 1000 lines / 64 KiB. Compound commands (`cmd1 && cmd2`) are wrapped per segment, so `git diff && git log` compresses both halves. Disable globally with `TOKEN_GOAT_BASH_COMPRESS=0`, per-filter via `[bash_compress] disabled_filters = ["docker"]` in config.toml, or preview the output of any command with `token-goat compress --cmd '<your command>'`. To exclude project-specific directories from indexing (temporary venvs, build sandboxes), add `[indexing] skip_dirs = ["my-tmpdir"]` to config.toml.
243
+ Built-in output compression covers 130+ dev tool CLIs: `pytest`, `jest` / `vitest`, `cargo`, `npm` / `pnpm` / `yarn` / `bun`, `docker`, `kubectl` / `helm`, `aws`, `ruff` / `eslint` / `mypy` / `pylint` / `oxlint`, `git`, `make` / `gradle` / `mvn` / `ant` / `bazel`, `go test` / `golangci-lint`, `terraform` / `pulumi` / `cdk`, `pip` / `uv` / `conda`, `python`, `gh`, `ansible`, `pre-commit`, `grep`, `eza` / `ls`, `fd`, `bat`, `jq`, `yq`, `curl` / `wget`, `rsync`, `dotnet`, `cmake` / `ctest`, `swift` / `xcodebuild`, `ruby` / `bundler`, `elixir` / `mix`, `php` / `composer`, `flutter` / `dart`, `rust` / `cargo`, `kotlin` / `ktlint`, `zig`, `crystal`, `haskell` / `cabal`, `nix`, `R`, `c++` (conan / vcpkg / cppcheck / clang-tidy), `wrangler` / `hardhat` / `serverless`, `erlang`, `fly.io`, `forge`, `elm`, `julia`, `tox`, `vault`, `packer`, `nx` / `lerna` / `turbo`, `prettier` / `biome`, `sass`, `wasm-pack`, `deno`, **and AI tool CLIs**: `aider`, `gemini`, `claude`, `gh copilot`, `copilot`, `cursor`, `windsurf` (incl. Cascade), `opencode`, `continue`, `cline`. Each filter strips ANSI escapes, collapses `\r` progress bars, dedupes repeated lines, groups linter issues by rule, keeps every error block verbatim, and caps total output at 1000 lines / 64 KiB. Compound commands (`cmd1 && cmd2`) are wrapped per segment, so `git diff && git log` compresses both halves. Disable globally with `TOKEN_GOAT_BASH_COMPRESS=0`, per-filter via `[bash_compress] disabled_filters = ["docker"]` in config.toml, or preview the output of any command with `token-goat compress --cmd '<your command>'`. To exclude project-specific directories from indexing (temporary venvs, build sandboxes), add `[indexing] skip_dirs = ["my-tmpdir"]` to config.toml.
243
244
 
244
245
  `gh api` responses get an extra pass: boilerplate `*_url` fields (`followers_url`, `gists_url`, `starred_url`, and around a dozen others) are stripped from JSON objects; `html_url`, `avatar_url`, `clone_url`, and `ssh_url` are kept. User and repo objects typically shrink 60–80%. When token-goat sees a GitHub permission error in the output or a non-zero exit on a security endpoint, it injects a system message suggesting `gh auth refresh -s security_events`.
245
246
 
@@ -9493,7 +9493,7 @@ init_define_import_meta_env();
9493
9493
  import { createRequire } from "node:module";
9494
9494
  function resolveVersion() {
9495
9495
  if (true) {
9496
- return "2.0.3";
9496
+ return "2.1.0";
9497
9497
  }
9498
9498
  const require2 = createRequire(import.meta.url);
9499
9499
  const pkg = require2("../package.json");
@@ -10567,6 +10567,10 @@ var BUILD_COMMAND_PATTERNS = [
10567
10567
  // Turbo
10568
10568
  /^\s*turbo\s+(build|dev)\b/i
10569
10569
  ];
10570
+ var LOCK_FILE_COUNT = LOCK_FILE_NAMES.size;
10571
+ var MANIFEST_FILE_COUNT = MANIFEST_FILE_NAMES.size + MANIFEST_EXTENSIONS.size + MANIFEST_BASENAME_PATTERNS.length;
10572
+ var BUILD_DIR_COUNT = BUILD_DIR_NAMES.size;
10573
+ var GENERATED_EXT_COUNT = ALWAYS_GENERATED_EXTS.size + CONDITIONALLY_GENERATED_EXTS.size;
10570
10574
  function isLockFile(basename7) {
10571
10575
  return LOCK_FILE_NAMES.has(basename7.toLowerCase());
10572
10576
  }
@@ -11845,6 +11849,8 @@ function isTsConfigFile(basename7) {
11845
11849
  return /^tsconfig(\..+)?\.json$/i.test(lower) || lower === "jsconfig.json";
11846
11850
  }
11847
11851
  var LARGE_FILE_BYTES = 100 * 1024;
11852
+ var REREAD_DENY_BYTES = 50 * 1024;
11853
+ var LARGE_FILE_DENY_BYTES = 500 * 1024;
11848
11854
  function isNodeModulesPath(path15) {
11849
11855
  const isWindows = process.platform === "win32";
11850
11856
  const check = isWindows ? path15.toLowerCase() : path15;
@@ -11925,6 +11931,13 @@ function preReadHandler(event) {
11925
11931
  }
11926
11932
  }
11927
11933
  }
11934
+ if (/^\.env(\.\w+)?$/.test(basename7) && wasFileReadThisSession(normalized)) {
11935
+ recordFileRead(normalized);
11936
+ recordStat("session_hint", 0, 0);
11937
+ return denyOutput(
11938
+ normalized + " was already read this session. Environment files rarely change mid-session. Use `token-goat config-get " + normalized + " KEY_NAME` to extract a specific variable."
11939
+ );
11940
+ }
11928
11941
  if (wasFileReadThisSession(normalized)) {
11929
11942
  const entry = getSessionFiles().get(normalized);
11930
11943
  const reads = entry?.readCount ?? 1;
@@ -11933,6 +11946,11 @@ function preReadHandler(event) {
11933
11946
  const hint = _isDocFile(normalized) ? 'Use `token-goat section "' + normalized + '::SectionName"` to read one section.' : "Use token-goat read/section/symbol to re-read surgically.";
11934
11947
  const rereadBytes = statSize(normalized) ?? 0;
11935
11948
  recordStat("session_hint", rereadBytes, Math.round(rereadBytes / 4));
11949
+ if (rereadBytes >= REREAD_DENY_BYTES || reads >= 2) {
11950
+ return denyOutput(
11951
+ normalized + " was already read this session (" + reads + " " + plural + "). " + hint
11952
+ );
11953
+ }
11936
11954
  return contextOutput(
11937
11955
  "Note: " + normalized + " was already read this session (" + reads + " " + plural + "). " + hint
11938
11956
  );
@@ -11943,6 +11961,11 @@ function preReadHandler(event) {
11943
11961
  recordFileRead(normalized);
11944
11962
  const hint = _isDocFile(normalized) ? 'Use `token-goat section "' + normalized + '::SectionName"` to read one section.' : "Consider token-goat skeleton or token-goat section.";
11945
11963
  recordStat("session_hint", size, Math.round(size / 4));
11964
+ if (size >= LARGE_FILE_DENY_BYTES) {
11965
+ return denyOutput(
11966
+ normalized + " is very large (" + kb + "KB). " + hint + " Use Read with offset/limit to sample specific sections."
11967
+ );
11968
+ }
11946
11969
  return contextOutput(
11947
11970
  "Note: " + normalized + " is large (" + kb + "kb). " + hint
11948
11971
  );
@@ -12489,10 +12512,125 @@ function extractCommand(event) {
12489
12512
  const cmd = event.toolInput["command"];
12490
12513
  return typeof cmd === "string" && cmd.trim() !== "" ? cmd.trim() : void 0;
12491
12514
  }
12515
+ function isTempPath(fp) {
12516
+ const norm = fp.replace(/\\/g, "/");
12517
+ return /^\/tmp\//i.test(norm) || /\/var\/folders\//i.test(norm) || /AppData\/Local\/Temp\//i.test(norm) || norm.startsWith("/c/Users/") && norm.includes("/AppData/Local/Temp/");
12518
+ }
12519
+ function isOrchestratorStateFile(filePath) {
12520
+ const basename7 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
12521
+ return /^\.improve-state-/.test(basename7);
12522
+ }
12492
12523
  function extractCatSourceFile(cmd) {
12493
12524
  const m = /^cat\s+(\S+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj))\s*$/.exec(cmd);
12494
12525
  return m?.[1] ?? null;
12495
12526
  }
12527
+ function extractCatFile(cmd) {
12528
+ const m = /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
12529
+ if (!m) return null;
12530
+ const filePath = m[1] ?? m[2] ?? m[3];
12531
+ if (filePath === void 0) return null;
12532
+ if (isTempPath(filePath)) return null;
12533
+ const basename7 = filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1) ?? filePath;
12534
+ const isEnvFile = /^\.env(\.\w+)?$/i.test(basename7);
12535
+ const hasKnownExt = /\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties|sql|env)$/i.test(filePath);
12536
+ if (!hasKnownExt && !isEnvFile) return null;
12537
+ const isDoc = /\.(?:md|mdx|rst|txt|sql)$/i.test(filePath);
12538
+ const isEnv = isEnvFile || /\.env$/i.test(filePath);
12539
+ const isConfig = /\.(?:json|yaml|yml|toml|conf|cfg|ini|properties)$/i.test(filePath);
12540
+ return { filePath, isDoc, isEnv, isConfig };
12541
+ }
12542
+ function extractWslCatFile(cmd) {
12543
+ const wslMatch = /^wsl(?:\s+-d\s+\S+)?\s+bash\s+-c\s+"cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+\/mnt\/([a-z])\/([^"]*)"/.exec(cmd);
12544
+ if (!wslMatch) return null;
12545
+ const drive = wslMatch[1]?.toUpperCase();
12546
+ const pathRest = wslMatch[2];
12547
+ if (!drive || !pathRest) return null;
12548
+ const filePath = drive + ":/" + pathRest;
12549
+ if (isTempPath(filePath)) return null;
12550
+ const basename7 = filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1) ?? filePath;
12551
+ const isEnvFile = /^\.env(\.\w+)?$/i.test(basename7);
12552
+ const hasKnownExt = /\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties|sql|env)$/i.test(filePath);
12553
+ if (!hasKnownExt && !isEnvFile) return null;
12554
+ const isDoc = /\.(?:md|mdx|rst|txt|sql)$/i.test(filePath);
12555
+ const isEnv = isEnvFile || /\.env$/i.test(filePath);
12556
+ const isConfig = /\.(?:json|yaml|yml|toml|conf|cfg|ini|properties)$/i.test(filePath);
12557
+ return { filePath, isDoc, isEnv, isConfig };
12558
+ }
12559
+ function extractPythonFileRead(cmd) {
12560
+ if (!/python3?/.test(cmd)) return null;
12561
+ const EXT = /\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties)/i;
12562
+ const direct = /open\(['"]([^'"]+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties))['"]/i.exec(cmd);
12563
+ if (direct) {
12564
+ const filePath = direct[1] ?? "";
12565
+ if (!filePath) return null;
12566
+ if (isOrchestratorStateFile(filePath)) return null;
12567
+ const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
12568
+ return { filePath, isDoc };
12569
+ }
12570
+ if (/open\s*\(/.test(cmd)) {
12571
+ const literal = /['"]([^'"]+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties))['"]/i.exec(cmd);
12572
+ if (literal) {
12573
+ const filePath = literal[1] ?? "";
12574
+ if (filePath) {
12575
+ if (isOrchestratorStateFile(filePath)) return null;
12576
+ if (EXT.test(filePath)) {
12577
+ const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
12578
+ return { filePath, isDoc };
12579
+ }
12580
+ }
12581
+ }
12582
+ }
12583
+ return null;
12584
+ }
12585
+ function extractHeadFile(cmd) {
12586
+ const m = /^head(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
12587
+ if (!m) return null;
12588
+ const n = parseInt(m[1] ?? m[2] ?? "0", 10);
12589
+ if (n > 0 && n < 10) return null;
12590
+ const filePath = m[3] ?? m[4] ?? m[5];
12591
+ if (filePath === void 0) return null;
12592
+ if (isTempPath(filePath)) return null;
12593
+ if (!/\.(?:ts|tsx|js|jsx|py|go|java|rs|rb|cs|md|mdx|rst|txt|json|yaml|yml|toml|sql|sh)$/i.test(filePath)) return null;
12594
+ const isDoc = /\.(?:md|mdx|rst|txt|sql)$/i.test(filePath);
12595
+ const isConfig = /\.(?:json|yaml|yml|toml|conf|cfg|ini|properties)$/i.test(filePath);
12596
+ return { filePath, isDoc, isConfig };
12597
+ }
12598
+ function extractNodeFileRead(cmd) {
12599
+ if (!/^node\s+-e/.test(cmd)) return null;
12600
+ const m = /readFileSync\(['"]([^'"]+\.(?:ts|tsx|js|jsx|py|go|java|rs|rb|cs|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties|sql))['"]/i.exec(cmd);
12601
+ if (!m || !m[1]) return null;
12602
+ const filePath = m[1];
12603
+ if (isOrchestratorStateFile(filePath)) return null;
12604
+ if (isTempPath(filePath)) return null;
12605
+ const isDoc = /\.(?:md|mdx|rst|txt|sql)$/i.test(filePath);
12606
+ return { filePath, isDoc };
12607
+ }
12608
+ function extractTailFile(cmd) {
12609
+ if (/-f\b/.test(cmd)) return null;
12610
+ if (/-c\b/.test(cmd)) return null;
12611
+ if (/-n\s*\+/.test(cmd)) return null;
12612
+ const m = /^tail(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
12613
+ if (!m) return null;
12614
+ const n = parseInt(m[1] ?? m[2] ?? "0", 10);
12615
+ if (n <= 10) return null;
12616
+ const filePath = m[3] ?? m[4] ?? m[5];
12617
+ if (!filePath) return null;
12618
+ if (isTempPath(filePath)) return null;
12619
+ if (!/\.(?:ts|tsx|js|jsx|py|go|java|rs|rb|cs|md|mdx|rst|txt|json|yaml|yml|toml|sql|sh)$/i.test(filePath)) return null;
12620
+ const isDoc = /\.(?:md|mdx|rst|txt|sql)$/i.test(filePath);
12621
+ return { filePath, isDoc };
12622
+ }
12623
+ function extractRgStructuralSearch(cmd) {
12624
+ if (!/^(?:rg|grep)\s+/.test(cmd)) return null;
12625
+ const hasStructural = /["']?\^?(?:def\s|class\s|function\s|func\s|fn\s|pub fn\s|import\s|from\s)/.test(cmd) || /["']\^(?:def|class|function|func|import|from)["']/.test(cmd) || /\\bdef\\b|\\bclass\\b/.test(cmd);
12626
+ if (!hasStructural) return null;
12627
+ const fileMatch = /(?:^|\s)(?:"([^"]+\.(?:py|ts|tsx|js|jsx|go|rs|rb|cs|java|cpp|cc|cxx|c|h|sh|bash))"|('([^']+\.(?:py|ts|tsx|js|jsx|go|rs|rb|cs|java|cpp|cc|cxx|c|h|sh|bash))')|([^\s"']+\.(?:py|ts|tsx|js|jsx|go|rs|rb|cs|java|cpp|cc|cxx|c|h|sh|bash)))\s*$/.exec(cmd);
12628
+ if (!fileMatch) return null;
12629
+ const filePath = fileMatch[1] ?? fileMatch[3] ?? fileMatch[4];
12630
+ if (!filePath) return null;
12631
+ if (isTempPath(filePath)) return null;
12632
+ return { filePath };
12633
+ }
12496
12634
  function isTscCommand(cmd) {
12497
12635
  return /^\s*tsc(\s|$)/i.test(cmd);
12498
12636
  }
@@ -12512,6 +12650,56 @@ function buildRecallHint(cmd, outputId) {
12512
12650
  function preBashHandler(event) {
12513
12651
  const cmd = extractCommand(event);
12514
12652
  if (cmd === void 0) return passOutput();
12653
+ const catResult = extractCatFile(cmd);
12654
+ if (catResult !== null) {
12655
+ const { filePath, isDoc, isEnv, isConfig } = catResult;
12656
+ const hint = isEnv ? 'Use `token-goat config-get "' + filePath + '" KEY_NAME` to read a specific variable.' : isConfig ? 'Use `token-goat config-get "' + filePath + '" KEY_NAME` or `token-goat section "' + filePath + '::sectionName"` to read a specific value.' : isDoc ? 'Use `token-goat section "' + filePath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + filePath + '::SymbolName"` to read one function or class.';
12657
+ recordStat("session_hint", 0, 0);
12658
+ return denyOutput("`cat` loads the entire file into context. " + hint);
12659
+ }
12660
+ const wslCatResult = extractWslCatFile(cmd);
12661
+ if (wslCatResult !== null) {
12662
+ const { filePath, isDoc, isEnv, isConfig } = wslCatResult;
12663
+ const hint = isEnv ? 'Use `token-goat config-get "' + filePath + '" KEY_NAME` to read a specific variable.' : isConfig ? 'Use `token-goat config-get "' + filePath + '" KEY_NAME` or `token-goat section "' + filePath + '::sectionName"` to read a specific value.' : isDoc ? 'Use `token-goat section "' + filePath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + filePath + '::SymbolName"` to read one function or class.';
12664
+ recordStat("session_hint", 0, 0);
12665
+ return denyOutput("`cat` loads the entire file into context. " + hint);
12666
+ }
12667
+ const pyRead = extractPythonFileRead(cmd);
12668
+ if (pyRead !== null) {
12669
+ const { filePath, isDoc } = pyRead;
12670
+ const hint = isDoc ? 'Use `token-goat section "' + filePath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + filePath + '::SymbolName"` to extract a specific symbol.';
12671
+ recordStat("session_hint", 0, 0);
12672
+ return denyOutput("Python `open()` file reads bypass read hooks. " + hint);
12673
+ }
12674
+ const tailResult = extractTailFile(cmd);
12675
+ if (tailResult !== null) {
12676
+ const { filePath, isDoc } = tailResult;
12677
+ const hint = isDoc ? 'Use `token-goat section "' + filePath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + filePath + '::SymbolName"` or `token-goat skeleton "' + filePath + '"` to see the file structure.';
12678
+ recordStat("session_hint", 0, 0);
12679
+ return contextOutput("`tail` bypasses read hooks. " + hint);
12680
+ }
12681
+ const headResult = extractHeadFile(cmd);
12682
+ if (headResult !== null) {
12683
+ const { filePath, isDoc, isConfig } = headResult;
12684
+ const hint = isConfig ? 'Use `token-goat config-get "' + filePath + '" KEY_NAME` or `token-goat section "' + filePath + '::sectionName"` to read a specific value.' : isDoc ? 'Use `token-goat section "' + filePath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + filePath + '::SymbolName"` or `token-goat skeleton "' + filePath + '"` to see the file structure.';
12685
+ recordStat("session_hint", 0, 0);
12686
+ return contextOutput("`head` bypasses read hooks. " + hint);
12687
+ }
12688
+ const nodeRead = extractNodeFileRead(cmd);
12689
+ if (nodeRead !== null) {
12690
+ const { filePath, isDoc } = nodeRead;
12691
+ const hint = isDoc ? 'Use `token-goat section "' + filePath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + filePath + '::SymbolName"` to extract a specific symbol.';
12692
+ recordStat("session_hint", 0, 0);
12693
+ return denyOutput("Node.js `fs.readFileSync()` bypasses read hooks. " + hint);
12694
+ }
12695
+ const rgStructural = extractRgStructuralSearch(cmd);
12696
+ if (rgStructural !== null) {
12697
+ const { filePath } = rgStructural;
12698
+ recordStat("session_hint", 0, 0);
12699
+ return contextOutput(
12700
+ 'Searching for code definitions with `rg`/`grep` is slower than surgical reads. Use `token-goat skeleton "' + filePath + '"` to see all symbols with line numbers, or `token-goat outline "' + filePath + '"` for symbols with docstrings and line ranges.'
12701
+ );
12702
+ }
12515
12703
  const monitoringHint = getMonitoringRecallHint(cmd);
12516
12704
  if (monitoringHint !== null) {
12517
12705
  const monCmdHash = fingerprintContent(cmd).slice(0, 16);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-goat",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
4
4
  "description": "Surgical token-reduction companion for Claude Code and other AI coding agents",
5
5
  "type": "module",
6
6
  "main": "./dist/token-goat.mjs",