token-goat 2.6.35 → 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 +3 -1
  3. package/dist/{token-goat-chunk-DRAIYVUI.mjs → token-goat-chunk-65BKISIS.mjs} +3 -3
  4. package/dist/{token-goat-chunk-UIZVX3HN.mjs → token-goat-chunk-CNDOJ3ZP.mjs} +2 -2
  5. package/dist/{token-goat-chunk-Y2NH6DGZ.mjs → token-goat-chunk-FRTBMRP7.mjs} +5 -5
  6. package/dist/{token-goat-chunk-D5IZEGN2.mjs → token-goat-chunk-IYTVE6KN.mjs} +13 -8
  7. package/dist/{token-goat-chunk-HW4VUKJ5.mjs → token-goat-chunk-KYFJC37X.mjs} +14 -13
  8. package/dist/{token-goat-chunk-YF67EWRH.mjs → token-goat-chunk-LN6OUHTV.mjs} +5 -5
  9. package/dist/{token-goat-hook-chunk-NIPXIAPA.mjs → token-goat-chunk-MGOUYAA2.mjs} +1 -1
  10. package/dist/{token-goat-chunk-I2KCEGIP.mjs → token-goat-chunk-UFOVM7ZN.mjs} +85 -45
  11. package/dist/{token-goat-chunk-K7W3P2TJ.mjs → token-goat-chunk-V465YKOR.mjs} +61 -17
  12. package/dist/{token-goat-chunk-SIHYQCTM.mjs → token-goat-chunk-VYMGEVZS.mjs} +16 -4
  13. package/dist/{token-goat-chunk-DQIT5JIF.mjs → token-goat-hook-chunk-3ZDBWJDF.mjs} +1 -1
  14. package/dist/{token-goat-hook-chunk-CRBMPT74.mjs → token-goat-hook-chunk-5UH54CW6.mjs} +16 -4
  15. package/dist/{token-goat-hook-chunk-VU6VGZBM.mjs → token-goat-hook-chunk-6ODM3MP7.mjs} +3 -3
  16. package/dist/{token-goat-hook-chunk-XM4W3WCO.mjs → token-goat-hook-chunk-A77A26A7.mjs} +85 -45
  17. package/dist/{token-goat-hook-chunk-4POU6OHK.mjs → token-goat-hook-chunk-BDR6C6IE.mjs} +5 -5
  18. package/dist/{token-goat-hook-chunk-3AAH72SO.mjs → token-goat-hook-chunk-C6GIABOX.mjs} +14 -13
  19. package/dist/{token-goat-hook-chunk-D6UJEFHX.mjs → token-goat-hook-chunk-E257IGSN.mjs} +2 -2
  20. package/dist/{token-goat-hook-chunk-NXVT4F3S.mjs → token-goat-hook-chunk-MW5HPEGD.mjs} +13 -8
  21. package/dist/{token-goat-hook-chunk-J4HKWUWZ.mjs → token-goat-hook-chunk-QSCYNJ2B.mjs} +5 -5
  22. package/dist/{token-goat-hook-chunk-2O7P4Z6Q.mjs → token-goat-hook-chunk-Y2WHX2P3.mjs} +61 -17
  23. package/dist/token-goat-hook.mjs +5 -5
  24. package/dist/token-goat.core.mjs +5 -5
  25. package/package.json +10 -9
  26. package/scripts/install-git-hooks.mjs +55 -0
@@ -8,7 +8,7 @@ import {
8
8
  import { createRequire } from "node:module";
9
9
  function resolveVersion() {
10
10
  if (true) {
11
- return "2.6.35";
11
+ return "2.6.36";
12
12
  }
13
13
  const require2 = createRequire(import.meta.url);
14
14
  const pkg = require2("../package.json");
@@ -742,6 +742,9 @@ function countContentLines(content) {
742
742
  const n = content.split("\n").length;
743
743
  return content.endsWith("\n") ? n - 1 : n;
744
744
  }
745
+ function pushAll(target, items) {
746
+ for (const item of items) target.push(item);
747
+ }
745
748
  function escapeRegExp(s) {
746
749
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
747
750
  }
@@ -2436,7 +2439,6 @@ var ENV_KEYS2 = [
2436
2439
  "TOKEN_GOAT_OVERFLOW_MAX_TOKENS",
2437
2440
  "TOKEN_GOAT_HINT_JSON_SIDECAR",
2438
2441
  "TOKEN_GOAT_LARGE_READ_BYTES",
2439
- "TOKEN_GOAT_CURATOR",
2440
2442
  "TOKEN_GOAT_HINT_BUDGET",
2441
2443
  "TOKEN_GOAT_HOOK_WATCHDOG_MS",
2442
2444
  "TOKEN_GOAT_WEB_COMPRESS",
@@ -2497,8 +2499,19 @@ var _lastProjectConfigParseError = null;
2497
2499
  function getLastProjectConfigParseError() {
2498
2500
  return _lastProjectConfigParseError;
2499
2501
  }
2500
- var PROJECT_LOCKED_SECTIONS = ["injection", "webfetch", "gdrive", "mcp", "network"];
2501
- var PROJECT_LOCKED_KEYS = ["indexing.cross_project_symbols", "worker.blocked_roots"];
2502
+ var PROJECT_LOCKED_SECTIONS = [
2503
+ "injection",
2504
+ "webfetch",
2505
+ "gdrive",
2506
+ "mcp",
2507
+ "network",
2508
+ "screenshot"
2509
+ ];
2510
+ var PROJECT_LOCKED_KEYS = [
2511
+ "image_shrink.max_image_pixels",
2512
+ "indexing.cross_project_symbols",
2513
+ "worker.blocked_roots"
2514
+ ];
2502
2515
  var _lastProjectConfigLockedKeys = [];
2503
2516
  function stripLockedProjectKeys(projectRaw) {
2504
2517
  const cleaned = {};
@@ -2629,10 +2642,12 @@ function resolveConfigKeyLayer(key, effectiveValue, cfg, projectInfo) {
2629
2642
  if (rawValueEquals(rawValue, effectiveValue)) return { layer: "project", path: projectInfo.path };
2630
2643
  return { layer: "project-invalid", path: projectInfo.path, rawValue, effectiveValue, reason: rejectionReason(key, rawValue, effectiveValue, cfg) };
2631
2644
  }
2645
+ function readConfigSource(p) {
2646
+ return decodeSource(fs5.readFileSync(p));
2647
+ }
2632
2648
  function readConfigToml(p) {
2633
2649
  try {
2634
- const text = fs5.readFileSync(p, "utf8");
2635
- return { raw: parse(text), parseError: null };
2650
+ return { raw: parse(readConfigSource(p)), parseError: null };
2636
2651
  } catch (e) {
2637
2652
  const code = e.code;
2638
2653
  if (code === "ENOENT") return { raw: {}, parseError: null };
@@ -2641,7 +2656,7 @@ function readConfigToml(p) {
2641
2656
  }
2642
2657
  function readConfigText(p) {
2643
2658
  try {
2644
- return { text: fs5.readFileSync(p, "utf8"), readError: null };
2659
+ return { text: readConfigSource(p), readError: null };
2645
2660
  } catch (e) {
2646
2661
  const code = e.code;
2647
2662
  if (code === "ENOENT") return { text: null, readError: null };
@@ -2683,11 +2698,11 @@ function isAutoTriggerMultiplierExplicit() {
2683
2698
  return ca_raw["auto_trigger_multiplier"] !== void 0;
2684
2699
  };
2685
2700
  try {
2686
- if (setsMultiplier(fs5.readFileSync(configPath(), "utf8"))) return true;
2701
+ if (setsMultiplier(readConfigSource(configPath()))) return true;
2687
2702
  } catch {
2688
2703
  }
2689
2704
  try {
2690
- return setsMultiplier(fs5.readFileSync(projectConfigPath(resolveConfigProjectRoot()), "utf8"));
2705
+ return setsMultiplier(readConfigSource(projectConfigPath(resolveConfigProjectRoot())));
2691
2706
  } catch {
2692
2707
  return false;
2693
2708
  }
@@ -4166,6 +4181,7 @@ function propagateEndLinesToSymbols(symbols, sections) {
4166
4181
  }
4167
4182
  function findMatchingBraceEndLine(content, openBraceIndex, totalLines, lineIndex, lineCommentPrefix, opts) {
4168
4183
  const block = opts?.blockComment;
4184
+ const backtick = opts?.backtickQuote === true;
4169
4185
  let depth = 0;
4170
4186
  let quote = null;
4171
4187
  for (let i = openBraceIndex; i < content.length; i++) {
@@ -4187,7 +4203,7 @@ function findMatchingBraceEndLine(content, openBraceIndex, totalLines, lineIndex
4187
4203
  while (i < content.length && content[i] !== "\n") i++;
4188
4204
  continue;
4189
4205
  }
4190
- if (ch === '"' || ch === "'") {
4206
+ if (ch === '"' || ch === "'" || backtick && ch === "`") {
4191
4207
  quote = ch;
4192
4208
  continue;
4193
4209
  }
@@ -4710,7 +4726,7 @@ function purgeDotenvEmbeddings(conn) {
4710
4726
  var MIGRATIONS = {
4711
4727
  // v1 -> v2: adds files.embed_sha, tracked separately from files.sha so embedding freshness can be gated independently of parse freshness (see makeIndexer in worker.ts). A pre-existing v1 database's `files` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has the column from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, so a genuine ALTER TABLE failure is never silently lost.
4712
4728
  1: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN embed_sha TEXT"),
4713
- // v2 -> v3: adds files.retry_count, a durable per-path counter for consecutive transient-read-failure requeues (see MAX_TRANSIENT_RETRIES / requeueDirtyPath / resetTransientRetryCount in worker.ts). Previously this counter lived only in an in-memory Map inside worker.ts, which meant resetTransientRetryCount -- called from appendDirtyPath (hooks_index.ts) in the short-lived hook CLI process -- could never actually reach the long-lived detached daemon process's own copy of that Map: they are different Node processes with no shared memory, so the reset was a silent no-op in the real deployed topology. Persisting the counter in `files` makes it visible to both processes via the one thing they do share: the index DB. Same swallow-duplicate-column pattern as v1 -> v2 above.
4729
+ // v2 -> v3: adds files.retry_count, a durable per-path counter for consecutive transient-read-failure requeues (see MAX_TRANSIENT_RETRIES / requeueDirtyPath / clearRetryCount in worker.ts). Previously this counter lived only in an in-memory Map inside worker.ts, which meant the retry-count reset -- run at the time in the short-lived hook CLI process -- could never actually reach the long-lived detached daemon process's own copy of that Map: they are different Node processes with no shared memory, so the reset was a silent no-op in the real deployed topology. Persisting the counter in `files` makes it visible to both processes via the one thing they do share: the index DB. Same swallow-duplicate-column pattern as v1 -> v2 above.
4714
4730
  2: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0"),
4715
4731
  // v8 -> v9: adds symbols.parent (see SCHEMA_VERSION comment above for why). A pre-existing v8 database's `symbols` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has the column from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, same pattern as v1 -> v2 / v2 -> v3 above.
4716
4732
  8: (conn) => alterTableIdempotent(conn, "ALTER TABLE symbols ADD COLUMN parent TEXT NOT NULL DEFAULT ''"),
@@ -4730,13 +4746,32 @@ function runMigrations(conn, fromVersion, toVersion) {
4730
4746
  MIGRATIONS[v]?.(conn);
4731
4747
  }
4732
4748
  }
4733
- function initConnection(conn) {
4734
- const walMode = conn.pragma("journal_mode = WAL", { simple: true });
4735
- if (String(walMode).toLowerCase() !== "wal") {
4736
- throw new Error(`db: failed to enable WAL mode (got: ${walMode})`);
4749
+ var WAL_SWITCH_DEADLINE_MS = 15e3;
4750
+ function enableWalWithRetry(conn, budgetMs = WAL_SWITCH_DEADLINE_MS) {
4751
+ const deadline = Date.now() + budgetMs;
4752
+ let lastError;
4753
+ for (; ; ) {
4754
+ try {
4755
+ const mode = conn.pragma("journal_mode = WAL", { simple: true });
4756
+ if (String(mode).toLowerCase() === "wal") return;
4757
+ lastError = new Error(`got: ${String(mode)}`);
4758
+ } catch (e) {
4759
+ lastError = e;
4760
+ }
4761
+ try {
4762
+ if (String(conn.pragma("journal_mode", { simple: true })).toLowerCase() === "wal") return;
4763
+ } catch {
4764
+ }
4765
+ if (Date.now() >= deadline) {
4766
+ throw new Error(`db: failed to enable WAL mode (${lastError instanceof Error ? lastError.message : String(lastError)})`);
4767
+ }
4768
+ sleepSync(25);
4737
4769
  }
4738
- conn.pragma("synchronous = NORMAL");
4770
+ }
4771
+ function initConnection(conn) {
4739
4772
  conn.pragma("busy_timeout = 15000");
4773
+ enableWalWithRetry(conn);
4774
+ conn.pragma("synchronous = NORMAL");
4740
4775
  conn.function(
4741
4776
  "TG_LOWER",
4742
4777
  { deterministic: true },
@@ -6093,7 +6128,14 @@ var SECRET_PATTERNS = [
6093
6128
  // realistic header ('{"alg":' or similar) always starts with "eyJ", so that's the practical
6094
6129
  // anchor here -- each of the three dot-separated segments requires a minimum length to avoid
6095
6130
  // matching a short, coincidentally dotted token.
6096
- ["jwt", /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g],
6131
+ //
6132
+ // The trailing `={0,2}` on each segment is what makes a padded token match. Base64url as the JWT
6133
+ // spec defines it drops the `=` padding, but producers that reach for a plain base64 encoder
6134
+ // emit it anyway, and a `=` in the header or payload segment used to defeat the match outright:
6135
+ // not a partial redaction, but none at all, so the entire token was printed. `=` cannot appear
6136
+ // anywhere except the end of a segment, since it is not one of the characters the segment body
6137
+ // allows, so accepting it here cannot widen the match onto anything else.
6138
+ ["jwt", /eyJ[A-Za-z0-9_-]{10,}={0,2}\.[A-Za-z0-9_-]{10,}={0,2}\.[A-Za-z0-9_-]{10,}={0,2}/g],
6097
6139
  ["npm_token", /npm_[A-Za-z0-9]{36}/g],
6098
6140
  // rk_live_ (restricted keys) share the sk_live_/sk_test_ secret-key shape and risk level, so
6099
6141
  // one pattern covers all three rather than adding a near-duplicate entry.
@@ -6284,6 +6326,7 @@ export {
6284
6326
  countNoun,
6285
6327
  excludeTestsHiddenNote,
6286
6328
  countContentLines,
6329
+ pushAll,
6287
6330
  escapeRegExp,
6288
6331
  windowsCmdQuoteArg,
6289
6332
  redactUrlQuery,
@@ -6324,6 +6367,7 @@ export {
6324
6367
  lastProjectConfigLockedKeys,
6325
6368
  getProjectConfigInfo,
6326
6369
  resolveConfigKeyLayer,
6370
+ readConfigSource,
6327
6371
  isAutoTriggerMultiplierExplicit,
6328
6372
  loadConfig,
6329
6373
  invalidateConfigCache,
@@ -2,11 +2,11 @@ import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
4
  relayInProcess
5
- } from "./token-goat-hook-chunk-4POU6OHK.mjs";
6
- import "./token-goat-hook-chunk-NIPXIAPA.mjs";
7
- import "./token-goat-hook-chunk-CRBMPT74.mjs";
8
- import "./token-goat-hook-chunk-XM4W3WCO.mjs";
9
- import "./token-goat-hook-chunk-2O7P4Z6Q.mjs";
5
+ } from "./token-goat-hook-chunk-BDR6C6IE.mjs";
6
+ import "./token-goat-hook-chunk-3ZDBWJDF.mjs";
7
+ import "./token-goat-hook-chunk-5UH54CW6.mjs";
8
+ import "./token-goat-hook-chunk-A77A26A7.mjs";
9
+ import "./token-goat-hook-chunk-Y2WHX2P3.mjs";
10
10
  import "./token-goat-hook-chunk-BUOCULAM.mjs";
11
11
  import "./token-goat-hook-chunk-RFRLWOQH.mjs";
12
12
  export {
@@ -2,13 +2,13 @@ import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./token-goat-chunk-HW4VUKJ5.mjs";
6
- import "./token-goat-chunk-D5IZEGN2.mjs";
7
- import "./token-goat-chunk-SIHYQCTM.mjs";
8
- import "./token-goat-chunk-I2KCEGIP.mjs";
5
+ } from "./token-goat-chunk-KYFJC37X.mjs";
6
+ import "./token-goat-chunk-IYTVE6KN.mjs";
7
+ import "./token-goat-chunk-VYMGEVZS.mjs";
8
+ import "./token-goat-chunk-UFOVM7ZN.mjs";
9
9
  import {
10
10
  installEpipeGuard
11
- } from "./token-goat-chunk-K7W3P2TJ.mjs";
11
+ } from "./token-goat-chunk-V465YKOR.mjs";
12
12
  import "./token-goat-chunk-AO2QD2AG.mjs";
13
13
  import "./token-goat-chunk-AEX54RUZ.mjs";
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-goat",
3
- "version": "2.6.35",
3
+ "version": "2.6.36",
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",
@@ -51,7 +51,8 @@
51
51
  "dist/",
52
52
  "README.md",
53
53
  "SECURITY.md",
54
- "LICENSE"
54
+ "LICENSE",
55
+ "scripts/install-git-hooks.mjs"
55
56
  ],
56
57
  "license": "PolyForm-Noncommercial-1.0.0",
57
58
  "engines": {
@@ -59,26 +60,26 @@
59
60
  },
60
61
  "dependencies": {
61
62
  "better-sqlite3": "^12.11.1",
62
- "commander": "^15.0.0",
63
- "csv-parse": "^7.0.2",
64
- "js-yaml": "^5.2.1",
65
- "jsonc-parser": "^3.3.1",
66
- "smol-toml": "^1.8.0",
67
- "zod": "^4.4.3"
63
+ "jsonc-parser": "^3.3.1"
68
64
  },
69
65
  "devDependencies": {
70
66
  "@eslint/js": "^10.0.1",
71
67
  "@types/better-sqlite3": "^9.6.0",
72
68
  "@types/node": "^26.2.0",
69
+ "commander": "^15.0.0",
70
+ "csv-parse": "^7.0.2",
73
71
  "esbuild": "^0.28.2",
74
72
  "eslint": "^10.8.1",
75
73
  "exceljs": "^4.4.0",
76
74
  "html-to-text": "^10.0.1",
75
+ "js-yaml": "^5.2.1",
77
76
  "lefthook": "^2.1.10",
77
+ "smol-toml": "^1.8.0",
78
78
  "tsx": "^4.23.12",
79
79
  "typescript": "^6.0.3",
80
80
  "typescript-eslint": "^8.67.0",
81
- "vitest": "^4.1.11"
81
+ "vitest": "^4.1.11",
82
+ "zod": "^4.4.3"
82
83
  },
83
84
  "optionalDependencies": {
84
85
  "@modelcontextprotocol/sdk": "^1.30.0",
@@ -0,0 +1,55 @@
1
+ /**
2
+ * `prepare` hook: wire up lefthook's git hooks for a development checkout.
3
+ *
4
+ * Runs `lefthook install` when lefthook is actually present, and skips quietly when it isn't.
5
+ *
6
+ * The skip matters because npm runs `prepare` for more than the dev-checkout case it was added
7
+ * for. `npm install -g .` runs it in a context without this project's node_modules/.bin on PATH,
8
+ * so a bare `lefthook install` dies with "'lefthook' is not recognized" and fails the global
9
+ * install that CLAUDE.md prescribes as the dogfood step for every CLI/hook change. Installing
10
+ * token-goat as a dependency has the same shape: no devDependencies, and no git repo to hook.
11
+ *
12
+ * Skipping is deliberately conditioned on lefthook being *absent*, not on the command failing.
13
+ * A developer whose `lefthook install` genuinely breaks still gets a hard error, because these
14
+ * hooks were decorative once already -- configured in lefthook.yml but never installed, so every
15
+ * guard in it silently did nothing -- and swallowing failures here is how that recurs.
16
+ */
17
+ import { spawnSync } from 'node:child_process'
18
+ import { fileURLToPath } from 'node:url'
19
+ import path from 'node:path'
20
+ import fs from 'node:fs'
21
+
22
+ /**
23
+ * Absolute path to the lefthook CLI shim, or null when lefthook isn't installed here.
24
+ *
25
+ * Looks for npm's own `.bin` shim rather than resolving the package, because the shim is what
26
+ * npm creates for a devDependency and it is the exact thing missing in the contexts we skip.
27
+ * `.cmd` first: on Windows the extensionless file is a shell script Node can't spawn directly.
28
+ */
29
+ function findLefthookBin(startDir) {
30
+ const binDir = path.join(startDir, 'node_modules', '.bin')
31
+ for (const name of ['lefthook.cmd', 'lefthook']) {
32
+ const candidate = path.join(binDir, name)
33
+ if (fs.existsSync(candidate)) return candidate
34
+ }
35
+ return null
36
+ }
37
+
38
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
39
+ const bin = findLefthookBin(projectRoot)
40
+
41
+ if (bin === null) {
42
+ // Not a dev checkout with lefthook installed: a global install, a published-package install, or
43
+ // a pack/prepare temp dir. Nothing to wire up and nothing wrong -- exit 0 so the install works.
44
+ process.exit(0)
45
+ }
46
+
47
+ // npm's Windows shim is a .cmd batch file, which Node refuses to spawn without a shell. Quote the
48
+ // path in that case, since `shell: true` re-parses the command string and the path can contain
49
+ // spaces (a checkout under "C:\Program Files", a user profile with a space in the name).
50
+ const needsShell = /\.(cmd|bat)$/i.test(bin)
51
+ const result = spawnSync(needsShell ? `"${bin}"` : bin, ['install'], {
52
+ stdio: 'inherit',
53
+ shell: needsShell,
54
+ })
55
+ process.exit(result.status ?? 1)