token-goat 2.6.33 → 2.6.35

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 (25) hide show
  1. package/README.md +4 -0
  2. package/SECURITY.md +11 -10
  3. package/dist/{token-goat-chunk-FN6WLFIX.mjs → token-goat-chunk-D5IZEGN2.mjs} +817 -53
  4. package/dist/{token-goat-hook-chunk-HXHFLKEL.mjs → token-goat-chunk-DQIT5JIF.mjs} +7 -2
  5. package/dist/{token-goat-chunk-72R7M5H7.mjs → token-goat-chunk-DRAIYVUI.mjs} +16 -14
  6. package/dist/{token-goat-chunk-RPOXYJWK.mjs → token-goat-chunk-HW4VUKJ5.mjs} +77 -26
  7. package/dist/{token-goat-chunk-TDYTBCT4.mjs → token-goat-chunk-I2KCEGIP.mjs} +704 -138
  8. package/dist/{token-goat-chunk-WFRR2DBG.mjs → token-goat-chunk-K7W3P2TJ.mjs} +583 -343
  9. package/dist/{token-goat-chunk-OHQ7RBLL.mjs → token-goat-chunk-SIHYQCTM.mjs} +2 -2
  10. package/dist/{token-goat-chunk-77CX3MCN.mjs → token-goat-chunk-UIZVX3HN.mjs} +2 -2
  11. package/dist/{token-goat-chunk-L6FVTDPD.mjs → token-goat-chunk-Y2NH6DGZ.mjs} +564 -31
  12. package/dist/{token-goat-chunk-HPWPUBAK.mjs → token-goat-chunk-YF67EWRH.mjs} +5 -5
  13. package/dist/{token-goat-hook-chunk-E6AIPKNF.mjs → token-goat-hook-chunk-2O7P4Z6Q.mjs} +579 -339
  14. package/dist/{token-goat-hook-chunk-WBBHV6TZ.mjs → token-goat-hook-chunk-3AAH72SO.mjs} +77 -26
  15. package/dist/{token-goat-hook-chunk-QD2DX2U7.mjs → token-goat-hook-chunk-4POU6OHK.mjs} +564 -31
  16. package/dist/{token-goat-hook-chunk-ZT47M3IA.mjs → token-goat-hook-chunk-CRBMPT74.mjs} +2 -2
  17. package/dist/{token-goat-hook-chunk-26DLRODR.mjs → token-goat-hook-chunk-D6UJEFHX.mjs} +2 -2
  18. package/dist/{token-goat-hook-chunk-XBOWDNJB.mjs → token-goat-hook-chunk-J4HKWUWZ.mjs} +5 -5
  19. package/dist/{token-goat-chunk-RQQ4SY2P.mjs → token-goat-hook-chunk-NIPXIAPA.mjs} +7 -2
  20. package/dist/{token-goat-hook-chunk-VDXQXYJK.mjs → token-goat-hook-chunk-NXVT4F3S.mjs} +817 -53
  21. package/dist/{token-goat-hook-chunk-EKGDLI4D.mjs → token-goat-hook-chunk-VU6VGZBM.mjs} +16 -14
  22. package/dist/{token-goat-hook-chunk-UWV4MTYI.mjs → token-goat-hook-chunk-XM4W3WCO.mjs} +704 -138
  23. package/dist/token-goat-hook.mjs +5 -5
  24. package/dist/token-goat.core.mjs +5 -5
  25. package/package.json +11 -10
@@ -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-hook-chunk-HXHFLKEL.mjs";
13
+ } from "./token-goat-hook-chunk-NIPXIAPA.mjs";
12
14
  import {
13
15
  BASH_OUTPUT_SUBDIR,
14
16
  HOOK_EVENTS,
@@ -26,7 +28,7 @@ import {
26
28
  storeBashOutput,
27
29
  storeWebOutput,
28
30
  summarizeOutputDelta
29
- } from "./token-goat-hook-chunk-ZT47M3IA.mjs";
31
+ } from "./token-goat-hook-chunk-CRBMPT74.mjs";
30
32
  import {
31
33
  BODY_FIRST_TOOL_RESPONSE_KEYS,
32
34
  OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
@@ -116,11 +118,12 @@ import {
116
118
  wasCliReadThisSession,
117
119
  wasFileReadThisSession,
118
120
  wasHintShown
119
- } from "./token-goat-hook-chunk-UWV4MTYI.mjs";
121
+ } from "./token-goat-hook-chunk-XM4W3WCO.mjs";
120
122
  import {
121
123
  VERSION,
122
124
  detectHarness,
123
125
  detectLanguage,
126
+ displaySafePath,
124
127
  extractErrorMessage,
125
128
  foldPath,
126
129
  getDb,
@@ -134,7 +137,7 @@ import {
134
137
  runGit,
135
138
  shortFingerprint,
136
139
  toKB
137
- } from "./token-goat-hook-chunk-E6AIPKNF.mjs";
140
+ } from "./token-goat-hook-chunk-2O7P4Z6Q.mjs";
138
141
 
139
142
  // src/hooks_grep.ts
140
143
  function grepIntInput(toolInput, key) {
@@ -311,9 +314,9 @@ function postEditHandlerInner(event) {
311
314
  } catch {
312
315
  }
313
316
  if (meetsSavingsFloor(editedSize)) {
314
- const escapedPath = normalized.replace(/`/g, "\\`").replace(/"/g, '\\"');
317
+ const escapedPath = displaySafePath(normalized).replace(/`/g, "\\`").replace(/"/g, '\\"');
315
318
  return contextOutput(
316
- editedBasename + ' was edited. Use `token-goat section "' + escapedPath + '::HeadingName"` to re-read a specific section rather than the full file.'
319
+ displaySafePath(editedBasename) + ' was edited. Use `token-goat section "' + escapedPath + '::HeadingName"` to re-read a specific section rather than the full file.'
317
320
  );
318
321
  }
319
322
  }
@@ -572,7 +575,7 @@ import crypto from "node:crypto";
572
575
  var TRACKED_SKILL = "token-goat";
573
576
  var MAX_COMMANDS_SHOWN = 8;
574
577
  async function currentCommandNames() {
575
- const { buildProgram } = await import("./token-goat-hook-chunk-WBBHV6TZ.mjs");
578
+ const { buildProgram } = await import("./token-goat-hook-chunk-3AAH72SO.mjs");
576
579
  return flattenCommandNames(buildCommandManifest(buildProgram()));
577
580
  }
578
581
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -7051,6 +7054,10 @@ function stripCdPrefix(cmd) {
7051
7054
  const stripped = cmd.replace(/^(?:cd\s+(?:"[^"]*"|'[^']*'|\S+)\s*&&\s*)+/, "");
7052
7055
  return stripped.trim() || cmd;
7053
7056
  }
7057
+ function stripTrailingStderrRedirect(cmd) {
7058
+ const stripped = cmd.replace(/\s*2>&1\s*$/, "");
7059
+ return stripped.trim() || cmd;
7060
+ }
7054
7061
  function hasTestRunScopeOrBudget(cmd) {
7055
7062
  const hasTimeout = /(?:^|\s)(?:timeout(?:\.exe)?\s+\S+|--(?:test)?timeout(?:=|\s)\S+)/i.test(cmd);
7056
7063
  const hasSelector = /(?:\s::\S+|\s(?:-k|-t|-run|--(?:testNamePattern|last-failed|test|project))(?:=|\s)|\b(?:tests?|spec)\S*\.(?:[cm]?[jt]sx?|py|go|rs)\b|(?:^|\s)\.\/\S+)/i.test(cmd);
@@ -7142,6 +7149,182 @@ function isHeadMovingGitCommand(cmd) {
7142
7149
  }
7143
7150
  var ORIG_HEAD_ELIGIBLE_GIT_RE = /^\s*git\s+(?:pull|merge|rebase)\b/i;
7144
7151
  var ORIG_HEAD_REFLOG_MSG_RE = /^(merge\s|rebase\s\(|pull\s)/i;
7152
+ var GIT_RESTORE_RE = /^\s*git\s+restore\b/i;
7153
+ var GIT_RESTORE_STAGED_ONLY_RE = /(?:^|\s)(?:--staged|-S)(?:\s|$)/;
7154
+ var GIT_STASH_APPLY_RE = /^\s*git\s+stash\s+(?:pop|apply)\b/i;
7155
+ var PATCH_APPLY_SEGMENT_RE = /^\s*(?:\S*[/\\])?(?:git\s+apply|patch)\b/i;
7156
+ var PATCH_APPLY_INSPECT_ONLY_RE = /(?:^|\s)--(?:check|stat|numstat|summary|dry-run)(?:\s|=|$)/i;
7157
+ function isFormatterWriteSegment(segment) {
7158
+ return /(?:^|\s|[/\\])(?:prettier|eslint)\b/i.test(segment) && /(?:^|\s)--(?:write|fix)(?:\s|$)/i.test(segment);
7159
+ }
7160
+ function splitShellSegments(cmd) {
7161
+ const segments = [];
7162
+ let cur = "";
7163
+ let quote = null;
7164
+ for (let i = 0; i < cmd.length; i++) {
7165
+ const ch = cmd[i];
7166
+ if (quote !== null) {
7167
+ cur += ch;
7168
+ if (ch === quote) quote = null;
7169
+ continue;
7170
+ }
7171
+ if (ch === '"' || ch === "'") {
7172
+ quote = ch;
7173
+ cur += ch;
7174
+ continue;
7175
+ }
7176
+ if (ch === "\\" && i + 1 < cmd.length) {
7177
+ cur += ch + cmd[i + 1];
7178
+ i++;
7179
+ continue;
7180
+ }
7181
+ if (ch === "|" || ch === ";" || ch === "\n" || ch === "&") {
7182
+ segments.push(cur);
7183
+ cur = "";
7184
+ if ((ch === "|" || ch === "&") && cmd[i + 1] === ch) i++;
7185
+ continue;
7186
+ }
7187
+ cur += ch;
7188
+ }
7189
+ segments.push(cur);
7190
+ return segments.map((s) => s.trim()).filter((s) => s.length > 0);
7191
+ }
7192
+ function safeShlexSplit(segment) {
7193
+ try {
7194
+ return shlexSplit(segment);
7195
+ } catch {
7196
+ return null;
7197
+ }
7198
+ }
7199
+ function segmentCommandIs(segment, name2) {
7200
+ return new RegExp("^\\s*(?:\\S*[/\\\\])?" + name2 + "(?:\\.exe)?\\b", "i").test(segment);
7201
+ }
7202
+ function extractSedInPlaceFiles(segment) {
7203
+ if (!segmentCommandIs(segment, "sed")) return [];
7204
+ const tokens = safeShlexSplit(segment);
7205
+ if (tokens === null) return [];
7206
+ const rest = tokens.slice(1);
7207
+ const inPlaceIdx = rest.findIndex((t) => t === "--in-place" || t.startsWith("--in-place=") || /^-[a-zA-Z]*i/.test(t));
7208
+ if (inPlaceIdx === -1) return [];
7209
+ const files = [];
7210
+ let scriptTaken = rest.some((t) => t === "-e" || t === "-f" || t.startsWith("--expression") || t.startsWith("--file"));
7211
+ for (let i = 0; i < rest.length; i++) {
7212
+ const t = rest[i];
7213
+ if (i === inPlaceIdx) {
7214
+ if (t === "-i" && rest[i + 1] === "") i++;
7215
+ continue;
7216
+ }
7217
+ if (t === "-e" || t === "-f" || t === "--expression" || t === "--file") {
7218
+ i++;
7219
+ continue;
7220
+ }
7221
+ if (t.length > 1 && t.startsWith("-")) continue;
7222
+ if (!scriptTaken) {
7223
+ scriptTaken = true;
7224
+ continue;
7225
+ }
7226
+ files.push(t);
7227
+ }
7228
+ return files;
7229
+ }
7230
+ function extractTeeFiles(segment) {
7231
+ if (!segmentCommandIs(segment, "tee")) return [];
7232
+ const tokens = safeShlexSplit(segment);
7233
+ if (tokens === null) return [];
7234
+ return tokens.slice(1).filter((t) => t.length > 0 && !t.startsWith("-"));
7235
+ }
7236
+ function extractRedirectTargets(segment) {
7237
+ const targets = [];
7238
+ const re = /(?:^|\s)[0-9]?>>?\s*("[^"]*"|'[^']*'|[^\s|&<>]+)/g;
7239
+ let m;
7240
+ while ((m = re.exec(segment)) !== null) {
7241
+ const raw = m[1];
7242
+ targets.push(raw.length >= 2 && (raw[0] === '"' || raw[0] === "'") ? raw.slice(1, -1) : raw);
7243
+ }
7244
+ return targets;
7245
+ }
7246
+ function extractGitRestorePathspecs(cmd) {
7247
+ const m = /^\s*git\s+restore\b(.*)$/i.exec(cmd);
7248
+ if (m === null) return [];
7249
+ const tokens = safeShlexSplit(m[1] ?? "");
7250
+ if (tokens === null) return [];
7251
+ const dashDash = tokens.indexOf("--");
7252
+ if (dashDash !== -1) return tokens.slice(dashDash + 1);
7253
+ const specs = [];
7254
+ let skipNext = false;
7255
+ for (const t of tokens) {
7256
+ if (skipNext) {
7257
+ skipNext = false;
7258
+ continue;
7259
+ }
7260
+ if (t === "-s" || t === "--source") {
7261
+ skipNext = true;
7262
+ continue;
7263
+ }
7264
+ if (t.startsWith("-")) continue;
7265
+ specs.push(t);
7266
+ }
7267
+ return specs;
7268
+ }
7269
+ function gitRepoRoot(gitDir) {
7270
+ const toplevel = runGit(["rev-parse", "--show-toplevel"], { cwd: gitDir, timeoutMs: 5e3 });
7271
+ return toplevel.exitCode === 0 && toplevel.stdout.trim() !== "" ? toplevel.stdout.trim() : gitDir;
7272
+ }
7273
+ function expandGitPathspecs(specs, gitDir) {
7274
+ if (specs.length === 0) return [];
7275
+ const listed = runGit(["ls-files", "--full-name", "--", ...specs], { cwd: gitDir, timeoutMs: 5e3 });
7276
+ if (listed.exitCode !== 0) return [];
7277
+ const repoRoot = gitRepoRoot(gitDir);
7278
+ return listed.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).map((rel) => resolveIndexPath(rel, repoRoot));
7279
+ }
7280
+ function workingTreeStatusPaths(gitDir) {
7281
+ const status = runGit(["status", "--porcelain"], { cwd: gitDir, timeoutMs: 5e3 });
7282
+ if (status.exitCode !== 0) return [];
7283
+ const repoRoot = gitRepoRoot(gitDir);
7284
+ const out = [];
7285
+ for (const line of status.stdout.split("\n")) {
7286
+ if (line.length < 4) continue;
7287
+ let rel = line.slice(3).trim();
7288
+ if (rel.length === 0) continue;
7289
+ const arrow = rel.lastIndexOf(" -> ");
7290
+ if (arrow !== -1) rel = rel.slice(arrow + 4);
7291
+ if (rel.startsWith('"')) {
7292
+ try {
7293
+ rel = JSON.parse(rel);
7294
+ } catch {
7295
+ continue;
7296
+ }
7297
+ }
7298
+ out.push(resolveIndexPath(rel, repoRoot));
7299
+ }
7300
+ return out;
7301
+ }
7302
+ function enqueueRewrittenPath(absPath) {
7303
+ if (/(?:^|[/\\])(?:NUL|nul)$/.test(absPath) || absPath.replace(/\\/g, "/").endsWith("/dev/null")) return;
7304
+ try {
7305
+ if (!statSync3(absPath).isFile()) return;
7306
+ } catch {
7307
+ return;
7308
+ }
7309
+ enqueueDirtyPathSafe(absPath, { alreadyResolved: true });
7310
+ }
7311
+ function enqueueNonHeadMovingRewrites(cmd, rawCmd, cwd) {
7312
+ const atCwd = (f) => resolveIndexPath(resolveCdHintPath(rawCmd, f, cwd), cwd);
7313
+ const paths = [];
7314
+ let needsStatusSweep = GIT_STASH_APPLY_RE.test(cmd);
7315
+ if (GIT_RESTORE_RE.test(cmd) && !GIT_RESTORE_STAGED_ONLY_RE.test(cmd)) {
7316
+ paths.push(...expandGitPathspecs(extractGitRestorePathspecs(cmd), cwd));
7317
+ }
7318
+ for (const segment of splitShellSegments(cmd)) {
7319
+ if (PATCH_APPLY_SEGMENT_RE.test(segment) && !PATCH_APPLY_INSPECT_ONLY_RE.test(segment)) needsStatusSweep = true;
7320
+ if (isFormatterWriteSegment(segment)) needsStatusSweep = true;
7321
+ for (const f of extractSedInPlaceFiles(segment)) paths.push(atCwd(f));
7322
+ for (const f of extractTeeFiles(segment)) paths.push(atCwd(f));
7323
+ for (const f of extractRedirectTargets(segment)) paths.push(atCwd(f));
7324
+ }
7325
+ if (needsStatusSweep) paths.push(...workingTreeStatusPaths(cwd));
7326
+ for (const p of paths) enqueueRewrittenPath(p);
7327
+ }
7145
7328
  function isTempPath(fp) {
7146
7329
  const norm = fp.replace(/\\/g, "/");
7147
7330
  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/") || isUnderSystemTemp(fp);
@@ -7256,9 +7439,126 @@ function extractWslCatFile(cmd) {
7256
7439
  if (flags === null) return null;
7257
7440
  return { filePath, ...flags };
7258
7441
  }
7442
+ function pythonScanText(text) {
7443
+ const dashC = /^python3?\s+-c\s*(['"])([\s\S]*)\1\s*$/.exec(text);
7444
+ if (dashC) {
7445
+ const source = dashC[2] ?? "";
7446
+ return { source, masked: maskPythonStrings(source) };
7447
+ }
7448
+ if (/^python3?\b/.test(text)) return { source: text, masked: text };
7449
+ return { source: text, masked: maskPythonStrings(text) };
7450
+ }
7451
+ function maskPythonStrings(text) {
7452
+ const out = text.split("");
7453
+ let i = 0;
7454
+ while (i < text.length) {
7455
+ const ch = text[i];
7456
+ if (ch !== "'" && ch !== '"') {
7457
+ i += 1;
7458
+ continue;
7459
+ }
7460
+ const triple = text.slice(i, i + 3);
7461
+ const delim = triple === ch + ch + ch ? triple : ch;
7462
+ let j = i + delim.length;
7463
+ while (j < text.length) {
7464
+ if (text[j] === "\\") {
7465
+ j += 2;
7466
+ continue;
7467
+ }
7468
+ if (text.slice(j, j + delim.length) === delim) break;
7469
+ out[j] = " ";
7470
+ j += 1;
7471
+ }
7472
+ i = j + delim.length;
7473
+ }
7474
+ return out.join("");
7475
+ }
7476
+ function pythonOpenCalls(text) {
7477
+ const { source, masked } = pythonScanText(text);
7478
+ const calls = [];
7479
+ for (const opener of masked.matchAll(/\bopen\s*\(/g)) {
7480
+ const start = (opener.index ?? 0) + opener[0].length;
7481
+ const args = [];
7482
+ let current = "";
7483
+ let depth = 0;
7484
+ let quote = "";
7485
+ let closed = false;
7486
+ for (let i = start; i < source.length; i++) {
7487
+ const ch = source[i] ?? "";
7488
+ if (quote !== "") {
7489
+ if (ch === "\\") {
7490
+ current += ch + (source[i + 1] ?? "");
7491
+ i += 1;
7492
+ continue;
7493
+ }
7494
+ if (ch === quote) quote = "";
7495
+ current += ch;
7496
+ continue;
7497
+ }
7498
+ if (ch === "'" || ch === '"') {
7499
+ quote = ch;
7500
+ current += ch;
7501
+ continue;
7502
+ }
7503
+ if (ch === "(" || ch === "[" || ch === "{") {
7504
+ depth += 1;
7505
+ current += ch;
7506
+ continue;
7507
+ }
7508
+ if (ch === ")" && depth === 0) {
7509
+ args.push(current);
7510
+ closed = true;
7511
+ break;
7512
+ }
7513
+ if (ch === ")" || ch === "]" || ch === "}") {
7514
+ depth -= 1;
7515
+ current += ch;
7516
+ continue;
7517
+ }
7518
+ if (ch === "," && depth === 0) {
7519
+ args.push(current);
7520
+ current = "";
7521
+ continue;
7522
+ }
7523
+ current += ch;
7524
+ }
7525
+ if (!closed) continue;
7526
+ const literalOf = (arg) => {
7527
+ const m = /^\s*r?(['"])([^'"]*)\1\s*$/.exec(arg);
7528
+ return m?.[2] ?? null;
7529
+ };
7530
+ const keyword = args.find((a) => /^\s*mode\s*=/.test(a));
7531
+ const positional = args[1] !== void 0 && /^\s*[A-Za-z_]\w*\s*=[^=]/.test(args[1]) ? void 0 : args[1];
7532
+ const modeArg = keyword !== void 0 ? keyword.replace(/^\s*mode\s*=/, "") : positional;
7533
+ calls.push({
7534
+ pathLiteral: args.length > 0 ? literalOf(args[0] ?? "") : null,
7535
+ hasMode: modeArg !== void 0,
7536
+ modeLiteral: modeArg === void 0 ? null : literalOf(modeArg)
7537
+ });
7538
+ }
7539
+ return calls;
7540
+ }
7541
+ function pythonOpenWritesAFile(text) {
7542
+ return pythonOpenCalls(text).some(
7543
+ (call) => call.hasMode && (call.modeLiteral === null || /[wax+]/.test(call.modeLiteral))
7544
+ );
7545
+ }
7546
+ function pythonWritesThroughFileObject(text) {
7547
+ const { masked } = pythonScanText(text);
7548
+ for (const call of masked.matchAll(/\.write(?:lines)?\s*\(/g)) {
7549
+ const receiver = masked.slice(0, call.index ?? 0);
7550
+ if (/(?:^|[^\w.])(?:sys\s*\.\s*)?(?:stdout|stderr|stdin)(?:\s*\.\s*buffer)?$/.test(receiver)) continue;
7551
+ return true;
7552
+ }
7553
+ return false;
7554
+ }
7555
+ function pythonOpenPathsAreAllLiteral(text) {
7556
+ const calls = pythonOpenCalls(text);
7557
+ return calls.length > 0 && calls.every((call) => call.pathLiteral !== null);
7558
+ }
7259
7559
  function extractPythonFileRead(cmd) {
7260
7560
  if (!/^python3?\b/.test(cmd)) return null;
7261
- if (/open\s*\([^)]*,\s*['"][wa]/i.test(cmd) || /\.write\s*\(/.test(cmd)) return null;
7561
+ if (pythonOpenWritesAFile(cmd) || pythonWritesThroughFileObject(cmd)) return null;
7262
7562
  const outputOpen = /open\s*\(\s*r?['"]([^'"]+\.output)['"]/i.exec(cmd);
7263
7563
  if (outputOpen?.[1]) {
7264
7564
  const filePath = outputOpen[1];
@@ -7269,7 +7569,7 @@ function extractPythonFileRead(cmd) {
7269
7569
  const heredocMatch = /^python3?\s+-\s+<<\s*'?(\w+)'?\s*\n([\s\S]*?)\n\1\s*$/.exec(cmd);
7270
7570
  if (heredocMatch) {
7271
7571
  const body = heredocMatch[2] ?? "";
7272
- if (/open\s*\([^)]*,\s*['"][wa]/i.test(body) || /\.write\s*\(/.test(body) || /\.writelines\s*\(/.test(body)) return null;
7572
+ if (pythonOpenWritesAFile(body) || pythonWritesThroughFileObject(body)) return null;
7273
7573
  const heredocOpen = /open\s*\(\s*r?['"]([^'"]+\.(?: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(body);
7274
7574
  if (heredocOpen?.[1]) {
7275
7575
  const filePath = heredocOpen[1];
@@ -7277,7 +7577,7 @@ function extractPythonFileRead(cmd) {
7277
7577
  const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7278
7578
  return { filePath, isDoc, isTranscript: false };
7279
7579
  }
7280
- if (/open\s*\(/.test(body)) {
7580
+ if (/open\s*\(/.test(body) && !pythonOpenPathsAreAllLiteral(body)) {
7281
7581
  const literal2 = /['"]([^'"]+\.(?: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(body);
7282
7582
  if (literal2?.[1]) {
7283
7583
  const filePath = literal2[1];
@@ -7298,7 +7598,7 @@ function extractPythonFileRead(cmd) {
7298
7598
  const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7299
7599
  return { filePath, isDoc, isTranscript: false };
7300
7600
  }
7301
- if (/open\s*\(/.test(cmd)) {
7601
+ if (/open\s*\(/.test(cmd) && !pythonOpenPathsAreAllLiteral(cmd)) {
7302
7602
  const literal2 = /['"]([^'"]+\.(?: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);
7303
7603
  if (literal2) {
7304
7604
  const filePath = literal2[1] ?? "";
@@ -7345,6 +7645,30 @@ function extractSedRange(cmd) {
7345
7645
  if (isTempPath(filePath)) return null;
7346
7646
  return { filePath, ranges };
7347
7647
  }
7648
+ function extractAwkRange(cmd) {
7649
+ const m = /^awk\s+(?:'([^']+)'|"([^"]+)")\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>\/dev\/null)?\s*$/.exec(cmd);
7650
+ if (!m) return null;
7651
+ const program = m[1] ?? m[2];
7652
+ if (program === void 0) return null;
7653
+ const cmp = /^\s*NR\s*>=\s*(\d+)\s*&&\s*NR\s*<=\s*(\d+)\s*$/.exec(program);
7654
+ const rng = /^\s*NR\s*==\s*(\d+)\s*,\s*NR\s*==\s*(\d+)\s*$/.exec(program);
7655
+ const hit = cmp ?? rng;
7656
+ if (!hit) return null;
7657
+ const start = parseInt(hit[1], 10);
7658
+ const end = parseInt(hit[2], 10);
7659
+ if (start < 1 || end < start) return null;
7660
+ const filePath = m[3] ?? m[4] ?? m[5];
7661
+ if (filePath === void 0) return null;
7662
+ if (isTempPath(filePath)) return null;
7663
+ return { filePath, ranges: [[start, end]] };
7664
+ }
7665
+ function extractLineRangeRead(cmd) {
7666
+ const sed = extractSedRange(cmd);
7667
+ if (sed !== null) return { ...sed, tool: "sed" };
7668
+ const awk = extractAwkRange(cmd);
7669
+ if (awk !== null) return { ...awk, tool: "awk" };
7670
+ return null;
7671
+ }
7348
7672
  var SYMBOL_BEARING_LANGUAGES = /* @__PURE__ */ new Set([
7349
7673
  "python",
7350
7674
  "typescript",
@@ -7375,11 +7699,11 @@ var SYMBOL_BEARING_LANGUAGES = /* @__PURE__ */ new Set([
7375
7699
  "salesforce_metadata",
7376
7700
  "salesforce_markup"
7377
7701
  ]);
7378
- function sedRangeHint(filePath, ranges) {
7702
+ function sedRangeHint(filePath, ranges, tool) {
7379
7703
  const lang = detectLanguage(filePath);
7380
7704
  const rangeReads = ranges.map(([s, e]) => '`token-goat read "' + filePath + "@" + s + "-" + e + '"`');
7381
7705
  const allReads = rangeReads.length === 2 ? rangeReads.join(" and ") : rangeReads.length >= 3 ? rangeReads.slice(0, -1).join(", ") + ", and " + rangeReads[rangeReads.length - 1] : rangeReads[0];
7382
- const prefix = "`sed -n` line-range reads bypass read hooks. ";
7706
+ const prefix = "`" + (tool === "awk" ? "awk" : "sed -n") + "` line-range reads bypass read hooks. ";
7383
7707
  if (lang === "markdown") {
7384
7708
  return prefix + 'For Markdown, `token-goat section "' + filePath + '::<heading>"` extracts a whole section by name (robust to line shifts); or ' + allReads + " for exactly those lines.";
7385
7709
  }
@@ -7404,7 +7728,7 @@ function findRangeOverlap(prior, start, end) {
7404
7728
  return best;
7405
7729
  }
7406
7730
  function sedOverlapHint(filePath, prior, start, end) {
7407
- const base = "You already read lines " + prior[0] + "-" + prior[1] + " of " + filePath + " via an earlier `sed` this session; this read (" + start + "-" + end + ") overlaps. ";
7731
+ const base = "You already read lines " + prior[0] + "-" + prior[1] + " of " + filePath + " via an earlier line-range read this session; this read (" + start + "-" + end + ") overlaps. ";
7408
7732
  const segments = [];
7409
7733
  if (start < prior[0]) segments.push([start, Math.min(end, prior[0] - 1)]);
7410
7734
  if (end > prior[1]) segments.push([Math.max(start, prior[1] + 1), end]);
@@ -7580,12 +7904,151 @@ function extractRgStructuralSearch(cmd) {
7580
7904
  function extractGrepPipeChain(cmd) {
7581
7905
  return /^(?:rg|grep)\b.*\|\s*(?:rg|grep)\b/.test(cmd);
7582
7906
  }
7907
+ function splitCommandWords(cmd) {
7908
+ const words = [];
7909
+ let cur = "";
7910
+ let quote = null;
7911
+ let started = false;
7912
+ for (let i = 0; i < cmd.length; i++) {
7913
+ const ch = cmd[i];
7914
+ if (quote !== null) {
7915
+ if (ch === "\\" && quote === '"' && i + 1 < cmd.length) {
7916
+ cur += cmd[i + 1];
7917
+ i++;
7918
+ continue;
7919
+ }
7920
+ if (ch === quote) quote = null;
7921
+ else cur += ch;
7922
+ continue;
7923
+ }
7924
+ if (ch === "\\" && i + 1 < cmd.length) {
7925
+ cur += cmd[i + 1];
7926
+ i++;
7927
+ started = true;
7928
+ continue;
7929
+ }
7930
+ if (ch === '"' || ch === "'") {
7931
+ quote = ch;
7932
+ started = true;
7933
+ continue;
7934
+ }
7935
+ if (/\s/.test(ch)) {
7936
+ if (started) words.push(cur);
7937
+ cur = "";
7938
+ started = false;
7939
+ continue;
7940
+ }
7941
+ cur += ch;
7942
+ started = true;
7943
+ }
7944
+ if (started) words.push(cur);
7945
+ return words;
7946
+ }
7947
+ var CURL_VALUE_FLAGS = /* @__PURE__ */ new Set([
7948
+ "-H",
7949
+ "--header",
7950
+ "-A",
7951
+ "--user-agent",
7952
+ "-e",
7953
+ "--referer",
7954
+ "-b",
7955
+ "--cookie",
7956
+ "-c",
7957
+ "--cookie-jar",
7958
+ "-o",
7959
+ "--output",
7960
+ "--output-dir",
7961
+ "-u",
7962
+ "--user",
7963
+ "-U",
7964
+ "--proxy-user",
7965
+ "-x",
7966
+ "--proxy",
7967
+ "-X",
7968
+ "--request",
7969
+ "-d",
7970
+ "--data",
7971
+ "--data-raw",
7972
+ "--data-binary",
7973
+ "--data-urlencode",
7974
+ "-F",
7975
+ "--form",
7976
+ "-T",
7977
+ "--upload-file",
7978
+ "-K",
7979
+ "--config",
7980
+ "-w",
7981
+ "--write-out",
7982
+ "-m",
7983
+ "--max-time",
7984
+ "--connect-to",
7985
+ "--resolve",
7986
+ "--retry",
7987
+ "--cacert",
7988
+ "-E",
7989
+ "--cert",
7990
+ "--key",
7991
+ "--range",
7992
+ "-r",
7993
+ "--interface",
7994
+ // Options whose own value is a URL or a host. Leaving these out was the same defect as reading
7995
+ // the first URL in the string: `curl --doh-url https://doh/ -o f https://target` answered with
7996
+ // the DoH resolver rather than the file being fetched. `--url-query` belongs here too -- it
7997
+ // appends query data to the request, it does not name the request target.
7998
+ "--url-query",
7999
+ "--doh-url",
8000
+ "--preproxy",
8001
+ "--proxy1.0",
8002
+ "--socks4",
8003
+ "--socks4a",
8004
+ "--socks5",
8005
+ "--socks5-hostname",
8006
+ "--proxy-header",
8007
+ "--noproxy",
8008
+ "--dns-servers",
8009
+ "--aws-sigv4",
8010
+ "--proxy-cacert",
8011
+ "--proxy-cert",
8012
+ "--proxy-key",
8013
+ "--oauth2-bearer",
8014
+ "--hsts",
8015
+ "--alt-svc",
8016
+ "--etag-save",
8017
+ "--etag-compare",
8018
+ "--trace",
8019
+ "--trace-ascii",
8020
+ "--dump-header",
8021
+ "-D"
8022
+ ]);
7583
8023
  function extractCurlUrl(cmd) {
7584
- const m = /(https?:\/\/[^\s'"]+)/.exec(cmd);
7585
- return m?.[1] ?? null;
8024
+ const words = splitCommandWords(cmd);
8025
+ let positional = null;
8026
+ for (let i = 0; i < words.length; i++) {
8027
+ const w = words[i];
8028
+ if (w === "--url") {
8029
+ const next = words[i + 1];
8030
+ if (next !== void 0 && /^https?:\/\//.test(next)) return next;
8031
+ i++;
8032
+ continue;
8033
+ }
8034
+ const eq = w.indexOf("=");
8035
+ if (w.startsWith("--") && eq > 0) {
8036
+ if (w.slice(0, eq) === "--url" && /^https?:\/\//.test(w.slice(eq + 1))) return w.slice(eq + 1);
8037
+ continue;
8038
+ }
8039
+ if (CURL_VALUE_FLAGS.has(w)) {
8040
+ i++;
8041
+ continue;
8042
+ }
8043
+ if (w.startsWith("-") && w !== "-") continue;
8044
+ if (/^https?:\/\//.test(w)) return w;
8045
+ positional ??= /https?:\/\/[^\s'")}`]+/.exec(w)?.[0] ?? null;
8046
+ }
8047
+ if (positional !== null) return positional;
8048
+ return null;
7586
8049
  }
7587
8050
  function extractTgSurgicalRead(cmd, cwd) {
7588
- const m = /^token-goat\s+(symbol|read|section|skill-body|skill-compact|map)(?:\s+(.*))?$/.exec(cmd);
8051
+ const m = /^(?:token-goat|tg)\s+(symbol|read|section|skill-body|skill-compact|map)(?:\s+(.*))?$/.exec(cmd);
7589
8052
  if (!m) return null;
7590
8053
  const sub = m[1];
7591
8054
  const rest = (m[2] ?? "").trim();
@@ -7664,9 +8127,9 @@ function extractCurlDownload(cmd) {
7664
8127
  const outputPath = outputMatch[1] ?? outputMatch[2] ?? outputMatch[3];
7665
8128
  if (!outputPath) return null;
7666
8129
  if (curlHasUnsafeFlags(cmd)) return null;
7667
- const urlMatch = /(https?:\/\/[^\s'"]+)/.exec(cmd);
7668
- if (!urlMatch?.[1]) return null;
7669
- return { url: urlMatch[1], outputPath };
8130
+ const url = extractCurlUrl(cmd);
8131
+ if (url === null) return null;
8132
+ return { url, outputPath };
7670
8133
  }
7671
8134
  function isTscCommand(cmd) {
7672
8135
  return /^\s*tsc(\s|$)/i.test(cmd);
@@ -7705,11 +8168,12 @@ function maybeCompressRewrite(event, rawCmd, cmd) {
7705
8168
  }
7706
8169
  if (!cfg.enabled) return null;
7707
8170
  if (!canRunWrappedShell()) return null;
7708
- const detected = detectFromCommand(cmd, getCwd(event));
8171
+ const gateCmd = stripTrailingStderrRedirect(cmd);
8172
+ const detected = detectFromCommand(gateCmd, getCwd(event));
7709
8173
  let filterName;
7710
8174
  if (detected !== null) {
7711
8175
  filterName = detected.filter.name;
7712
- } else if (isCompressibleSingleCommand(cmd)) {
8176
+ } else if (isCompressibleSingleCommand(gateCmd)) {
7713
8177
  filterName = "generic";
7714
8178
  } else {
7715
8179
  return null;
@@ -7718,9 +8182,34 @@ function maybeCompressRewrite(event, rawCmd, cmd) {
7718
8182
  const wrapped = `token-goat compress -f ${filterName} --timeout ${cfg.timeout_seconds} -c ${shellQuoteSingle(rawCmd)}`;
7719
8183
  return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: wrapped } };
7720
8184
  }
8185
+ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes) {
8186
+ if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
8187
+ if (isCompressibleSingleCommand(cmd)) return null;
8188
+ if (exitCode !== null && exitCode !== 0) return null;
8189
+ let cfg;
8190
+ try {
8191
+ cfg = loadConfig().bash_compress;
8192
+ } catch {
8193
+ return null;
8194
+ }
8195
+ if (!cfg.enabled || cfg.disabled_filters.includes("generic")) return null;
8196
+ if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return null;
8197
+ const filter3 = filterByName("generic");
8198
+ if (filter3 === null) return null;
8199
+ const compressed = compressOutput(filter3, output, "", exitCode ?? 0, [], {
8200
+ maxLines: cfg.max_lines,
8201
+ maxBytes: cfg.max_bytes
8202
+ });
8203
+ const minNet = resolveMinNetSavingsBytes();
8204
+ if (!compressed.worthApplying(minNet)) return null;
8205
+ const id = await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8206
+ recordStat("bash_compress:generic", compressed.bytesSaved, compressed.tokensSaved);
8207
+ const body = compressed.withMarker(minNet) + "\n[token-goat] full output: bash-output " + id + " --full";
8208
+ return { hookType: "rewriteOutput", updatedOutput: body };
8209
+ }
7721
8210
  function unwrapCompressCommand(executed) {
7722
8211
  const t = executed.trim();
7723
- if (!/^token-goat\s+compress\b/.test(t)) return null;
8212
+ if (!/^(?:token-goat|tg)\s+compress\b/.test(t)) return null;
7724
8213
  let argv;
7725
8214
  try {
7726
8215
  argv = shlexSplit(t);
@@ -7772,6 +8261,28 @@ function detectUnbalancedShellSyntax(cmd) {
7772
8261
  i = j;
7773
8262
  continue;
7774
8263
  }
8264
+ if (ch === "$" && cmd[i + 1] === "'") {
8265
+ let j = i + 2;
8266
+ let closed = false;
8267
+ while (j < cmd.length) {
8268
+ if (cmd[j] === "\\") {
8269
+ j += 2;
8270
+ continue;
8271
+ }
8272
+ if (cmd[j] === "'") {
8273
+ j++;
8274
+ closed = true;
8275
+ break;
8276
+ }
8277
+ j++;
8278
+ }
8279
+ if (!closed) {
8280
+ inSingle = true;
8281
+ break;
8282
+ }
8283
+ i = j;
8284
+ continue;
8285
+ }
7775
8286
  if (ch === "#" && (i === 0 || /\s/.test(cmd[i - 1] ?? ""))) {
7776
8287
  const nl2 = cmd.indexOf("\n", i);
7777
8288
  i = nl2 === -1 ? cmd.length : nl2 + 1;
@@ -7926,9 +8437,9 @@ function preBashHandlerInner(event) {
7926
8437
  "Use `token-goat outline <file>` to see symbol names and line counts without loading files."
7927
8438
  );
7928
8439
  }
7929
- const sedRange = extractSedRange(cmd);
8440
+ const sedRange = extractLineRangeRead(cmd);
7930
8441
  if (sedRange !== null) {
7931
- const { filePath, ranges } = sedRange;
8442
+ const { filePath, ranges, tool } = sedRange;
7932
8443
  const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
7933
8444
  recordStat("session_hint", 0, 0);
7934
8445
  const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
@@ -7944,7 +8455,7 @@ function preBashHandlerInner(event) {
7944
8455
  }
7945
8456
  }
7946
8457
  const hints = [...overlapHints];
7947
- if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges));
8458
+ if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
7948
8459
  return contextOutput(hints.join(" "));
7949
8460
  }
7950
8461
  const catJsonPipe = extractCatJsonPipe(cmd);
@@ -8197,7 +8708,7 @@ function preBashHandlerInner(event) {
8197
8708
  );
8198
8709
  }
8199
8710
  }
8200
- if (!isBuildCommand(cmd) && detectFromCommand(cmd, preHookCwd ?? void 0) === null) return passOutput();
8711
+ if (!isBuildCommand(cmd) && detectFromCommand(stripTrailingStderrRedirect(cmd), preHookCwd ?? void 0) === null) return passOutput();
8201
8712
  const cmdHash = shortFingerprint(stripOutputPipeline(cmd));
8202
8713
  const outputId = getBashOutputId(cmdHash);
8203
8714
  const entryRaw = outputId !== null ? getBashOutput(outputId) : null;
@@ -8312,8 +8823,7 @@ async function postBashHandler(event) {
8312
8823
  }
8313
8824
  const mutationDiff = runGit(["diff", "--name-only", diffBase, "HEAD"], { cwd: gitDir, timeoutMs: 5e3 });
8314
8825
  if (mutationDiff.exitCode === 0) {
8315
- const toplevel = runGit(["rev-parse", "--show-toplevel"], { cwd: gitDir, timeoutMs: 5e3 });
8316
- const repoRoot = toplevel.exitCode === 0 && toplevel.stdout.trim() !== "" ? toplevel.stdout.trim() : gitDir;
8826
+ const repoRoot = gitRepoRoot(gitDir);
8317
8827
  for (const rel of mutationDiff.stdout.split("\n")) {
8318
8828
  const trimmed = rel.trim();
8319
8829
  if (trimmed.length === 0) continue;
@@ -8321,6 +8831,9 @@ async function postBashHandler(event) {
8321
8831
  }
8322
8832
  }
8323
8833
  }
8834
+ if (exitCode === null || exitCode === 0) {
8835
+ enqueueNonHeadMovingRewrites(cmd, rawCmd, cwd ?? process.cwd());
8836
+ }
8324
8837
  const curlDl = extractCurlDownload(cmd);
8325
8838
  if (curlDl !== null && (exitCode === null || exitCode === 0)) {
8326
8839
  const resolvedOutputPath = resolveIndexPath(curlDl.outputPath, cwd ?? process.cwd());
@@ -8376,7 +8889,11 @@ async function postBashHandler(event) {
8376
8889
  recordBashOutput(gitScopedCacheHash, gitScopedCacheId, Buffer.byteLength(output, "utf-8"));
8377
8890
  }
8378
8891
  const isMonitoring = getMonitoringRecallHint(cmd) !== null;
8379
- if (!isMonitoring && !isBuildCommand(cmd) && !isCurlGetCommand(cmd)) return passOutput();
8892
+ if (!isMonitoring && !isBuildCommand(cmd) && !isCurlGetCommand(cmd)) {
8893
+ const compound = await maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes);
8894
+ if (compound !== null) return compound;
8895
+ return passOutput();
8896
+ }
8380
8897
  if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return passOutput();
8381
8898
  const cacheKey = isCurlGetCommand(cmd) ? extractCurlUrl(cmd) ?? cmd : stripOutputPipeline(cmd);
8382
8899
  const simpleHash = shortFingerprint(cacheKey);
@@ -8968,8 +9485,21 @@ function compressGWorkspaceMcpResult(toolName, resultText) {
8968
9485
  const summarized = summarizeAttachments(trimmed);
8969
9486
  return finishWithGenericPass(resultText, summarized);
8970
9487
  }
9488
+ var DATA_URL_BASE64_RE = /data:([a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)/g;
9489
+ var MIN_BASE64_PAYLOAD_LEN = 100;
9490
+ function stripBase64DataUrls(resultText) {
9491
+ let changed = false;
9492
+ const out = resultText.replace(DATA_URL_BASE64_RE, (match, mime, payload) => {
9493
+ if (payload.length < MIN_BASE64_PAYLOAD_LEN) return match;
9494
+ changed = true;
9495
+ return `data:${mime};base64,<stripped ${payload.length} bytes>`;
9496
+ });
9497
+ return changed ? out : null;
9498
+ }
8971
9499
  function compressMcpResultWithPacks(toolName, resultText) {
8972
- return compressGithubMcpResult(toolName, resultText) ?? compressBrowserMcpResult(toolName, resultText) ?? compressGWorkspaceMcpResult(toolName, resultText);
9500
+ const base64Stripped = stripBase64DataUrls(resultText);
9501
+ const text = base64Stripped ?? resultText;
9502
+ return compressGithubMcpResult(toolName, text) ?? compressBrowserMcpResult(toolName, text) ?? compressGWorkspaceMcpResult(toolName, text) ?? base64Stripped;
8973
9503
  }
8974
9504
 
8975
9505
  // src/hooks_mcp.ts
@@ -9489,6 +10019,9 @@ async function relayInProcess(eventName, rawPayload) {
9489
10019
  async function relay(eventName) {
9490
10020
  try {
9491
10021
  if (!isHookEventName(eventName)) {
10022
+ console.error(
10023
+ `[relay] unknown hook event '${eventName}'; nothing ran. Valid events: ${HOOK_EVENTS.join(", ")}`
10024
+ );
9492
10025
  process.stdout.write("{}");
9493
10026
  return;
9494
10027
  }