token-goat 2.6.34 → 2.6.36

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 (26) hide show
  1. package/README.md +4 -2
  2. package/SECURITY.md +14 -11
  3. package/dist/{token-goat-chunk-L7QLEVO7.mjs → token-goat-chunk-65BKISIS.mjs} +16 -14
  4. package/dist/{token-goat-chunk-I6EQH27G.mjs → token-goat-chunk-CNDOJ3ZP.mjs} +2 -2
  5. package/dist/{token-goat-chunk-FRMEOIRU.mjs → token-goat-chunk-FRTBMRP7.mjs} +208 -13
  6. package/dist/{token-goat-chunk-IYYQOPY4.mjs → token-goat-chunk-IYTVE6KN.mjs} +29 -10
  7. package/dist/{token-goat-chunk-GXBPEQR2.mjs → token-goat-chunk-KYFJC37X.mjs} +21 -14
  8. package/dist/{token-goat-chunk-CKRATWCJ.mjs → token-goat-chunk-LN6OUHTV.mjs} +5 -5
  9. package/dist/{token-goat-chunk-WA2ER6TZ.mjs → token-goat-chunk-MGOUYAA2.mjs} +1 -1
  10. package/dist/{token-goat-chunk-CN7GUPX6.mjs → token-goat-chunk-UFOVM7ZN.mjs} +461 -97
  11. package/dist/{token-goat-chunk-7FWBSPCT.mjs → token-goat-chunk-V465YKOR.mjs} +198 -21
  12. package/dist/{token-goat-chunk-OA3GFKP2.mjs → token-goat-chunk-VYMGEVZS.mjs} +16 -4
  13. package/dist/{token-goat-hook-chunk-2ISZ4AZR.mjs → token-goat-hook-chunk-3ZDBWJDF.mjs} +1 -1
  14. package/dist/{token-goat-hook-chunk-6C5RN3CM.mjs → token-goat-hook-chunk-5UH54CW6.mjs} +16 -4
  15. package/dist/{token-goat-hook-chunk-OQML5EMA.mjs → token-goat-hook-chunk-6ODM3MP7.mjs} +16 -14
  16. package/dist/{token-goat-hook-chunk-6QTWMSRO.mjs → token-goat-hook-chunk-A77A26A7.mjs} +461 -97
  17. package/dist/{token-goat-hook-chunk-BU5OFDGQ.mjs → token-goat-hook-chunk-BDR6C6IE.mjs} +208 -13
  18. package/dist/{token-goat-hook-chunk-I5VM5PO5.mjs → token-goat-hook-chunk-C6GIABOX.mjs} +21 -14
  19. package/dist/{token-goat-hook-chunk-QTZ6YUMQ.mjs → token-goat-hook-chunk-E257IGSN.mjs} +2 -2
  20. package/dist/{token-goat-hook-chunk-KGLMNOGD.mjs → token-goat-hook-chunk-MW5HPEGD.mjs} +29 -10
  21. package/dist/{token-goat-hook-chunk-ATEGFQAU.mjs → token-goat-hook-chunk-QSCYNJ2B.mjs} +5 -5
  22. package/dist/{token-goat-hook-chunk-CNLMPHXT.mjs → token-goat-hook-chunk-Y2WHX2P3.mjs} +198 -21
  23. package/dist/token-goat-hook.mjs +5 -5
  24. package/dist/token-goat.core.mjs +5 -5
  25. package/package.json +13 -11
  26. package/scripts/install-git-hooks.mjs +55 -0
package/README.md CHANGED
@@ -349,8 +349,10 @@ For archive/document work specifically, token-goat's bounded SQLite, XLSX, and P
349
349
  brew install ast-grep uv ruff ripgrep fd bat eza git-delta jq yq sd miller sqlite gh hyperfine fzf lazygit
350
350
 
351
351
  # Debian / Ubuntu — note the binary renames: rg=ripgrep, fd=fdfind, bat=batcat
352
- sudo apt install -y ripgrep fd-find bat jq sqlite3 fzf
353
- curl -LsSf https://astral.sh/uv/install.sh | sh # uv, then: uv tool install ruff
352
+ sudo apt install -y ripgrep fd-find bat jq sqlite3 fzf pipx
353
+ pipx install uv && pipx ensurepath # pipx puts uv in ~/.local/bin
354
+ export PATH="$HOME/.local/bin:$PATH" # this shell; ensurepath covers later ones
355
+ uv tool install ruff
354
356
  npm install -g @ast-grep/cli
355
357
  ```
356
358
 
package/SECURITY.md CHANGED
@@ -46,7 +46,7 @@ The following are not treated as security issues unless paired with a working pr
46
46
  | What you scan | Command | Result |
47
47
  | --- | --- | --- |
48
48
  | this repository | `npm audit` | clean, development dependencies included |
49
- | an install without optional packages | `npm install --omit=optional token-goat` then `npm audit --omit=dev --omit=optional` | clean, 46 packages |
49
+ | an install without optional packages | `npm install --omit=optional token-goat` then `npm audit --omit=dev --omit=optional` | clean, 40 packages |
50
50
  | a default install | `npm install token-goat` then `npm audit --omit=dev` | the optional packages in the table below |
51
51
 
52
52
  The repository is clean because [`package.json`](package.json) carries an `overrides` block that pins five transitive packages to patched versions. **npm applies `overrides` only in the root project**, so those pins do not travel to anyone who installs Token-Goat as a dependency. We are saying so plainly rather than letting a clean repository scan stand in for a clean install: if your scanner reads this repository or its lockfile it will report nothing, and that is not the whole picture.
@@ -56,12 +56,15 @@ Without the overrides a consumer resolves `protobufjs` at 6.x, which carries mor
56
56
  | Package | Advisories it carries | Where it loads | Why the advisory does not reach you through Token-Goat |
57
57
  | --- | --- | --- | --- |
58
58
  | `@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 |
59
- | `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 |
60
59
 
61
- `npm install --omit=optional` gives you an install without either of these, and `npm audit` reports it clean. Every command still starts; the ones that need a package you skipped say so. The `xlsx-*` commands report that ExcelJS is not installed rather than failing oddly, `zip-list`/`zip-read` do the same for fflate, and `semantic` keeps working on keyword search alone: it is the embedding half that goes away, not the command.
60
+ `npm install --omit=optional` gives you an install without it, and `npm audit` reports it clean. Every command still starts; the ones that need a package you skipped say so. The `xlsx-*`, `docx-*` and `pptx-*` commands report that fflate is not installed rather than failing oddly, `zip-list`/`zip-read` do the same, and `semantic` keeps working on keyword search alone: it is the embedding half that goes away, not the command.
61
+
62
+ `exceljs` used to appear in that table, carrying [`uuid`](https://github.com/advisories/GHSA-w5hq-g745-h8pq). It is now a development dependency instead. The `xlsx-*` commands read the workbook container directly with `fflate` and `fast-xml-parser`, the same two packages the `.docx` and `.pptx` readers already used, so `exceljs` is only a test fixture writer now. That removes 55 packages from a default install, including every deprecated one in the tree.
62
63
 
63
64
  `html-to-text` used to appear in that table, carrying [`deepmerge-ts`](https://github.com/advisories/GHSA-ggr8-5vv4-36mx). It is now a development dependency instead. esbuild inlines it into `dist/token-goat.mjs` at build time and nothing in the published bundle imports it, so it was a runtime dependency in name only: moving it removes `html-to-text`, `deepmerge-ts`, `htmlparser2`, `selderee`, and `dom-serializer` from an installed copy while the HTML-to-text output stays byte-for-byte identical. That is what takes the no-optional install to zero, and it is better than the alternative we had considered, rolling `html-to-text` back to 9.x: that version pins `htmlparser2` two majors lower, and `htmlparser2` is what parses fetched pages, so it would have traded an advisory in an options merger for an older parser on the one path that handles untrusted input.
64
65
 
66
+ Five more packages were in `dependencies` for the same reason and have moved the same way: `commander`, `csv-parse`, `js-yaml`, `smol-toml` and `zod`. esbuild inlines each one into the bundle, and the published bundle resolves none of them, so a consumer was downloading code the artifact already carried. Moving them takes an install without optional packages from 46 packages to 40. `better-sqlite3` and `jsonc-parser` stay, because the bundle really does load them at run time: the first is a native addon, and the second is reached through `createRequire` rather than an import esbuild can inline. `zod` is the largest of the five on disk and the one that saves least in practice, because `@modelcontextprotocol/sdk` and `puppeteer-core` both depend on it and a default install still gets it from them.
67
+
65
68
  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.
66
69
 
67
70
  For a scanner that ingests a bill of materials rather than a lockfile, `npm run sbom` writes CycloneDX 1.5 to stdout.
@@ -89,8 +92,8 @@ Token-Goat is source-available under the PolyForm Noncommercial License 1.0.0. S
89
92
 
90
93
  Every production dependency is permissively licensed, but a scan does not read it that way on its
91
94
  own. Counted from `package-lock.json`, which lists the packages for every platform rather than only
92
- the ones this machine installed, 22 entries need a human answer: 7 declare a license a scanner
93
- cannot resolve, and 15 carry a copyleft term. All 22 arrive through optional dependencies. Install
95
+ the ones this machine installed, 21 entries need a human answer: 7 declare a license a scanner
96
+ cannot resolve, and 14 carry a copyleft term. All 21 arrive through optional dependencies. Install
94
97
  with `npm install --omit=optional token-goat` and not one of them is present.
95
98
 
96
99
  **Declarations a scanner cannot resolve.** Both are upstream mistakes, and both are the same
@@ -110,19 +113,19 @@ an unresolvable one.
110
113
  | --- | --- | --- |
111
114
  | `@img/sharp-libvips-<platform>` (10 packages) | `LGPL-3.0-or-later` | libvips, shipped as a prebuilt shared library and used unmodified. LGPL asks that the library stay replaceable, and it is: it is a separate package that `sharp` loads at runtime. |
112
115
  | `@img/sharp-<platform>` (4 packages) | `Apache-2.0 AND LGPL-3.0-or-later` | the Apache half is `sharp` itself, the LGPL half is the same libvips |
113
- | `jszip` | `(MIT OR GPL-3.0-or-later)` | a choice between the two, and the MIT half is taken |
114
116
 
115
- `sharp` and `jszip` are both optional: `sharp` powers image shrinking, `jszip` arrives through
116
- `exceljs` for the `xlsx-*` commands.
117
+ `sharp` is optional: it powers image shrinking. `jszip` used to be listed here too; it arrived
118
+ through `exceljs`, which is no longer a dependency a consumer installs.
117
119
 
118
120
  **Three packages with no license at all used to be here.** `buffers@0.1.1` and `chainsaw@0.1.0`
119
121
  shipped with neither a `license` field nor a license file, and `traverse@0.3.9` had the file but
120
122
  not the field. No grant at all is worse for a review than a copyleft grant, because there is
121
123
  nothing to apply policy to. They arrived through `exceljs`, which depends on `unzipper`, which
122
124
  depended on `binary`, which depended on all three. `unzipper` dropped `binary` in 0.11, so an
123
- override to `^0.12.5` removes the sub-chain, and the deprecated `fstream` with it. Token-goat reads
124
- workbooks through `wb.xlsx.readFile()`, which goes through `jszip`; `unzipper` is only reachable
125
- through the streaming reader in `exceljs`, which this code never calls.
125
+ override to `^0.12.5` removes the sub-chain, and the deprecated `fstream` with it. `exceljs` is now
126
+ a development dependency, so none of that chain reaches an installed copy either way; the override
127
+ stays because it is what keeps this repository's own `npm audit`, which includes development
128
+ dependencies, clean.
126
129
 
127
130
  Reproduce the whole picture:
128
131
 
@@ -25,7 +25,7 @@ import {
25
25
  runSkeleton,
26
26
  runSymbol,
27
27
  withPinnedReads
28
- } from "./token-goat-chunk-IYYQOPY4.mjs";
28
+ } from "./token-goat-chunk-IYTVE6KN.mjs";
29
29
  import {
30
30
  buildProjectMap,
31
31
  embeddingsDepsAvailable,
@@ -34,7 +34,7 @@ import {
34
34
  getProjectIndexCounts,
35
35
  isWorkerRunning,
36
36
  mapLookupBytesSaved
37
- } from "./token-goat-chunk-CN7GUPX6.mjs";
37
+ } from "./token-goat-chunk-UFOVM7ZN.mjs";
38
38
  import {
39
39
  VERSION,
40
40
  dataDir,
@@ -45,7 +45,7 @@ import {
45
45
  normalizePath,
46
46
  recordStat,
47
47
  resolveProjectRoot
48
- } from "./token-goat-chunk-7FWBSPCT.mjs";
48
+ } from "./token-goat-chunk-V465YKOR.mjs";
49
49
  import "./token-goat-chunk-AO2QD2AG.mjs";
50
50
  import "./token-goat-chunk-AEX54RUZ.mjs";
51
51
 
@@ -126,7 +126,19 @@ function forCompare(p) {
126
126
  return process.platform === "win32" ? p.toLowerCase() : p;
127
127
  }
128
128
  function resolveToolRoot(projectRoot) {
129
- return resolveProjectRoot(projectRoot !== void 0 ? { project: projectRoot } : {});
129
+ const resolved = resolveProjectRoot(projectRoot !== void 0 ? { project: projectRoot } : {});
130
+ assertRootAllowed(resolved);
131
+ return resolved;
132
+ }
133
+ var RootNotAllowedError = class extends Error {
134
+ };
135
+ function assertRootAllowed(resolvedRoot) {
136
+ const allowedRoots = loadConfig().mcp.allowed_roots;
137
+ if (allowedRoots.length === 0) return;
138
+ if (allowedRoots.some((allowed) => checkWithinProjectRoot(resolvedRoot, allowed).inside)) return;
139
+ throw new RootNotAllowedError(
140
+ `refused: "${resolvedRoot}" is not inside any root listed in mcp.allowed_roots. A caller-supplied projectRoot is untrusted input, so this deployment pins which roots may be named; add the root to mcp.allowed_roots (or TOKEN_GOAT_MCP_ALLOWED_ROOTS) to permit it.`
141
+ );
130
142
  }
131
143
  function checkWithinProjectRoot(target, resolvedRoot) {
132
144
  const root = forCompare(normalizePath(realPathOrSelf(resolvedRoot)));
@@ -150,16 +162,6 @@ function specFilePart(spec) {
150
162
  var NO_PINS = /* @__PURE__ */ new Map();
151
163
  function confineTargets(targets, resolvedRoot, splitCommas = true) {
152
164
  if (!loadConfig(resolvedRoot).mcp.confine_reads_to_project_root) return { ok: true, targets, pins: NO_PINS };
153
- const allowedRoots = loadConfig().mcp.allowed_roots;
154
- if (allowedRoots.length > 0 && !allowedRoots.some((allowed) => checkWithinProjectRoot(resolvedRoot, allowed).inside)) {
155
- return {
156
- ok: false,
157
- refusal: toCallToolResult({
158
- text: `refused: "${resolvedRoot}" is not inside any root listed in mcp.allowed_roots. A caller-supplied projectRoot is untrusted input, so this deployment pins which roots may be named; add the root to mcp.allowed_roots (or TOKEN_GOAT_MCP_ALLOWED_ROOTS) to permit it.`,
159
- code: 1
160
- })
161
- };
162
- }
163
165
  const checked = [];
164
166
  const pins = /* @__PURE__ */ new Map();
165
167
  for (const raw of targets) {
@@ -10,11 +10,11 @@ import {
10
10
  selectFilter,
11
11
  shlexSplit,
12
12
  wrappedShell
13
- } from "./token-goat-chunk-WA2ER6TZ.mjs";
13
+ } from "./token-goat-chunk-MGOUYAA2.mjs";
14
14
  import {
15
15
  loadConfig,
16
16
  recordStat
17
- } from "./token-goat-chunk-7FWBSPCT.mjs";
17
+ } from "./token-goat-chunk-V465YKOR.mjs";
18
18
  import "./token-goat-chunk-AO2QD2AG.mjs";
19
19
  import "./token-goat-chunk-AEX54RUZ.mjs";
20
20
 
@@ -2,13 +2,15 @@ import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
4
  canRunWrappedShell,
5
+ compressOutput,
5
6
  dedupeConsecutive,
6
7
  detectFromCommand,
8
+ filterByName,
7
9
  hasBareBackgroundOrNewline,
8
10
  isRewriteWorthwhile,
9
11
  resolveMinNetSavingsBytes,
10
12
  shlexSplit
11
- } from "./token-goat-chunk-WA2ER6TZ.mjs";
13
+ } from "./token-goat-chunk-MGOUYAA2.mjs";
12
14
  import {
13
15
  BASH_OUTPUT_SUBDIR,
14
16
  HOOK_EVENTS,
@@ -27,7 +29,7 @@ import {
27
29
  storeBashOutput,
28
30
  storeWebOutput,
29
31
  summarizeOutputDelta
30
- } from "./token-goat-chunk-OA3GFKP2.mjs";
32
+ } from "./token-goat-chunk-VYMGEVZS.mjs";
31
33
  import {
32
34
  BODY_FIRST_TOOL_RESPONSE_KEYS,
33
35
  OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
@@ -117,7 +119,7 @@ import {
117
119
  wasCliReadThisSession,
118
120
  wasFileReadThisSession,
119
121
  wasHintShown
120
- } from "./token-goat-chunk-CN7GUPX6.mjs";
122
+ } from "./token-goat-chunk-UFOVM7ZN.mjs";
121
123
  import {
122
124
  VERSION,
123
125
  detectHarness,
@@ -136,7 +138,7 @@ import {
136
138
  runGit,
137
139
  shortFingerprint,
138
140
  toKB
139
- } from "./token-goat-chunk-7FWBSPCT.mjs";
141
+ } from "./token-goat-chunk-V465YKOR.mjs";
140
142
  import "./token-goat-chunk-AO2QD2AG.mjs";
141
143
  import "./token-goat-chunk-AEX54RUZ.mjs";
142
144
 
@@ -576,7 +578,7 @@ import crypto from "node:crypto";
576
578
  var TRACKED_SKILL = "token-goat";
577
579
  var MAX_COMMANDS_SHOWN = 8;
578
580
  async function currentCommandNames() {
579
- const { buildProgram } = await import("./token-goat-chunk-CKRATWCJ.mjs");
581
+ const { buildProgram } = await import("./token-goat-chunk-LN6OUHTV.mjs");
580
582
  return flattenCommandNames(buildCommandManifest(buildProgram()));
581
583
  }
582
584
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -7905,12 +7907,151 @@ function extractRgStructuralSearch(cmd) {
7905
7907
  function extractGrepPipeChain(cmd) {
7906
7908
  return /^(?:rg|grep)\b.*\|\s*(?:rg|grep)\b/.test(cmd);
7907
7909
  }
7910
+ function splitCommandWords(cmd) {
7911
+ const words = [];
7912
+ let cur = "";
7913
+ let quote = null;
7914
+ let started = false;
7915
+ for (let i = 0; i < cmd.length; i++) {
7916
+ const ch = cmd[i];
7917
+ if (quote !== null) {
7918
+ if (ch === "\\" && quote === '"' && i + 1 < cmd.length) {
7919
+ cur += cmd[i + 1];
7920
+ i++;
7921
+ continue;
7922
+ }
7923
+ if (ch === quote) quote = null;
7924
+ else cur += ch;
7925
+ continue;
7926
+ }
7927
+ if (ch === "\\" && i + 1 < cmd.length) {
7928
+ cur += cmd[i + 1];
7929
+ i++;
7930
+ started = true;
7931
+ continue;
7932
+ }
7933
+ if (ch === '"' || ch === "'") {
7934
+ quote = ch;
7935
+ started = true;
7936
+ continue;
7937
+ }
7938
+ if (/\s/.test(ch)) {
7939
+ if (started) words.push(cur);
7940
+ cur = "";
7941
+ started = false;
7942
+ continue;
7943
+ }
7944
+ cur += ch;
7945
+ started = true;
7946
+ }
7947
+ if (started) words.push(cur);
7948
+ return words;
7949
+ }
7950
+ var CURL_VALUE_FLAGS = /* @__PURE__ */ new Set([
7951
+ "-H",
7952
+ "--header",
7953
+ "-A",
7954
+ "--user-agent",
7955
+ "-e",
7956
+ "--referer",
7957
+ "-b",
7958
+ "--cookie",
7959
+ "-c",
7960
+ "--cookie-jar",
7961
+ "-o",
7962
+ "--output",
7963
+ "--output-dir",
7964
+ "-u",
7965
+ "--user",
7966
+ "-U",
7967
+ "--proxy-user",
7968
+ "-x",
7969
+ "--proxy",
7970
+ "-X",
7971
+ "--request",
7972
+ "-d",
7973
+ "--data",
7974
+ "--data-raw",
7975
+ "--data-binary",
7976
+ "--data-urlencode",
7977
+ "-F",
7978
+ "--form",
7979
+ "-T",
7980
+ "--upload-file",
7981
+ "-K",
7982
+ "--config",
7983
+ "-w",
7984
+ "--write-out",
7985
+ "-m",
7986
+ "--max-time",
7987
+ "--connect-to",
7988
+ "--resolve",
7989
+ "--retry",
7990
+ "--cacert",
7991
+ "-E",
7992
+ "--cert",
7993
+ "--key",
7994
+ "--range",
7995
+ "-r",
7996
+ "--interface",
7997
+ // Options whose own value is a URL or a host. Leaving these out was the same defect as reading
7998
+ // the first URL in the string: `curl --doh-url https://doh/ -o f https://target` answered with
7999
+ // the DoH resolver rather than the file being fetched. `--url-query` belongs here too -- it
8000
+ // appends query data to the request, it does not name the request target.
8001
+ "--url-query",
8002
+ "--doh-url",
8003
+ "--preproxy",
8004
+ "--proxy1.0",
8005
+ "--socks4",
8006
+ "--socks4a",
8007
+ "--socks5",
8008
+ "--socks5-hostname",
8009
+ "--proxy-header",
8010
+ "--noproxy",
8011
+ "--dns-servers",
8012
+ "--aws-sigv4",
8013
+ "--proxy-cacert",
8014
+ "--proxy-cert",
8015
+ "--proxy-key",
8016
+ "--oauth2-bearer",
8017
+ "--hsts",
8018
+ "--alt-svc",
8019
+ "--etag-save",
8020
+ "--etag-compare",
8021
+ "--trace",
8022
+ "--trace-ascii",
8023
+ "--dump-header",
8024
+ "-D"
8025
+ ]);
7908
8026
  function extractCurlUrl(cmd) {
7909
- const m = /(https?:\/\/[^\s'"]+)/.exec(cmd);
7910
- return m?.[1] ?? null;
8027
+ const words = splitCommandWords(cmd);
8028
+ let positional = null;
8029
+ for (let i = 0; i < words.length; i++) {
8030
+ const w = words[i];
8031
+ if (w === "--url") {
8032
+ const next = words[i + 1];
8033
+ if (next !== void 0 && /^https?:\/\//.test(next)) return next;
8034
+ i++;
8035
+ continue;
8036
+ }
8037
+ const eq = w.indexOf("=");
8038
+ if (w.startsWith("--") && eq > 0) {
8039
+ if (w.slice(0, eq) === "--url" && /^https?:\/\//.test(w.slice(eq + 1))) return w.slice(eq + 1);
8040
+ continue;
8041
+ }
8042
+ if (CURL_VALUE_FLAGS.has(w)) {
8043
+ i++;
8044
+ continue;
8045
+ }
8046
+ if (w.startsWith("-") && w !== "-") continue;
8047
+ if (/^https?:\/\//.test(w)) return w;
8048
+ positional ??= /https?:\/\/[^\s'")}`]+/.exec(w)?.[0] ?? null;
8049
+ }
8050
+ if (positional !== null) return positional;
8051
+ return null;
7911
8052
  }
7912
8053
  function extractTgSurgicalRead(cmd, cwd) {
7913
- const m = /^token-goat\s+(symbol|read|section|skill-body|skill-compact|map)(?:\s+(.*))?$/.exec(cmd);
8054
+ const m = /^(?:token-goat|tg)\s+(symbol|read|section|skill-body|skill-compact|map)(?:\s+(.*))?$/.exec(cmd);
7914
8055
  if (!m) return null;
7915
8056
  const sub = m[1];
7916
8057
  const rest = (m[2] ?? "").trim();
@@ -7989,9 +8130,9 @@ function extractCurlDownload(cmd) {
7989
8130
  const outputPath = outputMatch[1] ?? outputMatch[2] ?? outputMatch[3];
7990
8131
  if (!outputPath) return null;
7991
8132
  if (curlHasUnsafeFlags(cmd)) return null;
7992
- const urlMatch = /(https?:\/\/[^\s'"]+)/.exec(cmd);
7993
- if (!urlMatch?.[1]) return null;
7994
- return { url: urlMatch[1], outputPath };
8133
+ const url = extractCurlUrl(cmd);
8134
+ if (url === null) return null;
8135
+ return { url, outputPath };
7995
8136
  }
7996
8137
  function isTscCommand(cmd) {
7997
8138
  return /^\s*tsc(\s|$)/i.test(cmd);
@@ -8044,9 +8185,34 @@ function maybeCompressRewrite(event, rawCmd, cmd) {
8044
8185
  const wrapped = `token-goat compress -f ${filterName} --timeout ${cfg.timeout_seconds} -c ${shellQuoteSingle(rawCmd)}`;
8045
8186
  return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: wrapped } };
8046
8187
  }
8188
+ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes) {
8189
+ if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
8190
+ if (isCompressibleSingleCommand(cmd)) return null;
8191
+ if (exitCode !== null && exitCode !== 0) return null;
8192
+ let cfg;
8193
+ try {
8194
+ cfg = loadConfig().bash_compress;
8195
+ } catch {
8196
+ return null;
8197
+ }
8198
+ if (!cfg.enabled || cfg.disabled_filters.includes("generic")) return null;
8199
+ if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return null;
8200
+ const filter3 = filterByName("generic");
8201
+ if (filter3 === null) return null;
8202
+ const compressed = compressOutput(filter3, output, "", exitCode ?? 0, [], {
8203
+ maxLines: cfg.max_lines,
8204
+ maxBytes: cfg.max_bytes
8205
+ });
8206
+ const minNet = resolveMinNetSavingsBytes();
8207
+ if (!compressed.worthApplying(minNet)) return null;
8208
+ const id = await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8209
+ recordStat("bash_compress:generic", compressed.bytesSaved, compressed.tokensSaved);
8210
+ const body = compressed.withMarker(minNet) + "\n[token-goat] full output: bash-output " + id + " --full";
8211
+ return { hookType: "rewriteOutput", updatedOutput: body };
8212
+ }
8047
8213
  function unwrapCompressCommand(executed) {
8048
8214
  const t = executed.trim();
8049
- if (!/^token-goat\s+compress\b/.test(t)) return null;
8215
+ if (!/^(?:token-goat|tg)\s+compress\b/.test(t)) return null;
8050
8216
  let argv;
8051
8217
  try {
8052
8218
  argv = shlexSplit(t);
@@ -8098,6 +8264,28 @@ function detectUnbalancedShellSyntax(cmd) {
8098
8264
  i = j;
8099
8265
  continue;
8100
8266
  }
8267
+ if (ch === "$" && cmd[i + 1] === "'") {
8268
+ let j = i + 2;
8269
+ let closed = false;
8270
+ while (j < cmd.length) {
8271
+ if (cmd[j] === "\\") {
8272
+ j += 2;
8273
+ continue;
8274
+ }
8275
+ if (cmd[j] === "'") {
8276
+ j++;
8277
+ closed = true;
8278
+ break;
8279
+ }
8280
+ j++;
8281
+ }
8282
+ if (!closed) {
8283
+ inSingle = true;
8284
+ break;
8285
+ }
8286
+ i = j;
8287
+ continue;
8288
+ }
8101
8289
  if (ch === "#" && (i === 0 || /\s/.test(cmd[i - 1] ?? ""))) {
8102
8290
  const nl2 = cmd.indexOf("\n", i);
8103
8291
  i = nl2 === -1 ? cmd.length : nl2 + 1;
@@ -8704,7 +8892,11 @@ async function postBashHandler(event) {
8704
8892
  recordBashOutput(gitScopedCacheHash, gitScopedCacheId, Buffer.byteLength(output, "utf-8"));
8705
8893
  }
8706
8894
  const isMonitoring = getMonitoringRecallHint(cmd) !== null;
8707
- if (!isMonitoring && !isBuildCommand(cmd) && !isCurlGetCommand(cmd)) return passOutput();
8895
+ if (!isMonitoring && !isBuildCommand(cmd) && !isCurlGetCommand(cmd)) {
8896
+ const compound = await maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes);
8897
+ if (compound !== null) return compound;
8898
+ return passOutput();
8899
+ }
8708
8900
  if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return passOutput();
8709
8901
  const cacheKey = isCurlGetCommand(cmd) ? extractCurlUrl(cmd) ?? cmd : stripOutputPipeline(cmd);
8710
8902
  const simpleHash = shortFingerprint(cacheKey);
@@ -9830,6 +10022,9 @@ async function relayInProcess(eventName, rawPayload) {
9830
10022
  async function relay(eventName) {
9831
10023
  try {
9832
10024
  if (!isHookEventName(eventName)) {
10025
+ console.error(
10026
+ `[relay] unknown hook event '${eventName}'; nothing ran. Valid events: ${HOOK_EVENTS.join(", ")}`
10027
+ );
9833
10028
  process.stdout.write("{}");
9834
10029
  return;
9835
10030
  }
@@ -50,7 +50,7 @@ import {
50
50
  walkProject,
51
51
  yamlLineClosesQuote,
52
52
  yamlOpenQuoteAfter
53
- } from "./token-goat-chunk-CN7GUPX6.mjs";
53
+ } from "./token-goat-chunk-UFOVM7ZN.mjs";
54
54
  import {
55
55
  PER_FILE_COUNTERFACTUAL_CEILING,
56
56
  _detectOpenQuote,
@@ -83,6 +83,7 @@ import {
83
83
  loadConfig,
84
84
  normalizePath,
85
85
  offsetToLine,
86
+ pushAll,
86
87
  recordStat,
87
88
  redactIfDotenv,
88
89
  redactSecrets,
@@ -100,7 +101,7 @@ import {
100
101
  unsupportedLanguageName,
101
102
  windowsCmdQuoteArg,
102
103
  withExtension
103
- } from "./token-goat-chunk-7FWBSPCT.mjs";
104
+ } from "./token-goat-chunk-V465YKOR.mjs";
104
105
  import {
105
106
  registerReset
106
107
  } from "./token-goat-chunk-AO2QD2AG.mjs";
@@ -563,9 +564,9 @@ function evalJsonPath(data, ops) {
563
564
  } else if (op.kind === "wildcard") {
564
565
  fanned = true;
565
566
  if (Array.isArray(item)) {
566
- next.push(...item);
567
+ pushAll(next, item);
567
568
  } else if (typeof item === "object" && item !== null) {
568
- next.push(...Object.values(item));
569
+ pushAll(next, Object.values(item));
569
570
  }
570
571
  } else {
571
572
  fanned = true;
@@ -997,7 +998,7 @@ function queryXml(xmlText, pathStr) {
997
998
  const attrName = step.attributeSelect;
998
999
  for (const cand of currentCandidates) {
999
1000
  if (attrName === "*") {
1000
- attrVals.push(...Object.values(cand.attributes));
1001
+ pushAll(attrVals, Object.values(cand.attributes));
1001
1002
  } else if (cand.attributes[attrName] !== void 0) {
1002
1003
  attrVals.push(cand.attributes[attrName]);
1003
1004
  }
@@ -1046,7 +1047,7 @@ function queryXml(xmlText, pathStr) {
1046
1047
  if (step.allIndices || matching.length > 1 || step.attributeFilter !== void 0) {
1047
1048
  hasFanned = true;
1048
1049
  }
1049
- nextCandidates.push(...matching);
1050
+ pushAll(nextCandidates, matching);
1050
1051
  }
1051
1052
  }
1052
1053
  currentCandidates = nextCandidates;
@@ -6552,6 +6553,10 @@ function mergeListingJson(files, blocks, anyOk) {
6552
6553
  const payload = { items, truncated, totalCount, ...errors.length > 0 ? { errors } : {} };
6553
6554
  return { text: JSON.stringify(payload, null, 2), code: anyOk ? 0 : 1 };
6554
6555
  }
6556
+ function symbolCountLabel(shown, truncated, trueCount) {
6557
+ if (!truncated || trueCount === void 0 || trueCount <= shown) return countNoun(shown, "symbol");
6558
+ return `${shown} of ${countNoun(trueCount, "symbol")}`;
6559
+ }
6555
6560
  function runSkeleton(opts) {
6556
6561
  const multiFiles = parseMultiFileSpec(opts.file);
6557
6562
  if (multiFiles !== null) return runPerFileListing(multiFiles, (file) => runSkeleton({ ...opts, file, includeFilePath: true }), opts.json === true);
@@ -6584,7 +6589,7 @@ function runSkeleton(opts) {
6584
6589
  return { text: text2, code: 0 };
6585
6590
  }
6586
6591
  const totalLines = filtered.length > 0 ? Math.max(...filtered.map((s) => s.lineEnd)) : 0;
6587
- const lines = [`# Skeleton: ${opts.file} (${countNoun(filtered.length, "symbol")}, ${countNoun(totalLines, "line")})`];
6592
+ const lines = [`# Skeleton: ${opts.file} (${symbolCountLabel(filtered.length, symbolsTruncated, trueSymbolCount)}, ${countNoun(totalLines, "line")})`];
6588
6593
  if (filtered.length === 0 && preFilterCount > 0) lines.push(filteredToEmptyNotice(preFilterCount, opts.minLines, opts.grep));
6589
6594
  for (const sym of filtered) {
6590
6595
  const lineStr = sym.lineStart.toString().padStart(6);
@@ -6626,7 +6631,7 @@ function runOutline(opts) {
6626
6631
  recordReadStat("outline", fullSourceBytes, text2, opts.file);
6627
6632
  return { text: text2, code: 0 };
6628
6633
  }
6629
- const lines = [`# Outline: ${opts.file} (${countNoun(filtered.length, "symbol")})`];
6634
+ const lines = [`# Outline: ${opts.file} (${symbolCountLabel(filtered.length, symbolsTruncated, trueSymbolCount)})`];
6630
6635
  if (filtered.length === 0 && preFilterCount > 0) lines.push(filteredToEmptyNotice(preFilterCount, opts.minLines, opts.grep));
6631
6636
  for (const sym of filtered) {
6632
6637
  const rangeStr = `${sym.lineStart.toString().padStart(4)}-${sym.lineEnd.toString().padEnd(6)}`;
@@ -7644,10 +7649,16 @@ function buildChangedRefHint(cwd, ref) {
7644
7649
  const commitWord = commitCount === 1 ? "1 commit" : `${commitCount} commits`;
7645
7650
  return `Hint: this repo has only ${commitWord}; '${ref}' does not exist. Try: token-goat changed --since ${suggestedRef}`;
7646
7651
  }
7652
+ function refIsSafe(ref) {
7653
+ return !ref.startsWith("-");
7654
+ }
7655
+ function emitUnsafeRef(ref) {
7656
+ emitErr(`Refusing a git ref that starts with '-': ${ref}`);
7657
+ }
7647
7658
  function runChanged(opts = {}) {
7648
7659
  const ref = opts.ref ?? "HEAD~5";
7649
- if (ref.startsWith("-")) {
7650
- emitErr(`Refusing a git ref that starts with '-': ${ref}`);
7660
+ if (!refIsSafe(ref)) {
7661
+ emitUnsafeRef(ref);
7651
7662
  return 1;
7652
7663
  }
7653
7664
  const cwd = opts.projectRoot ?? process.cwd();
@@ -7824,6 +7835,10 @@ function resolveSymbolSpecOrEmitError(commandName, spec, projectRoot) {
7824
7835
  return resolution.entry;
7825
7836
  }
7826
7837
  function runDiff(opts) {
7838
+ if (opts.ref !== void 0 && !refIsSafe(opts.ref)) {
7839
+ emitUnsafeRef(opts.ref);
7840
+ return 1;
7841
+ }
7827
7842
  const match = resolveSymbolSpecOrEmitError("diff", opts.spec, opts.projectRoot);
7828
7843
  if (match === null) return 1;
7829
7844
  const cwd = opts.projectRoot ?? process.cwd();
@@ -7911,6 +7926,10 @@ function parseLogDashLOutput(stdout) {
7911
7926
  return entries;
7912
7927
  }
7913
7928
  function runLog(opts) {
7929
+ if (opts.ref !== void 0 && !refIsSafe(opts.ref)) {
7930
+ emitUnsafeRef(opts.ref);
7931
+ return 1;
7932
+ }
7914
7933
  const match = resolveSymbolSpecOrEmitError("log", opts.spec, opts.projectRoot);
7915
7934
  if (match === null) return 1;
7916
7935
  const cwd = opts.projectRoot ?? process.cwd();