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-chunk-RQQ4SY2P.mjs";
13
+ } from "./token-goat-chunk-DQIT5JIF.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-OHQ7RBLL.mjs";
32
+ } from "./token-goat-chunk-SIHYQCTM.mjs";
31
33
  import {
32
34
  BODY_FIRST_TOOL_RESPONSE_KEYS,
33
35
  OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
@@ -117,11 +119,12 @@ import {
117
119
  wasCliReadThisSession,
118
120
  wasFileReadThisSession,
119
121
  wasHintShown
120
- } from "./token-goat-chunk-TDYTBCT4.mjs";
122
+ } from "./token-goat-chunk-I2KCEGIP.mjs";
121
123
  import {
122
124
  VERSION,
123
125
  detectHarness,
124
126
  detectLanguage,
127
+ displaySafePath,
125
128
  extractErrorMessage,
126
129
  foldPath,
127
130
  getDb,
@@ -135,7 +138,7 @@ import {
135
138
  runGit,
136
139
  shortFingerprint,
137
140
  toKB
138
- } from "./token-goat-chunk-WFRR2DBG.mjs";
141
+ } from "./token-goat-chunk-K7W3P2TJ.mjs";
139
142
  import "./token-goat-chunk-AO2QD2AG.mjs";
140
143
  import "./token-goat-chunk-AEX54RUZ.mjs";
141
144
 
@@ -314,9 +317,9 @@ function postEditHandlerInner(event) {
314
317
  } catch {
315
318
  }
316
319
  if (meetsSavingsFloor(editedSize)) {
317
- const escapedPath = normalized.replace(/`/g, "\\`").replace(/"/g, '\\"');
320
+ const escapedPath = displaySafePath(normalized).replace(/`/g, "\\`").replace(/"/g, '\\"');
318
321
  return contextOutput(
319
- editedBasename + ' was edited. Use `token-goat section "' + escapedPath + '::HeadingName"` to re-read a specific section rather than the full file.'
322
+ displaySafePath(editedBasename) + ' was edited. Use `token-goat section "' + escapedPath + '::HeadingName"` to re-read a specific section rather than the full file.'
320
323
  );
321
324
  }
322
325
  }
@@ -575,7 +578,7 @@ import crypto from "node:crypto";
575
578
  var TRACKED_SKILL = "token-goat";
576
579
  var MAX_COMMANDS_SHOWN = 8;
577
580
  async function currentCommandNames() {
578
- const { buildProgram } = await import("./token-goat-chunk-HPWPUBAK.mjs");
581
+ const { buildProgram } = await import("./token-goat-chunk-YF67EWRH.mjs");
579
582
  return flattenCommandNames(buildCommandManifest(buildProgram()));
580
583
  }
581
584
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -7054,6 +7057,10 @@ function stripCdPrefix(cmd) {
7054
7057
  const stripped = cmd.replace(/^(?:cd\s+(?:"[^"]*"|'[^']*'|\S+)\s*&&\s*)+/, "");
7055
7058
  return stripped.trim() || cmd;
7056
7059
  }
7060
+ function stripTrailingStderrRedirect(cmd) {
7061
+ const stripped = cmd.replace(/\s*2>&1\s*$/, "");
7062
+ return stripped.trim() || cmd;
7063
+ }
7057
7064
  function hasTestRunScopeOrBudget(cmd) {
7058
7065
  const hasTimeout = /(?:^|\s)(?:timeout(?:\.exe)?\s+\S+|--(?:test)?timeout(?:=|\s)\S+)/i.test(cmd);
7059
7066
  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);
@@ -7145,6 +7152,182 @@ function isHeadMovingGitCommand(cmd) {
7145
7152
  }
7146
7153
  var ORIG_HEAD_ELIGIBLE_GIT_RE = /^\s*git\s+(?:pull|merge|rebase)\b/i;
7147
7154
  var ORIG_HEAD_REFLOG_MSG_RE = /^(merge\s|rebase\s\(|pull\s)/i;
7155
+ var GIT_RESTORE_RE = /^\s*git\s+restore\b/i;
7156
+ var GIT_RESTORE_STAGED_ONLY_RE = /(?:^|\s)(?:--staged|-S)(?:\s|$)/;
7157
+ var GIT_STASH_APPLY_RE = /^\s*git\s+stash\s+(?:pop|apply)\b/i;
7158
+ var PATCH_APPLY_SEGMENT_RE = /^\s*(?:\S*[/\\])?(?:git\s+apply|patch)\b/i;
7159
+ var PATCH_APPLY_INSPECT_ONLY_RE = /(?:^|\s)--(?:check|stat|numstat|summary|dry-run)(?:\s|=|$)/i;
7160
+ function isFormatterWriteSegment(segment) {
7161
+ return /(?:^|\s|[/\\])(?:prettier|eslint)\b/i.test(segment) && /(?:^|\s)--(?:write|fix)(?:\s|$)/i.test(segment);
7162
+ }
7163
+ function splitShellSegments(cmd) {
7164
+ const segments = [];
7165
+ let cur = "";
7166
+ let quote = null;
7167
+ for (let i = 0; i < cmd.length; i++) {
7168
+ const ch = cmd[i];
7169
+ if (quote !== null) {
7170
+ cur += ch;
7171
+ if (ch === quote) quote = null;
7172
+ continue;
7173
+ }
7174
+ if (ch === '"' || ch === "'") {
7175
+ quote = ch;
7176
+ cur += ch;
7177
+ continue;
7178
+ }
7179
+ if (ch === "\\" && i + 1 < cmd.length) {
7180
+ cur += ch + cmd[i + 1];
7181
+ i++;
7182
+ continue;
7183
+ }
7184
+ if (ch === "|" || ch === ";" || ch === "\n" || ch === "&") {
7185
+ segments.push(cur);
7186
+ cur = "";
7187
+ if ((ch === "|" || ch === "&") && cmd[i + 1] === ch) i++;
7188
+ continue;
7189
+ }
7190
+ cur += ch;
7191
+ }
7192
+ segments.push(cur);
7193
+ return segments.map((s) => s.trim()).filter((s) => s.length > 0);
7194
+ }
7195
+ function safeShlexSplit(segment) {
7196
+ try {
7197
+ return shlexSplit(segment);
7198
+ } catch {
7199
+ return null;
7200
+ }
7201
+ }
7202
+ function segmentCommandIs(segment, name2) {
7203
+ return new RegExp("^\\s*(?:\\S*[/\\\\])?" + name2 + "(?:\\.exe)?\\b", "i").test(segment);
7204
+ }
7205
+ function extractSedInPlaceFiles(segment) {
7206
+ if (!segmentCommandIs(segment, "sed")) return [];
7207
+ const tokens = safeShlexSplit(segment);
7208
+ if (tokens === null) return [];
7209
+ const rest = tokens.slice(1);
7210
+ const inPlaceIdx = rest.findIndex((t) => t === "--in-place" || t.startsWith("--in-place=") || /^-[a-zA-Z]*i/.test(t));
7211
+ if (inPlaceIdx === -1) return [];
7212
+ const files = [];
7213
+ let scriptTaken = rest.some((t) => t === "-e" || t === "-f" || t.startsWith("--expression") || t.startsWith("--file"));
7214
+ for (let i = 0; i < rest.length; i++) {
7215
+ const t = rest[i];
7216
+ if (i === inPlaceIdx) {
7217
+ if (t === "-i" && rest[i + 1] === "") i++;
7218
+ continue;
7219
+ }
7220
+ if (t === "-e" || t === "-f" || t === "--expression" || t === "--file") {
7221
+ i++;
7222
+ continue;
7223
+ }
7224
+ if (t.length > 1 && t.startsWith("-")) continue;
7225
+ if (!scriptTaken) {
7226
+ scriptTaken = true;
7227
+ continue;
7228
+ }
7229
+ files.push(t);
7230
+ }
7231
+ return files;
7232
+ }
7233
+ function extractTeeFiles(segment) {
7234
+ if (!segmentCommandIs(segment, "tee")) return [];
7235
+ const tokens = safeShlexSplit(segment);
7236
+ if (tokens === null) return [];
7237
+ return tokens.slice(1).filter((t) => t.length > 0 && !t.startsWith("-"));
7238
+ }
7239
+ function extractRedirectTargets(segment) {
7240
+ const targets = [];
7241
+ const re = /(?:^|\s)[0-9]?>>?\s*("[^"]*"|'[^']*'|[^\s|&<>]+)/g;
7242
+ let m;
7243
+ while ((m = re.exec(segment)) !== null) {
7244
+ const raw = m[1];
7245
+ targets.push(raw.length >= 2 && (raw[0] === '"' || raw[0] === "'") ? raw.slice(1, -1) : raw);
7246
+ }
7247
+ return targets;
7248
+ }
7249
+ function extractGitRestorePathspecs(cmd) {
7250
+ const m = /^\s*git\s+restore\b(.*)$/i.exec(cmd);
7251
+ if (m === null) return [];
7252
+ const tokens = safeShlexSplit(m[1] ?? "");
7253
+ if (tokens === null) return [];
7254
+ const dashDash = tokens.indexOf("--");
7255
+ if (dashDash !== -1) return tokens.slice(dashDash + 1);
7256
+ const specs = [];
7257
+ let skipNext = false;
7258
+ for (const t of tokens) {
7259
+ if (skipNext) {
7260
+ skipNext = false;
7261
+ continue;
7262
+ }
7263
+ if (t === "-s" || t === "--source") {
7264
+ skipNext = true;
7265
+ continue;
7266
+ }
7267
+ if (t.startsWith("-")) continue;
7268
+ specs.push(t);
7269
+ }
7270
+ return specs;
7271
+ }
7272
+ function gitRepoRoot(gitDir) {
7273
+ const toplevel = runGit(["rev-parse", "--show-toplevel"], { cwd: gitDir, timeoutMs: 5e3 });
7274
+ return toplevel.exitCode === 0 && toplevel.stdout.trim() !== "" ? toplevel.stdout.trim() : gitDir;
7275
+ }
7276
+ function expandGitPathspecs(specs, gitDir) {
7277
+ if (specs.length === 0) return [];
7278
+ const listed = runGit(["ls-files", "--full-name", "--", ...specs], { cwd: gitDir, timeoutMs: 5e3 });
7279
+ if (listed.exitCode !== 0) return [];
7280
+ const repoRoot = gitRepoRoot(gitDir);
7281
+ return listed.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).map((rel) => resolveIndexPath(rel, repoRoot));
7282
+ }
7283
+ function workingTreeStatusPaths(gitDir) {
7284
+ const status = runGit(["status", "--porcelain"], { cwd: gitDir, timeoutMs: 5e3 });
7285
+ if (status.exitCode !== 0) return [];
7286
+ const repoRoot = gitRepoRoot(gitDir);
7287
+ const out = [];
7288
+ for (const line of status.stdout.split("\n")) {
7289
+ if (line.length < 4) continue;
7290
+ let rel = line.slice(3).trim();
7291
+ if (rel.length === 0) continue;
7292
+ const arrow = rel.lastIndexOf(" -> ");
7293
+ if (arrow !== -1) rel = rel.slice(arrow + 4);
7294
+ if (rel.startsWith('"')) {
7295
+ try {
7296
+ rel = JSON.parse(rel);
7297
+ } catch {
7298
+ continue;
7299
+ }
7300
+ }
7301
+ out.push(resolveIndexPath(rel, repoRoot));
7302
+ }
7303
+ return out;
7304
+ }
7305
+ function enqueueRewrittenPath(absPath) {
7306
+ if (/(?:^|[/\\])(?:NUL|nul)$/.test(absPath) || absPath.replace(/\\/g, "/").endsWith("/dev/null")) return;
7307
+ try {
7308
+ if (!statSync3(absPath).isFile()) return;
7309
+ } catch {
7310
+ return;
7311
+ }
7312
+ enqueueDirtyPathSafe(absPath, { alreadyResolved: true });
7313
+ }
7314
+ function enqueueNonHeadMovingRewrites(cmd, rawCmd, cwd) {
7315
+ const atCwd = (f) => resolveIndexPath(resolveCdHintPath(rawCmd, f, cwd), cwd);
7316
+ const paths = [];
7317
+ let needsStatusSweep = GIT_STASH_APPLY_RE.test(cmd);
7318
+ if (GIT_RESTORE_RE.test(cmd) && !GIT_RESTORE_STAGED_ONLY_RE.test(cmd)) {
7319
+ paths.push(...expandGitPathspecs(extractGitRestorePathspecs(cmd), cwd));
7320
+ }
7321
+ for (const segment of splitShellSegments(cmd)) {
7322
+ if (PATCH_APPLY_SEGMENT_RE.test(segment) && !PATCH_APPLY_INSPECT_ONLY_RE.test(segment)) needsStatusSweep = true;
7323
+ if (isFormatterWriteSegment(segment)) needsStatusSweep = true;
7324
+ for (const f of extractSedInPlaceFiles(segment)) paths.push(atCwd(f));
7325
+ for (const f of extractTeeFiles(segment)) paths.push(atCwd(f));
7326
+ for (const f of extractRedirectTargets(segment)) paths.push(atCwd(f));
7327
+ }
7328
+ if (needsStatusSweep) paths.push(...workingTreeStatusPaths(cwd));
7329
+ for (const p of paths) enqueueRewrittenPath(p);
7330
+ }
7148
7331
  function isTempPath(fp) {
7149
7332
  const norm = fp.replace(/\\/g, "/");
7150
7333
  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);
@@ -7259,9 +7442,126 @@ function extractWslCatFile(cmd) {
7259
7442
  if (flags === null) return null;
7260
7443
  return { filePath, ...flags };
7261
7444
  }
7445
+ function pythonScanText(text) {
7446
+ const dashC = /^python3?\s+-c\s*(['"])([\s\S]*)\1\s*$/.exec(text);
7447
+ if (dashC) {
7448
+ const source = dashC[2] ?? "";
7449
+ return { source, masked: maskPythonStrings(source) };
7450
+ }
7451
+ if (/^python3?\b/.test(text)) return { source: text, masked: text };
7452
+ return { source: text, masked: maskPythonStrings(text) };
7453
+ }
7454
+ function maskPythonStrings(text) {
7455
+ const out = text.split("");
7456
+ let i = 0;
7457
+ while (i < text.length) {
7458
+ const ch = text[i];
7459
+ if (ch !== "'" && ch !== '"') {
7460
+ i += 1;
7461
+ continue;
7462
+ }
7463
+ const triple = text.slice(i, i + 3);
7464
+ const delim = triple === ch + ch + ch ? triple : ch;
7465
+ let j = i + delim.length;
7466
+ while (j < text.length) {
7467
+ if (text[j] === "\\") {
7468
+ j += 2;
7469
+ continue;
7470
+ }
7471
+ if (text.slice(j, j + delim.length) === delim) break;
7472
+ out[j] = " ";
7473
+ j += 1;
7474
+ }
7475
+ i = j + delim.length;
7476
+ }
7477
+ return out.join("");
7478
+ }
7479
+ function pythonOpenCalls(text) {
7480
+ const { source, masked } = pythonScanText(text);
7481
+ const calls = [];
7482
+ for (const opener of masked.matchAll(/\bopen\s*\(/g)) {
7483
+ const start = (opener.index ?? 0) + opener[0].length;
7484
+ const args = [];
7485
+ let current = "";
7486
+ let depth = 0;
7487
+ let quote = "";
7488
+ let closed = false;
7489
+ for (let i = start; i < source.length; i++) {
7490
+ const ch = source[i] ?? "";
7491
+ if (quote !== "") {
7492
+ if (ch === "\\") {
7493
+ current += ch + (source[i + 1] ?? "");
7494
+ i += 1;
7495
+ continue;
7496
+ }
7497
+ if (ch === quote) quote = "";
7498
+ current += ch;
7499
+ continue;
7500
+ }
7501
+ if (ch === "'" || ch === '"') {
7502
+ quote = ch;
7503
+ current += ch;
7504
+ continue;
7505
+ }
7506
+ if (ch === "(" || ch === "[" || ch === "{") {
7507
+ depth += 1;
7508
+ current += ch;
7509
+ continue;
7510
+ }
7511
+ if (ch === ")" && depth === 0) {
7512
+ args.push(current);
7513
+ closed = true;
7514
+ break;
7515
+ }
7516
+ if (ch === ")" || ch === "]" || ch === "}") {
7517
+ depth -= 1;
7518
+ current += ch;
7519
+ continue;
7520
+ }
7521
+ if (ch === "," && depth === 0) {
7522
+ args.push(current);
7523
+ current = "";
7524
+ continue;
7525
+ }
7526
+ current += ch;
7527
+ }
7528
+ if (!closed) continue;
7529
+ const literalOf = (arg) => {
7530
+ const m = /^\s*r?(['"])([^'"]*)\1\s*$/.exec(arg);
7531
+ return m?.[2] ?? null;
7532
+ };
7533
+ const keyword = args.find((a) => /^\s*mode\s*=/.test(a));
7534
+ const positional = args[1] !== void 0 && /^\s*[A-Za-z_]\w*\s*=[^=]/.test(args[1]) ? void 0 : args[1];
7535
+ const modeArg = keyword !== void 0 ? keyword.replace(/^\s*mode\s*=/, "") : positional;
7536
+ calls.push({
7537
+ pathLiteral: args.length > 0 ? literalOf(args[0] ?? "") : null,
7538
+ hasMode: modeArg !== void 0,
7539
+ modeLiteral: modeArg === void 0 ? null : literalOf(modeArg)
7540
+ });
7541
+ }
7542
+ return calls;
7543
+ }
7544
+ function pythonOpenWritesAFile(text) {
7545
+ return pythonOpenCalls(text).some(
7546
+ (call) => call.hasMode && (call.modeLiteral === null || /[wax+]/.test(call.modeLiteral))
7547
+ );
7548
+ }
7549
+ function pythonWritesThroughFileObject(text) {
7550
+ const { masked } = pythonScanText(text);
7551
+ for (const call of masked.matchAll(/\.write(?:lines)?\s*\(/g)) {
7552
+ const receiver = masked.slice(0, call.index ?? 0);
7553
+ if (/(?:^|[^\w.])(?:sys\s*\.\s*)?(?:stdout|stderr|stdin)(?:\s*\.\s*buffer)?$/.test(receiver)) continue;
7554
+ return true;
7555
+ }
7556
+ return false;
7557
+ }
7558
+ function pythonOpenPathsAreAllLiteral(text) {
7559
+ const calls = pythonOpenCalls(text);
7560
+ return calls.length > 0 && calls.every((call) => call.pathLiteral !== null);
7561
+ }
7262
7562
  function extractPythonFileRead(cmd) {
7263
7563
  if (!/^python3?\b/.test(cmd)) return null;
7264
- if (/open\s*\([^)]*,\s*['"][wa]/i.test(cmd) || /\.write\s*\(/.test(cmd)) return null;
7564
+ if (pythonOpenWritesAFile(cmd) || pythonWritesThroughFileObject(cmd)) return null;
7265
7565
  const outputOpen = /open\s*\(\s*r?['"]([^'"]+\.output)['"]/i.exec(cmd);
7266
7566
  if (outputOpen?.[1]) {
7267
7567
  const filePath = outputOpen[1];
@@ -7272,7 +7572,7 @@ function extractPythonFileRead(cmd) {
7272
7572
  const heredocMatch = /^python3?\s+-\s+<<\s*'?(\w+)'?\s*\n([\s\S]*?)\n\1\s*$/.exec(cmd);
7273
7573
  if (heredocMatch) {
7274
7574
  const body = heredocMatch[2] ?? "";
7275
- if (/open\s*\([^)]*,\s*['"][wa]/i.test(body) || /\.write\s*\(/.test(body) || /\.writelines\s*\(/.test(body)) return null;
7575
+ if (pythonOpenWritesAFile(body) || pythonWritesThroughFileObject(body)) return null;
7276
7576
  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);
7277
7577
  if (heredocOpen?.[1]) {
7278
7578
  const filePath = heredocOpen[1];
@@ -7280,7 +7580,7 @@ function extractPythonFileRead(cmd) {
7280
7580
  const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7281
7581
  return { filePath, isDoc, isTranscript: false };
7282
7582
  }
7283
- if (/open\s*\(/.test(body)) {
7583
+ if (/open\s*\(/.test(body) && !pythonOpenPathsAreAllLiteral(body)) {
7284
7584
  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);
7285
7585
  if (literal2?.[1]) {
7286
7586
  const filePath = literal2[1];
@@ -7301,7 +7601,7 @@ function extractPythonFileRead(cmd) {
7301
7601
  const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7302
7602
  return { filePath, isDoc, isTranscript: false };
7303
7603
  }
7304
- if (/open\s*\(/.test(cmd)) {
7604
+ if (/open\s*\(/.test(cmd) && !pythonOpenPathsAreAllLiteral(cmd)) {
7305
7605
  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);
7306
7606
  if (literal2) {
7307
7607
  const filePath = literal2[1] ?? "";
@@ -7348,6 +7648,30 @@ function extractSedRange(cmd) {
7348
7648
  if (isTempPath(filePath)) return null;
7349
7649
  return { filePath, ranges };
7350
7650
  }
7651
+ function extractAwkRange(cmd) {
7652
+ const m = /^awk\s+(?:'([^']+)'|"([^"]+)")\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>\/dev\/null)?\s*$/.exec(cmd);
7653
+ if (!m) return null;
7654
+ const program = m[1] ?? m[2];
7655
+ if (program === void 0) return null;
7656
+ const cmp = /^\s*NR\s*>=\s*(\d+)\s*&&\s*NR\s*<=\s*(\d+)\s*$/.exec(program);
7657
+ const rng = /^\s*NR\s*==\s*(\d+)\s*,\s*NR\s*==\s*(\d+)\s*$/.exec(program);
7658
+ const hit = cmp ?? rng;
7659
+ if (!hit) return null;
7660
+ const start = parseInt(hit[1], 10);
7661
+ const end = parseInt(hit[2], 10);
7662
+ if (start < 1 || end < start) return null;
7663
+ const filePath = m[3] ?? m[4] ?? m[5];
7664
+ if (filePath === void 0) return null;
7665
+ if (isTempPath(filePath)) return null;
7666
+ return { filePath, ranges: [[start, end]] };
7667
+ }
7668
+ function extractLineRangeRead(cmd) {
7669
+ const sed = extractSedRange(cmd);
7670
+ if (sed !== null) return { ...sed, tool: "sed" };
7671
+ const awk = extractAwkRange(cmd);
7672
+ if (awk !== null) return { ...awk, tool: "awk" };
7673
+ return null;
7674
+ }
7351
7675
  var SYMBOL_BEARING_LANGUAGES = /* @__PURE__ */ new Set([
7352
7676
  "python",
7353
7677
  "typescript",
@@ -7378,11 +7702,11 @@ var SYMBOL_BEARING_LANGUAGES = /* @__PURE__ */ new Set([
7378
7702
  "salesforce_metadata",
7379
7703
  "salesforce_markup"
7380
7704
  ]);
7381
- function sedRangeHint(filePath, ranges) {
7705
+ function sedRangeHint(filePath, ranges, tool) {
7382
7706
  const lang = detectLanguage(filePath);
7383
7707
  const rangeReads = ranges.map(([s, e]) => '`token-goat read "' + filePath + "@" + s + "-" + e + '"`');
7384
7708
  const allReads = rangeReads.length === 2 ? rangeReads.join(" and ") : rangeReads.length >= 3 ? rangeReads.slice(0, -1).join(", ") + ", and " + rangeReads[rangeReads.length - 1] : rangeReads[0];
7385
- const prefix = "`sed -n` line-range reads bypass read hooks. ";
7709
+ const prefix = "`" + (tool === "awk" ? "awk" : "sed -n") + "` line-range reads bypass read hooks. ";
7386
7710
  if (lang === "markdown") {
7387
7711
  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.";
7388
7712
  }
@@ -7407,7 +7731,7 @@ function findRangeOverlap(prior, start, end) {
7407
7731
  return best;
7408
7732
  }
7409
7733
  function sedOverlapHint(filePath, prior, start, end) {
7410
- const base = "You already read lines " + prior[0] + "-" + prior[1] + " of " + filePath + " via an earlier `sed` this session; this read (" + start + "-" + end + ") overlaps. ";
7734
+ 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. ";
7411
7735
  const segments = [];
7412
7736
  if (start < prior[0]) segments.push([start, Math.min(end, prior[0] - 1)]);
7413
7737
  if (end > prior[1]) segments.push([Math.max(start, prior[1] + 1), end]);
@@ -7583,12 +7907,151 @@ function extractRgStructuralSearch(cmd) {
7583
7907
  function extractGrepPipeChain(cmd) {
7584
7908
  return /^(?:rg|grep)\b.*\|\s*(?:rg|grep)\b/.test(cmd);
7585
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
+ ]);
7586
8026
  function extractCurlUrl(cmd) {
7587
- const m = /(https?:\/\/[^\s'"]+)/.exec(cmd);
7588
- 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;
7589
8052
  }
7590
8053
  function extractTgSurgicalRead(cmd, cwd) {
7591
- 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);
7592
8055
  if (!m) return null;
7593
8056
  const sub = m[1];
7594
8057
  const rest = (m[2] ?? "").trim();
@@ -7667,9 +8130,9 @@ function extractCurlDownload(cmd) {
7667
8130
  const outputPath = outputMatch[1] ?? outputMatch[2] ?? outputMatch[3];
7668
8131
  if (!outputPath) return null;
7669
8132
  if (curlHasUnsafeFlags(cmd)) return null;
7670
- const urlMatch = /(https?:\/\/[^\s'"]+)/.exec(cmd);
7671
- if (!urlMatch?.[1]) return null;
7672
- return { url: urlMatch[1], outputPath };
8133
+ const url = extractCurlUrl(cmd);
8134
+ if (url === null) return null;
8135
+ return { url, outputPath };
7673
8136
  }
7674
8137
  function isTscCommand(cmd) {
7675
8138
  return /^\s*tsc(\s|$)/i.test(cmd);
@@ -7708,11 +8171,12 @@ function maybeCompressRewrite(event, rawCmd, cmd) {
7708
8171
  }
7709
8172
  if (!cfg.enabled) return null;
7710
8173
  if (!canRunWrappedShell()) return null;
7711
- const detected = detectFromCommand(cmd, getCwd(event));
8174
+ const gateCmd = stripTrailingStderrRedirect(cmd);
8175
+ const detected = detectFromCommand(gateCmd, getCwd(event));
7712
8176
  let filterName;
7713
8177
  if (detected !== null) {
7714
8178
  filterName = detected.filter.name;
7715
- } else if (isCompressibleSingleCommand(cmd)) {
8179
+ } else if (isCompressibleSingleCommand(gateCmd)) {
7716
8180
  filterName = "generic";
7717
8181
  } else {
7718
8182
  return null;
@@ -7721,9 +8185,34 @@ function maybeCompressRewrite(event, rawCmd, cmd) {
7721
8185
  const wrapped = `token-goat compress -f ${filterName} --timeout ${cfg.timeout_seconds} -c ${shellQuoteSingle(rawCmd)}`;
7722
8186
  return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: wrapped } };
7723
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
+ }
7724
8213
  function unwrapCompressCommand(executed) {
7725
8214
  const t = executed.trim();
7726
- if (!/^token-goat\s+compress\b/.test(t)) return null;
8215
+ if (!/^(?:token-goat|tg)\s+compress\b/.test(t)) return null;
7727
8216
  let argv;
7728
8217
  try {
7729
8218
  argv = shlexSplit(t);
@@ -7775,6 +8264,28 @@ function detectUnbalancedShellSyntax(cmd) {
7775
8264
  i = j;
7776
8265
  continue;
7777
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
+ }
7778
8289
  if (ch === "#" && (i === 0 || /\s/.test(cmd[i - 1] ?? ""))) {
7779
8290
  const nl2 = cmd.indexOf("\n", i);
7780
8291
  i = nl2 === -1 ? cmd.length : nl2 + 1;
@@ -7929,9 +8440,9 @@ function preBashHandlerInner(event) {
7929
8440
  "Use `token-goat outline <file>` to see symbol names and line counts without loading files."
7930
8441
  );
7931
8442
  }
7932
- const sedRange = extractSedRange(cmd);
8443
+ const sedRange = extractLineRangeRead(cmd);
7933
8444
  if (sedRange !== null) {
7934
- const { filePath, ranges } = sedRange;
8445
+ const { filePath, ranges, tool } = sedRange;
7935
8446
  const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
7936
8447
  recordStat("session_hint", 0, 0);
7937
8448
  const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
@@ -7947,7 +8458,7 @@ function preBashHandlerInner(event) {
7947
8458
  }
7948
8459
  }
7949
8460
  const hints = [...overlapHints];
7950
- if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges));
8461
+ if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
7951
8462
  return contextOutput(hints.join(" "));
7952
8463
  }
7953
8464
  const catJsonPipe = extractCatJsonPipe(cmd);
@@ -8200,7 +8711,7 @@ function preBashHandlerInner(event) {
8200
8711
  );
8201
8712
  }
8202
8713
  }
8203
- if (!isBuildCommand(cmd) && detectFromCommand(cmd, preHookCwd ?? void 0) === null) return passOutput();
8714
+ if (!isBuildCommand(cmd) && detectFromCommand(stripTrailingStderrRedirect(cmd), preHookCwd ?? void 0) === null) return passOutput();
8204
8715
  const cmdHash = shortFingerprint(stripOutputPipeline(cmd));
8205
8716
  const outputId = getBashOutputId(cmdHash);
8206
8717
  const entryRaw = outputId !== null ? getBashOutput(outputId) : null;
@@ -8315,8 +8826,7 @@ async function postBashHandler(event) {
8315
8826
  }
8316
8827
  const mutationDiff = runGit(["diff", "--name-only", diffBase, "HEAD"], { cwd: gitDir, timeoutMs: 5e3 });
8317
8828
  if (mutationDiff.exitCode === 0) {
8318
- const toplevel = runGit(["rev-parse", "--show-toplevel"], { cwd: gitDir, timeoutMs: 5e3 });
8319
- const repoRoot = toplevel.exitCode === 0 && toplevel.stdout.trim() !== "" ? toplevel.stdout.trim() : gitDir;
8829
+ const repoRoot = gitRepoRoot(gitDir);
8320
8830
  for (const rel of mutationDiff.stdout.split("\n")) {
8321
8831
  const trimmed = rel.trim();
8322
8832
  if (trimmed.length === 0) continue;
@@ -8324,6 +8834,9 @@ async function postBashHandler(event) {
8324
8834
  }
8325
8835
  }
8326
8836
  }
8837
+ if (exitCode === null || exitCode === 0) {
8838
+ enqueueNonHeadMovingRewrites(cmd, rawCmd, cwd ?? process.cwd());
8839
+ }
8327
8840
  const curlDl = extractCurlDownload(cmd);
8328
8841
  if (curlDl !== null && (exitCode === null || exitCode === 0)) {
8329
8842
  const resolvedOutputPath = resolveIndexPath(curlDl.outputPath, cwd ?? process.cwd());
@@ -8379,7 +8892,11 @@ async function postBashHandler(event) {
8379
8892
  recordBashOutput(gitScopedCacheHash, gitScopedCacheId, Buffer.byteLength(output, "utf-8"));
8380
8893
  }
8381
8894
  const isMonitoring = getMonitoringRecallHint(cmd) !== null;
8382
- 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
+ }
8383
8900
  if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return passOutput();
8384
8901
  const cacheKey = isCurlGetCommand(cmd) ? extractCurlUrl(cmd) ?? cmd : stripOutputPipeline(cmd);
8385
8902
  const simpleHash = shortFingerprint(cacheKey);
@@ -8971,8 +9488,21 @@ function compressGWorkspaceMcpResult(toolName, resultText) {
8971
9488
  const summarized = summarizeAttachments(trimmed);
8972
9489
  return finishWithGenericPass(resultText, summarized);
8973
9490
  }
9491
+ var DATA_URL_BASE64_RE = /data:([a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)/g;
9492
+ var MIN_BASE64_PAYLOAD_LEN = 100;
9493
+ function stripBase64DataUrls(resultText) {
9494
+ let changed = false;
9495
+ const out = resultText.replace(DATA_URL_BASE64_RE, (match, mime, payload) => {
9496
+ if (payload.length < MIN_BASE64_PAYLOAD_LEN) return match;
9497
+ changed = true;
9498
+ return `data:${mime};base64,<stripped ${payload.length} bytes>`;
9499
+ });
9500
+ return changed ? out : null;
9501
+ }
8974
9502
  function compressMcpResultWithPacks(toolName, resultText) {
8975
- return compressGithubMcpResult(toolName, resultText) ?? compressBrowserMcpResult(toolName, resultText) ?? compressGWorkspaceMcpResult(toolName, resultText);
9503
+ const base64Stripped = stripBase64DataUrls(resultText);
9504
+ const text = base64Stripped ?? resultText;
9505
+ return compressGithubMcpResult(toolName, text) ?? compressBrowserMcpResult(toolName, text) ?? compressGWorkspaceMcpResult(toolName, text) ?? base64Stripped;
8976
9506
  }
8977
9507
 
8978
9508
  // src/hooks_mcp.ts
@@ -9492,6 +10022,9 @@ async function relayInProcess(eventName, rawPayload) {
9492
10022
  async function relay(eventName) {
9493
10023
  try {
9494
10024
  if (!isHookEventName(eventName)) {
10025
+ console.error(
10026
+ `[relay] unknown hook event '${eventName}'; nothing ran. Valid events: ${HOOK_EVENTS.join(", ")}`
10027
+ );
9495
10028
  process.stdout.write("{}");
9496
10029
  return;
9497
10030
  }