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
@@ -8,7 +8,7 @@ import {
8
8
  import { createRequire } from "node:module";
9
9
  function resolveVersion() {
10
10
  if (true) {
11
- return "2.6.34";
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
  }
@@ -1183,6 +1186,9 @@ function getDisplayRoot(explicitRoot) {
1183
1186
  _displayRootCache = { cwd, root };
1184
1187
  return root;
1185
1188
  }
1189
+ registerReset(() => {
1190
+ _displayRootCache = null;
1191
+ });
1186
1192
  function isUnderSystemTemp(filePath) {
1187
1193
  let sysTemp;
1188
1194
  try {
@@ -2433,7 +2439,6 @@ var ENV_KEYS2 = [
2433
2439
  "TOKEN_GOAT_OVERFLOW_MAX_TOKENS",
2434
2440
  "TOKEN_GOAT_HINT_JSON_SIDECAR",
2435
2441
  "TOKEN_GOAT_LARGE_READ_BYTES",
2436
- "TOKEN_GOAT_CURATOR",
2437
2442
  "TOKEN_GOAT_HINT_BUDGET",
2438
2443
  "TOKEN_GOAT_HOOK_WATCHDOG_MS",
2439
2444
  "TOKEN_GOAT_WEB_COMPRESS",
@@ -2494,8 +2499,19 @@ var _lastProjectConfigParseError = null;
2494
2499
  function getLastProjectConfigParseError() {
2495
2500
  return _lastProjectConfigParseError;
2496
2501
  }
2497
- var PROJECT_LOCKED_SECTIONS = ["injection", "webfetch", "gdrive", "mcp", "network"];
2498
- var PROJECT_LOCKED_KEYS = ["indexing.cross_project_symbols"];
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
+ ];
2499
2515
  var _lastProjectConfigLockedKeys = [];
2500
2516
  function stripLockedProjectKeys(projectRaw) {
2501
2517
  const cleaned = {};
@@ -2626,10 +2642,12 @@ function resolveConfigKeyLayer(key, effectiveValue, cfg, projectInfo) {
2626
2642
  if (rawValueEquals(rawValue, effectiveValue)) return { layer: "project", path: projectInfo.path };
2627
2643
  return { layer: "project-invalid", path: projectInfo.path, rawValue, effectiveValue, reason: rejectionReason(key, rawValue, effectiveValue, cfg) };
2628
2644
  }
2645
+ function readConfigSource(p) {
2646
+ return decodeSource(fs5.readFileSync(p));
2647
+ }
2629
2648
  function readConfigToml(p) {
2630
2649
  try {
2631
- const text = fs5.readFileSync(p, "utf8");
2632
- return { raw: parse(text), parseError: null };
2650
+ return { raw: parse(readConfigSource(p)), parseError: null };
2633
2651
  } catch (e) {
2634
2652
  const code = e.code;
2635
2653
  if (code === "ENOENT") return { raw: {}, parseError: null };
@@ -2638,7 +2656,7 @@ function readConfigToml(p) {
2638
2656
  }
2639
2657
  function readConfigText(p) {
2640
2658
  try {
2641
- return { text: fs5.readFileSync(p, "utf8"), readError: null };
2659
+ return { text: readConfigSource(p), readError: null };
2642
2660
  } catch (e) {
2643
2661
  const code = e.code;
2644
2662
  if (code === "ENOENT") return { text: null, readError: null };
@@ -2667,6 +2685,9 @@ function resolveConfigProjectRoot() {
2667
2685
  _projectRootCache = { cwd, root };
2668
2686
  return root;
2669
2687
  }
2688
+ registerReset(() => {
2689
+ _projectRootCache = null;
2690
+ });
2670
2691
  function isAutoTriggerMultiplierExplicit() {
2671
2692
  const setsMultiplier = (text) => {
2672
2693
  const raw = parse(text);
@@ -2677,11 +2698,11 @@ function isAutoTriggerMultiplierExplicit() {
2677
2698
  return ca_raw["auto_trigger_multiplier"] !== void 0;
2678
2699
  };
2679
2700
  try {
2680
- if (setsMultiplier(fs5.readFileSync(configPath(), "utf8"))) return true;
2701
+ if (setsMultiplier(readConfigSource(configPath()))) return true;
2681
2702
  } catch {
2682
2703
  }
2683
2704
  try {
2684
- return setsMultiplier(fs5.readFileSync(projectConfigPath(resolveConfigProjectRoot()), "utf8"));
2705
+ return setsMultiplier(readConfigSource(projectConfigPath(resolveConfigProjectRoot())));
2685
2706
  } catch {
2686
2707
  return false;
2687
2708
  }
@@ -4158,7 +4179,9 @@ function propagateEndLinesToSymbols(symbols, sections) {
4158
4179
  return sym;
4159
4180
  });
4160
4181
  }
4161
- function findMatchingBraceEndLine(content, openBraceIndex, totalLines, lineIndex, lineCommentPrefix) {
4182
+ function findMatchingBraceEndLine(content, openBraceIndex, totalLines, lineIndex, lineCommentPrefix, opts) {
4183
+ const block = opts?.blockComment;
4184
+ const backtick = opts?.backtickQuote === true;
4162
4185
  let depth = 0;
4163
4186
  let quote = null;
4164
4187
  for (let i = openBraceIndex; i < content.length; i++) {
@@ -4171,11 +4194,16 @@ function findMatchingBraceEndLine(content, openBraceIndex, totalLines, lineIndex
4171
4194
  if (ch === quote) quote = null;
4172
4195
  continue;
4173
4196
  }
4197
+ if (block !== void 0 && content.startsWith(block[0], i)) {
4198
+ const close = content.indexOf(block[1], i + block[0].length);
4199
+ i = close === -1 ? content.length : close + block[1].length - 1;
4200
+ continue;
4201
+ }
4174
4202
  if (lineCommentPrefix !== void 0 && content.startsWith(lineCommentPrefix, i)) {
4175
4203
  while (i < content.length && content[i] !== "\n") i++;
4176
4204
  continue;
4177
4205
  }
4178
- if (ch === '"' || ch === "'") {
4206
+ if (ch === '"' || ch === "'" || backtick && ch === "`") {
4179
4207
  quote = ch;
4180
4208
  continue;
4181
4209
  }
@@ -4187,7 +4215,106 @@ function findMatchingBraceEndLine(content, openBraceIndex, totalLines, lineIndex
4187
4215
  }
4188
4216
  }
4189
4217
  }
4190
- return totalLines;
4218
+ return opts?.noMatchValue ?? totalLines;
4219
+ }
4220
+ function assignBraceBlockSpans(symbols, content, lineCommentPrefix) {
4221
+ if (symbols.length === 0) return [...symbols];
4222
+ const lines = content.split("\n");
4223
+ const totalLines = lines.length;
4224
+ const lineIndex = buildLineIndex(content);
4225
+ const starts = [...new Set(symbols.map((s) => s.lineStart))].sort((a, b) => a - b);
4226
+ const blockComment = lineCommentPrefix === "//" ? ["/*", "*/"] : lineCommentPrefix === "#" ? ["<#", "#>"] : void 0;
4227
+ return symbols.map((sym) => {
4228
+ if (sym.lineEnd !== sym.lineStart) return sym;
4229
+ const nextStart = starts.find((s) => s > sym.lineStart);
4230
+ const lastSearchLine = Math.min(nextStart !== void 0 ? nextStart - 1 : totalLines, sym.lineStart + BRACE_SEARCH_MAX_LINES);
4231
+ const openIndex = findBlockOpenBrace(content, lineIndex, sym.lineStart, lastSearchLine, lineCommentPrefix, blockComment);
4232
+ if (openIndex === null) return sym;
4233
+ const endLine = findMatchingBraceEndLine(
4234
+ content,
4235
+ openIndex,
4236
+ totalLines,
4237
+ lineIndex,
4238
+ lineCommentPrefix,
4239
+ blockComment === void 0 ? { noMatchValue: -1 } : { blockComment, noMatchValue: -1 }
4240
+ );
4241
+ if (endLine <= sym.lineStart) return sym;
4242
+ return { ...sym, lineEnd: endLine, body: lines.slice(sym.lineStart - 1, endLine).join("\n") };
4243
+ });
4244
+ }
4245
+ var BRACE_SEARCH_MAX_LINES = 10;
4246
+ function findBlockOpenBrace(content, lineIndex, startLine, lastSearchLine, lineCommentPrefix, blockComment) {
4247
+ const from = lineIndex[startLine - 1];
4248
+ if (from === void 0) return null;
4249
+ const to = lineIndex[lastSearchLine] ?? content.length;
4250
+ let quote = null;
4251
+ let parenDepth = 0;
4252
+ let linesSeen = 0;
4253
+ let atLineStart = false;
4254
+ for (let i = from; i < to; i++) {
4255
+ const ch = content[i];
4256
+ if (ch === void 0) break;
4257
+ if (quote !== null) {
4258
+ if (ch === "\\") {
4259
+ i++;
4260
+ continue;
4261
+ }
4262
+ if (ch === quote) quote = null;
4263
+ continue;
4264
+ }
4265
+ if (ch === "\n") {
4266
+ linesSeen++;
4267
+ atLineStart = true;
4268
+ continue;
4269
+ }
4270
+ if (blockComment !== void 0 && content.startsWith(blockComment[0], i)) {
4271
+ const close = content.indexOf(blockComment[1], i + blockComment[0].length);
4272
+ if (close === -1) return null;
4273
+ i = close + blockComment[1].length - 1;
4274
+ continue;
4275
+ }
4276
+ if (lineCommentPrefix !== void 0 && content.startsWith(lineCommentPrefix, i)) {
4277
+ while (i + 1 < to && content[i + 1] !== "\n") i++;
4278
+ continue;
4279
+ }
4280
+ if (atLineStart && !/\s/.test(ch)) {
4281
+ atLineStart = false;
4282
+ if (parenDepth === 0 && linesSeen >= 1 && startsWithBlockKeyword(content, i, to)) return null;
4283
+ }
4284
+ if (ch === '"' || ch === "'") {
4285
+ quote = ch;
4286
+ continue;
4287
+ }
4288
+ if (ch === "(" || ch === "[") parenDepth++;
4289
+ else if (ch === ")" || ch === "]") {
4290
+ if (parenDepth > 0) parenDepth--;
4291
+ } else if (ch === ";") return null;
4292
+ else if (ch === "{") return i;
4293
+ }
4294
+ return null;
4295
+ }
4296
+ var BLOCK_OPENING_KEYWORDS = /* @__PURE__ */ new Set([
4297
+ "if",
4298
+ "for",
4299
+ "while",
4300
+ "do",
4301
+ "switch",
4302
+ "when",
4303
+ "match",
4304
+ "try",
4305
+ "foreach",
4306
+ "loop",
4307
+ "guard",
4308
+ "repeat",
4309
+ "unless",
4310
+ "until"
4311
+ ]);
4312
+ function startsWithBlockKeyword(content, i, to) {
4313
+ const first = content[i];
4314
+ if (first === void 0 || !/[A-Za-z_]/.test(first)) return false;
4315
+ let j = i + 1;
4316
+ while (j < to && /[A-Za-z0-9_]/.test(content[j])) j++;
4317
+ return BLOCK_OPENING_KEYWORDS.has(content.slice(i, j));
4191
4318
  }
4192
4319
 
4193
4320
  // src/languages/ini_idx.ts
@@ -4599,7 +4726,7 @@ function purgeDotenvEmbeddings(conn) {
4599
4726
  var MIGRATIONS = {
4600
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.
4601
4728
  1: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN embed_sha TEXT"),
4602
- // 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.
4603
4730
  2: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0"),
4604
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.
4605
4732
  8: (conn) => alterTableIdempotent(conn, "ALTER TABLE symbols ADD COLUMN parent TEXT NOT NULL DEFAULT ''"),
@@ -4619,13 +4746,32 @@ function runMigrations(conn, fromVersion, toVersion) {
4619
4746
  MIGRATIONS[v]?.(conn);
4620
4747
  }
4621
4748
  }
4622
- function initConnection(conn) {
4623
- const walMode = conn.pragma("journal_mode = WAL", { simple: true });
4624
- if (String(walMode).toLowerCase() !== "wal") {
4625
- 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);
4626
4769
  }
4627
- conn.pragma("synchronous = NORMAL");
4770
+ }
4771
+ function initConnection(conn) {
4628
4772
  conn.pragma("busy_timeout = 15000");
4773
+ enableWalWithRetry(conn);
4774
+ conn.pragma("synchronous = NORMAL");
4629
4775
  conn.function(
4630
4776
  "TG_LOWER",
4631
4777
  { deterministic: true },
@@ -5970,13 +6116,26 @@ var SECRET_PATTERNS = [
5970
6116
  // directly against the header name and the scheme directly against the space, so a body like
5971
6117
  // {"Authorization": "Bearer <token>"} -- the shape any logged fetch or MCP result arrives in
5972
6118
  // -- matched nothing and the token was cached verbatim.
5973
- ["auth_bearer_token", /(?<=Authorization["']?[ \t]{0,8}:[ \t]{0,8}["']?[ \t]{0,8}Bearer[ \t])[A-Za-z0-9\-._~+/]{10,}=*/gi],
6119
+ // `token` is the second scheme spelling in wide use -- it is what curl and gh examples pass for
6120
+ // GitHub and many other APIs -- and an opaque value behind it carries exactly the same authority
6121
+ // as one behind `Bearer`. The trailing gap is `{1,8}` rather than a single space for the same
6122
+ // reason every other gap in this lookbehind already is: a hand-aligned or reformatted header
6123
+ // ("Authorization: Bearer <token>") is ordinary, and demanding exactly one space there made
6124
+ // this the one position in the pattern that a second space defeated.
6125
+ ["auth_bearer_token", /(?<=Authorization["']?[ \t]{0,8}:[ \t]{0,8}["']?[ \t]{0,8}(?:Bearer|token)[ \t]{1,8})[A-Za-z0-9\-._~+/]{10,}=*/gi],
5974
6126
  ["auth_basic_token", /(?<=Authorization["']?[ \t]{0,8}:[ \t]{0,8}["']?[ \t]{0,8}Basic[ \t])[A-Za-z0-9+/]{6,}=*/gi],
5975
6127
  // JWTs have no distinctive prefix of their own, but the base64url encoding of the smallest
5976
6128
  // realistic header ('{"alg":' or similar) always starts with "eyJ", so that's the practical
5977
6129
  // anchor here -- each of the three dot-separated segments requires a minimum length to avoid
5978
6130
  // matching a short, coincidentally dotted token.
5979
- ["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],
5980
6139
  ["npm_token", /npm_[A-Za-z0-9]{36}/g],
5981
6140
  // rk_live_ (restricted keys) share the sk_live_/sk_test_ secret-key shape and risk level, so
5982
6141
  // one pattern covers all three rather than adding a near-duplicate entry.
@@ -6024,7 +6183,22 @@ var SECRET_PATTERNS = [
6024
6183
  // SECRET_KEY=, and DB_PASSWORD_HASH= all passed through in full. That class matches
6025
6184
  // identifier characters only, so prose that merely mentions a keyword still never reaches
6026
6185
  // a separator and stays unredacted. api[_-]?key covers the apikey and api-key spellings too.
6027
- ["generic_secret_assignment", /(?<=(?:password|passwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token)[a-z0-9_-]{0,64}["']?[ \t]{0,8}[:=][ \t]{0,8}["']?)(?:\\[^\n]|[^\s\\&;#'"[\],{}:]){4,}/gi]
6186
+ // `& ; # , :` play two incompatible roles. They separate one field from the next (a query
6187
+ // string, a cookie header, an inline env list), and they are also perfectly ordinary password
6188
+ // characters. Rejecting them outright got the first role right and the second badly wrong: the
6189
+ // match stopped at the first one and left everything after it in plain text, so
6190
+ // `password=corr&horse&battery` redacted four characters and printed the rest, and
6191
+ // `DB_PASSWORD=Aa1:xyz` matched nothing at all because the run before the `:` was under the
6192
+ // four-character floor. A tail left sitting in the open is the outcome this module's header
6193
+ // calls worse than no redaction, because it reads as handled.
6194
+ //
6195
+ // So the separator role is decided by what follows rather than assumed: one of these characters
6196
+ // ends the value only when the next thing along is another `name=` / `name:` pair, which is what
6197
+ // an actual field separator is always followed by. `,OTHER=public` and `; other=1` still end it;
6198
+ // the `&` in the middle of a passphrase does not. Whitespace, quotes and brackets are unchanged
6199
+ // -- they end a value unconditionally, which is also what keeps this pattern from re-matching
6200
+ // the `[REDACTED:...]` placeholder it just wrote.
6201
+ ["generic_secret_assignment", /(?<=(?:password|passwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token)[a-z0-9_-]{0,64}["']?[ \t]{0,8}[:=][ \t]{0,8}["']?)(?:\\[^\n]|[^\s\\&;#,:'"[\]{}]|[&;#,:](?![ \t]*[A-Za-z_][A-Za-z0-9_.-]*[ \t]*[:=])){4,}/gi]
6028
6202
  ];
6029
6203
  function redactSecrets(text) {
6030
6204
  let count = 0;
@@ -6152,6 +6326,7 @@ export {
6152
6326
  countNoun,
6153
6327
  excludeTestsHiddenNote,
6154
6328
  countContentLines,
6329
+ pushAll,
6155
6330
  escapeRegExp,
6156
6331
  windowsCmdQuoteArg,
6157
6332
  redactUrlQuery,
@@ -6192,6 +6367,7 @@ export {
6192
6367
  lastProjectConfigLockedKeys,
6193
6368
  getProjectConfigInfo,
6194
6369
  resolveConfigKeyLayer,
6370
+ readConfigSource,
6195
6371
  isAutoTriggerMultiplierExplicit,
6196
6372
  loadConfig,
6197
6373
  invalidateConfigCache,
@@ -6221,6 +6397,7 @@ export {
6221
6397
  assignFlatEndLines,
6222
6398
  propagateEndLinesToSymbols,
6223
6399
  findMatchingBraceEndLine,
6400
+ assignBraceBlockSpans,
6224
6401
  extractIni,
6225
6402
  _lineClosesQuote,
6226
6403
  _detectOpenQuote,
@@ -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-BU5OFDGQ.mjs";
6
- import "./token-goat-hook-chunk-2ISZ4AZR.mjs";
7
- import "./token-goat-hook-chunk-6C5RN3CM.mjs";
8
- import "./token-goat-hook-chunk-6QTWMSRO.mjs";
9
- import "./token-goat-hook-chunk-CNLMPHXT.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-GXBPEQR2.mjs";
6
- import "./token-goat-chunk-IYYQOPY4.mjs";
7
- import "./token-goat-chunk-OA3GFKP2.mjs";
8
- import "./token-goat-chunk-CN7GUPX6.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-7FWBSPCT.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,11 +1,12 @@
1
1
  {
2
2
  "name": "token-goat",
3
- "version": "2.6.34",
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",
7
7
  "bin": {
8
- "token-goat": "./dist/token-goat.mjs"
8
+ "token-goat": "./dist/token-goat.mjs",
9
+ "tg": "./dist/token-goat.mjs"
9
10
  },
10
11
  "repository": {
11
12
  "type": "git",
@@ -50,7 +51,8 @@
50
51
  "dist/",
51
52
  "README.md",
52
53
  "SECURITY.md",
53
- "LICENSE"
54
+ "LICENSE",
55
+ "scripts/install-git-hooks.mjs"
54
56
  ],
55
57
  "license": "PolyForm-Noncommercial-1.0.0",
56
58
  "engines": {
@@ -58,30 +60,30 @@
58
60
  },
59
61
  "dependencies": {
60
62
  "better-sqlite3": "^12.11.1",
61
- "commander": "^15.0.0",
62
- "csv-parse": "^7.0.2",
63
- "js-yaml": "^5.2.1",
64
- "jsonc-parser": "^3.3.1",
65
- "smol-toml": "^1.8.0",
66
- "zod": "^4.4.3"
63
+ "jsonc-parser": "^3.3.1"
67
64
  },
68
65
  "devDependencies": {
69
66
  "@eslint/js": "^10.0.1",
70
67
  "@types/better-sqlite3": "^9.6.0",
71
68
  "@types/node": "^26.2.0",
69
+ "commander": "^15.0.0",
70
+ "csv-parse": "^7.0.2",
72
71
  "esbuild": "^0.28.2",
73
72
  "eslint": "^10.8.1",
73
+ "exceljs": "^4.4.0",
74
74
  "html-to-text": "^10.0.1",
75
+ "js-yaml": "^5.2.1",
75
76
  "lefthook": "^2.1.10",
77
+ "smol-toml": "^1.8.0",
76
78
  "tsx": "^4.23.12",
77
79
  "typescript": "^6.0.3",
78
80
  "typescript-eslint": "^8.67.0",
79
- "vitest": "^4.1.11"
81
+ "vitest": "^4.1.11",
82
+ "zod": "^4.4.3"
80
83
  },
81
84
  "optionalDependencies": {
82
85
  "@modelcontextprotocol/sdk": "^1.30.0",
83
86
  "@xenova/transformers": "^2.17.2",
84
- "exceljs": "^4.4.0",
85
87
  "fast-xml-parser": "^5.9.3",
86
88
  "fflate": "^0.8.3",
87
89
  "pdfjs-dist": "^6.1.200",
@@ -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)