token-goat 2.9.18 → 2.9.19

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 (42) hide show
  1. package/README.md +2 -0
  2. package/dist/{token-goat-chunk-V3ZWBEHM.mjs → token-goat-chunk-2GTB5RBZ.mjs} +365 -26
  3. package/dist/{token-goat-chunk-ARNRFR6M.mjs → token-goat-chunk-3W5P673V.mjs} +1 -1
  4. package/dist/{token-goat-chunk-YQZE5NS6.mjs → token-goat-chunk-62VJ47WN.mjs} +6 -6
  5. package/dist/{token-goat-chunk-CTOYJ232.mjs → token-goat-chunk-7DGTVBWP.mjs} +45 -8
  6. package/dist/{token-goat-chunk-5MMJAJF3.mjs → token-goat-chunk-AVRD7SBO.mjs} +60 -8
  7. package/dist/{token-goat-chunk-7VLWNQDQ.mjs → token-goat-chunk-BTKDZW7F.mjs} +1 -1
  8. package/dist/{token-goat-chunk-LMFO66YD.mjs → token-goat-chunk-DA43OE7L.mjs} +1 -1
  9. package/dist/{token-goat-chunk-6LGVCCMY.mjs → token-goat-chunk-DA6TIDLI.mjs} +21 -16
  10. package/dist/{token-goat-chunk-UENEL6KX.mjs → token-goat-chunk-DHIXE6GV.mjs} +2 -2
  11. package/dist/{token-goat-chunk-W3E65QOW.mjs → token-goat-chunk-EP7TCWHM.mjs} +304 -46
  12. package/dist/{token-goat-chunk-HIZXQVNU.mjs → token-goat-chunk-EXLLLIC3.mjs} +4 -4
  13. package/dist/{token-goat-chunk-H3FONFBO.mjs → token-goat-chunk-F24NE3FO.mjs} +3 -3
  14. package/dist/{token-goat-chunk-LA3O4JGD.mjs → token-goat-chunk-FJZ5MZK7.mjs} +3 -3
  15. package/dist/{token-goat-chunk-BFEWOMA3.mjs → token-goat-chunk-FQYMZASI.mjs} +632 -352
  16. package/dist/{token-goat-chunk-OWSJJY5R.mjs → token-goat-chunk-FX5DHKGF.mjs} +5 -5
  17. package/dist/{token-goat-chunk-K4DG6EEF.mjs → token-goat-chunk-GYC34EGE.mjs} +7 -8
  18. package/dist/{token-goat-chunk-WFX2V3OF.mjs → token-goat-chunk-HPMMLV37.mjs} +358 -116
  19. package/dist/{token-goat-chunk-I67CPTDD.mjs → token-goat-chunk-IBTFGZBX.mjs} +19 -14
  20. package/dist/{token-goat-chunk-L7VCX33I.mjs → token-goat-chunk-JNIFSVWM.mjs} +36 -10
  21. package/dist/{token-goat-chunk-EPXNOKIV.mjs → token-goat-chunk-KWM6KBPA.mjs} +2 -2
  22. package/dist/{token-goat-chunk-BWCRC23L.mjs → token-goat-chunk-L2UOJBV4.mjs} +21 -20
  23. package/dist/{token-goat-chunk-52IMXJ2L.mjs → token-goat-chunk-LACH3WKS.mjs} +4 -4
  24. package/dist/token-goat-chunk-MU6I4IDI.mjs +337 -0
  25. package/dist/{token-goat-chunk-GRI6HWX3.mjs → token-goat-chunk-NJKNAC37.mjs} +1 -1
  26. package/dist/{token-goat-chunk-ERTXEKB6.mjs → token-goat-chunk-NMTKNYGF.mjs} +2 -0
  27. package/dist/{token-goat-chunk-34OUJ2IE.mjs → token-goat-chunk-NYFYIRG3.mjs} +12 -12
  28. package/dist/{token-goat-chunk-4EXFN2AW.mjs → token-goat-chunk-OZRSREKR.mjs} +2 -2
  29. package/dist/token-goat-chunk-P46RTOAQ.mjs +27 -0
  30. package/dist/{token-goat-chunk-QML7ITHJ.mjs → token-goat-chunk-PIAESKVU.mjs} +139 -23
  31. package/dist/{token-goat-chunk-HPOFR6QX.mjs → token-goat-chunk-S7RFIX4Y.mjs} +180 -50
  32. package/dist/{token-goat-chunk-CA6V36KW.mjs → token-goat-chunk-SOKAPOOC.mjs} +23 -4
  33. package/dist/token-goat-chunk-VJQBYSXG.mjs +20 -0
  34. package/dist/{token-goat-chunk-LTYWLKAG.mjs → token-goat-chunk-WKO5TVNA.mjs} +922 -611
  35. package/dist/{token-goat-chunk-QIDLJGXZ.mjs → token-goat-chunk-ZGQXRYKE.mjs} +29 -5
  36. package/dist/token-goat-hook.mjs +12 -12
  37. package/dist/token-goat.core.mjs +21 -20
  38. package/docs/cli.md +2 -1
  39. package/docs/security.md +1 -1
  40. package/package.json +1 -1
  41. package/dist/token-goat-chunk-2KTZ6J7T.mjs +0 -173
  42. package/dist/token-goat-chunk-RHHFEXKF.mjs +0 -93
@@ -1,23 +1,27 @@
1
1
  import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
+ MAX_CAPTURE_BYTES,
4
5
  canRunWrappedShell,
5
6
  clipToDeliveryCap,
6
7
  deliveredOutputBytes
7
- } from "./token-goat-chunk-RHHFEXKF.mjs";
8
+ } from "./token-goat-chunk-MU6I4IDI.mjs";
8
9
  import {
9
10
  DEFAULT_RECONCILE_BUDGET_MS,
10
11
  HOOK_EVENTS,
12
+ MCP_TOOL_PATTERN,
13
+ RECALL_COMMAND,
11
14
  buildCommandManifest,
12
15
  checkSymbolBodySize,
13
16
  diagnoseSqlFailure,
14
17
  flattenCommandNames,
15
18
  getWebOutput,
16
19
  isReconcileClean,
20
+ projectTranscriptsDir,
17
21
  readStdinJson,
18
22
  reconcileProject,
19
23
  storeWebOutput
20
- } from "./token-goat-chunk-V3ZWBEHM.mjs";
24
+ } from "./token-goat-chunk-2GTB5RBZ.mjs";
21
25
  import {
22
26
  BODY_FIRST_TOOL_RESPONSE_KEYS,
23
27
  GENERIC_SERVED_OUTPUT_KEY,
@@ -32,6 +36,7 @@ import {
32
36
  buildProjectMap,
33
37
  classifyBashHint,
34
38
  classifyEditHint,
39
+ claudeConfigDir,
35
40
  clearCurlDownload,
36
41
  compactPathFor,
37
42
  computeAdaptiveBudget,
@@ -41,6 +46,7 @@ import {
41
46
  emitRewriteIfChanged,
42
47
  enqueueDirtyPathSafe,
43
48
  ensureWorkerAlive,
49
+ escapeHintName,
44
50
  estimateTokens,
45
51
  extractCompactFromMarker,
46
52
  extractToolResponseField,
@@ -84,6 +90,7 @@ import {
84
90
  listSiblingSessionStates,
85
91
  loadSessionCache,
86
92
  loadSessionState,
93
+ logSuppressedDetection,
87
94
  makeDedupHintHandlers,
88
95
  markCompactStale,
89
96
  markCompacted,
@@ -122,6 +129,7 @@ import {
122
129
  registerHook,
123
130
  removeOutstandingAgentSpawn,
124
131
  repeatedSkillBodyHint,
132
+ resolveLineRegions,
125
133
  runHook,
126
134
  saveSessionState,
127
135
  scanAndRecord,
@@ -144,9 +152,10 @@ import {
144
152
  wasFileReadThisSession,
145
153
  wasHintShown,
146
154
  withPreservedBlocks
147
- } from "./token-goat-chunk-BFEWOMA3.mjs";
155
+ } from "./token-goat-chunk-FQYMZASI.mjs";
148
156
  import {
149
157
  BASH_OUTPUT_SUBDIR,
158
+ bashOutputIdSync,
150
159
  commandHash,
151
160
  compressOutput,
152
161
  dedupeConsecutive,
@@ -162,41 +171,47 @@ import {
162
171
  isRewriteWorthwhile,
163
172
  isScopedGitStatusOrDiffStatCommand,
164
173
  isTestRunnerCommand,
174
+ looksLikePytestOutput,
175
+ pathStem,
176
+ pytestFilter,
165
177
  resolveMinNetSavingsBytes,
166
178
  shlexSplit,
167
179
  splitOwnTrailingNotices,
168
180
  storeBashOutput,
169
181
  summarizeOutputDelta
170
- } from "./token-goat-chunk-CTOYJ232.mjs";
182
+ } from "./token-goat-chunk-7DGTVBWP.mjs";
171
183
  import {
172
184
  recordStat,
173
185
  redactSecrets,
174
186
  savedTokensFromBytes,
175
187
  storeBlob,
176
188
  stripAnsiEscapes
177
- } from "./token-goat-chunk-HPOFR6QX.mjs";
189
+ } from "./token-goat-chunk-S7RFIX4Y.mjs";
178
190
  import {
179
191
  extractMarkdownHeadings,
180
192
  formatHeadingTreeParts
181
- } from "./token-goat-chunk-W3E65QOW.mjs";
193
+ } from "./token-goat-chunk-EP7TCWHM.mjs";
182
194
  import {
183
- countSymbols
184
- } from "./token-goat-chunk-I67CPTDD.mjs";
195
+ countSymbols,
196
+ getFileEntry,
197
+ querySymbols
198
+ } from "./token-goat-chunk-IBTFGZBX.mjs";
185
199
  import {
186
200
  detectLanguage,
187
201
  getDb,
188
202
  languageHasFlag
189
- } from "./token-goat-chunk-L7VCX33I.mjs";
203
+ } from "./token-goat-chunk-JNIFSVWM.mjs";
190
204
  import {
191
205
  detectHarness,
192
206
  envBool,
193
207
  envInt,
194
208
  findProject,
209
+ fingerprintFile,
195
210
  getHarnessName,
196
211
  isUnderSystemTemp,
197
212
  loadConfig,
198
213
  shortFingerprint
199
- } from "./token-goat-chunk-CA6V36KW.mjs";
214
+ } from "./token-goat-chunk-SOKAPOOC.mjs";
200
215
  import {
201
216
  ENV_KEYS,
202
217
  IDENTICAL_READ_MIN_BODY_BYTES,
@@ -216,8 +231,9 @@ import {
216
231
  isInsideRoot,
217
232
  runGit,
218
233
  toKB
219
- } from "./token-goat-chunk-QIDLJGXZ.mjs";
234
+ } from "./token-goat-chunk-ZGQXRYKE.mjs";
220
235
  import {
236
+ TOOL_RESULTS_ID_CHARS,
221
237
  UNTRUSTED_TOOL_TAG,
222
238
  displaySafePath,
223
239
  displaySafeText,
@@ -226,7 +242,7 @@ import {
226
242
  normalizePath,
227
243
  resolveIndexPath,
228
244
  toDisplayPath
229
- } from "./token-goat-chunk-ERTXEKB6.mjs";
245
+ } from "./token-goat-chunk-NMTKNYGF.mjs";
230
246
  import {
231
247
  init_define_import_meta_env
232
248
  } from "./token-goat-chunk-A37V4PBF.mjs";
@@ -404,7 +420,7 @@ function grepIntInput(toolInput, key) {
404
420
  function grepSignature(toolInput) {
405
421
  const pattern = toolInput["pattern"];
406
422
  if (typeof pattern !== "string" || pattern === "") return null;
407
- const path5 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
423
+ const path6 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
408
424
  const outputMode = typeof toolInput["output_mode"] === "string" ? toolInput["output_mode"] : "files_with_matches";
409
425
  const glob = typeof toolInput["glob"] === "string" ? toolInput["glob"] : "";
410
426
  const type = typeof toolInput["type"] === "string" ? toolInput["type"] : "";
@@ -419,7 +435,7 @@ function grepSignature(toolInput) {
419
435
  const offset = grepIntInput(toolInput, "offset");
420
436
  return JSON.stringify([
421
437
  pattern,
422
- path5,
438
+ path6,
423
439
  outputMode,
424
440
  glob,
425
441
  type,
@@ -563,8 +579,8 @@ function isBroadCatchAllGlob(pattern, pathArg) {
563
579
  function globSignature(toolInput) {
564
580
  const pattern = toolInput["pattern"];
565
581
  if (typeof pattern !== "string" || pattern === "") return null;
566
- const path5 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
567
- return JSON.stringify([pattern, path5]);
582
+ const path6 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
583
+ return JSON.stringify([pattern, path6]);
568
584
  }
569
585
  var { post: postGlobHandler, pre: preGlobDedupHandler } = makeDedupHintHandlers({
570
586
  toolName: "Glob",
@@ -953,7 +969,7 @@ init_define_import_meta_env();
953
969
  var TRACKED_SKILL = "token-goat";
954
970
  var MAX_COMMANDS_SHOWN = 8;
955
971
  async function currentCommandNames() {
956
- const { buildProgram } = await import("./token-goat-chunk-BWCRC23L.mjs");
972
+ const { buildProgram } = await import("./token-goat-chunk-L2UOJBV4.mjs");
957
973
  return flattenCommandNames(buildCommandManifest(buildProgram()));
958
974
  }
959
975
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -1259,7 +1275,8 @@ function reconcileNote(cwd, indexed) {
1259
1275
  if (drifted > 0) clauses.push(`${countNoun(drifted, "file")} that changed outside this session${breakdown}`);
1260
1276
  if (upgraded > 0) clauses.push(`${countNoun(upgraded, "file")} unchanged on disk but indexed by an older version of token-goat`);
1261
1277
  return `token-goat: reindexing ${clauses.join(" and ")}${truncated}. Symbol lookups may be briefly stale.`;
1262
- } catch {
1278
+ } catch (e) {
1279
+ recordStat("reconcile_note_failed", 0, 0, void 0, extractErrorMessage(e));
1263
1280
  return null;
1264
1281
  }
1265
1282
  }
@@ -1870,11 +1887,11 @@ function mapInner(r, f) {
1870
1887
  function mapOuter(r, f) {
1871
1888
  return r.matched ? f(r) : r;
1872
1889
  }
1873
- function ab(pa, pb, join3) {
1874
- return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join3(ma.value, vb, data, i, j)));
1890
+ function ab(pa, pb, join4) {
1891
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join4(ma.value, vb, data, i, j)));
1875
1892
  }
1876
- function abc(pa, pb, pc, join3) {
1877
- return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join3(ma.value, mb.value, vc, data, i, j))));
1893
+ function abc(pa, pb, pc, join4) {
1894
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join4(ma.value, mb.value, vc, data, i, j))));
1878
1895
  }
1879
1896
  function ahead(p) {
1880
1897
  return (data, i) => mapOuter(p(data, i), (m1) => ({
@@ -5851,8 +5868,8 @@ function trimCharacterEnd(str, char) {
5851
5868
  function unicodeEscape(str) {
5852
5869
  return str.replace(/[\s\S]/g, (c) => "\\u" + c.charCodeAt().toString(16).padStart(4, "0"));
5853
5870
  }
5854
- function get(obj, path5) {
5855
- for (const key of path5) {
5871
+ function get(obj, path6) {
5872
+ for (const key of path6) {
5856
5873
  if (!obj) {
5857
5874
  return void 0;
5858
5875
  }
@@ -7053,8 +7070,8 @@ function withBrackets(str, brackets) {
7053
7070
  const rbr = typeof brackets[1] === "string" ? brackets[1] : "]";
7054
7071
  return lbr + str + rbr;
7055
7072
  }
7056
- function pathRewrite(path5, rewriter, baseUrl, metadata, elem) {
7057
- const modifiedPath = typeof rewriter === "function" ? rewriter(path5, metadata, elem) : path5;
7073
+ function pathRewrite(path6, rewriter, baseUrl, metadata, elem) {
7074
+ const modifiedPath = typeof rewriter === "function" ? rewriter(path6, metadata, elem) : path6;
7058
7075
  return modifiedPath[0] === "/" && baseUrl ? trimCharacterEnd(baseUrl, "/") + modifiedPath : modifiedPath;
7059
7076
  }
7060
7077
  function formatImage(elem, walk, builder, formatOptions) {
@@ -7378,9 +7395,9 @@ function handleDeprecatedOptions(options) {
7378
7395
  options.selectors.push(...tagDefinitions);
7379
7396
  options.selectors = mergeDuplicatesPreferLast(options.selectors, ((s) => s.selector));
7380
7397
  }
7381
- function set(obj, path5, value) {
7382
- const valueKey = path5.pop();
7383
- for (const key of path5) {
7398
+ function set(obj, path6, value) {
7399
+ const valueKey = path6.pop();
7400
+ for (const key of path6) {
7384
7401
  let nested = obj[key];
7385
7402
  if (!nested) {
7386
7403
  nested = {};
@@ -7670,20 +7687,147 @@ registerHook("post_tool_use", postSkillHandler, { toolName: "Skill" });
7670
7687
 
7671
7688
  // src/hooks_bash.ts
7672
7689
  init_define_import_meta_env();
7673
- import { statSync as statSync6, existsSync } from "node:fs";
7674
7690
 
7675
- // src/hooks_bash_commands.ts
7691
+ // src/bash_structural_index.ts
7692
+ init_define_import_meta_env();
7693
+ import { statSync as statSync4 } from "node:fs";
7694
+ var ALLOWED_BINARIES = /* @__PURE__ */ new Set(["rg", "grep", "egrep", "fgrep"]);
7695
+ var PY_DEF_RE = /^\^?(?:async\s+)?def\b(?:\s+([A-Za-z_]\w*))?$/;
7696
+ var PY_CLASS_RE = /^\^?class\b(?:\s+([A-Za-z_]\w*))?$/;
7697
+ var IMPORT_RE = /^\^import\b/;
7698
+ var SHELL_SAFE_UNQUOTED_RE = /^[A-Za-z0-9_.:/+,=@^-]+$/;
7699
+ var SHELL_UNSAFE_IN_EITHER_QUOTING_RE = /[$`"\\\r\n]/;
7700
+ function dualShellArg(s) {
7701
+ if (SHELL_SAFE_UNQUOTED_RE.test(s)) return s;
7702
+ if (SHELL_UNSAFE_IN_EITHER_QUOTING_RE.test(s)) return null;
7703
+ return `"${s}"`;
7704
+ }
7705
+ function detectStructuralIndexRewrite(rawCmd, cwd) {
7706
+ try {
7707
+ const detected = detectFromCommand(rawCmd, cwd);
7708
+ if (detected === null || detected.filter.name !== "grep") return null;
7709
+ const argv = detected.argv;
7710
+ if (argv.length !== 3) return null;
7711
+ const [bin, pattern, rawPath] = argv;
7712
+ if (bin === void 0 || pattern === void 0 || rawPath === void 0) return null;
7713
+ if (!ALLOWED_BINARIES.has(pathStem(bin).toLowerCase())) return null;
7714
+ if (/[*?[\]{}]/.test(rawPath)) return null;
7715
+ const resolved = resolveIndexPath(rawPath, cwd);
7716
+ let stat;
7717
+ try {
7718
+ stat = statSync4(resolved);
7719
+ } catch {
7720
+ return null;
7721
+ }
7722
+ if (!stat.isFile()) return null;
7723
+ const entry = getFileEntry(resolved);
7724
+ if (entry === null) return null;
7725
+ if (entry.sha !== "") {
7726
+ const diskSha = fingerprintFile(resolved);
7727
+ if (diskSha === null || diskSha !== entry.sha) return null;
7728
+ }
7729
+ const pathArg = dualShellArg(rawPath);
7730
+ if (pathArg === null) return null;
7731
+ let target = null;
7732
+ if (DOC_EXT_RE.test(rawPath) && STRUCTURAL_DOC_PATTERN_RE.test(pattern)) {
7733
+ target = { command: `outline ${pathArg}`, kind: "headings" };
7734
+ } else {
7735
+ const lang = detectLanguage(resolved);
7736
+ const trimmedPattern = pattern.trim();
7737
+ if (lang === "python" && languageHasFlag(lang, "grepSource")) {
7738
+ const defMatch = PY_DEF_RE.exec(trimmedPattern);
7739
+ const classMatch = defMatch === null ? PY_CLASS_RE.exec(trimmedPattern) : null;
7740
+ const nameMatch = defMatch ?? classMatch;
7741
+ if (nameMatch !== null) {
7742
+ const name2 = nameMatch[1];
7743
+ if (name2) {
7744
+ const grepArg = dualShellArg("^" + name2);
7745
+ target = grepArg === null ? null : { command: `outline ${pathArg} --grep ${grepArg}`, kind: `symbols named starting with ${name2} (functions, classes or any other kind sharing that name)` };
7746
+ } else {
7747
+ target = { command: `outline ${pathArg}`, kind: "all symbols in the file (not just functions/classes)" };
7748
+ }
7749
+ } else if (IMPORT_RE.test(trimmedPattern)) {
7750
+ target = { command: `imports ${pathArg}`, kind: "imports" };
7751
+ }
7752
+ } else if (lang !== "unknown" && languageHasFlag(lang, "grepSource") && IMPORT_RE.test(trimmedPattern)) {
7753
+ target = { command: `imports ${pathArg}`, kind: "imports" };
7754
+ }
7755
+ }
7756
+ if (target === null) return null;
7757
+ const notice = `[token-goat: rewrote this rg/grep search to an indexed answer (${target.kind}) instead of running it -- re-run your original command yourself for a literal text search]`;
7758
+ const noticeArg = dualShellArg(notice);
7759
+ if (noticeArg === null) return null;
7760
+ return { command: `token-goat --notice ${noticeArg} ${target.command}`, kind: target.kind };
7761
+ } catch {
7762
+ return null;
7763
+ }
7764
+ }
7765
+
7766
+ // src/bash_range_savings.ts
7676
7767
  init_define_import_meta_env();
7677
- import { statSync as statSync5 } from "node:fs";
7768
+ import { statSync as statSync5, readFileSync as readFileSync3 } from "node:fs";
7769
+ var ALL_SYMBOLS_IN_FILE = -1;
7770
+ var REGION_HEADER_BYTES = 56;
7771
+ function rangeSubstituteFor(hintPath, cwd, ranges) {
7772
+ if (ranges.length === 0) return null;
7773
+ try {
7774
+ const resolved = resolveIndexPath(hintPath, cwd);
7775
+ if (!statSync5(resolved).isFile()) return null;
7776
+ const entry = getFileEntry(resolved);
7777
+ if (entry === null) return null;
7778
+ if (entry.sha !== "") {
7779
+ const diskSha = fingerprintFile(resolved);
7780
+ if (diskSha === null || diskSha !== entry.sha) return null;
7781
+ }
7782
+ const lines = readFileSync3(resolved, "utf8").split("\n");
7783
+ const symbols = querySymbols({ filePath: resolved, limit: ALL_SYMBOLS_IN_FILE });
7784
+ if (symbols.length === 0) return null;
7785
+ const bytesOf = (start, end) => Buffer.byteLength(lines.slice(start - 1, Math.min(end, lines.length)).join("\n"), "utf8");
7786
+ const requestedLines = /* @__PURE__ */ new Set();
7787
+ const regionBySpan = /* @__PURE__ */ new Map();
7788
+ for (const [start, end] of ranges) {
7789
+ for (let l = start; l <= Math.min(end, lines.length); l++) requestedLines.add(l);
7790
+ for (const r of resolveLineRegions(symbols, lines.length, start, end)) {
7791
+ const named = symbols.find((s) => s.lineStart === r.start && s.lineEnd === r.end);
7792
+ regionBySpan.set(`${r.start}-${r.end}`, {
7793
+ kind: r.kind,
7794
+ name: r.kind === "symbol" && r.end > r.start && named !== void 0 ? named.name : null,
7795
+ start: r.start,
7796
+ end: r.end
7797
+ });
7798
+ }
7799
+ }
7800
+ if (regionBySpan.size === 0) return null;
7801
+ let requestedBytes = 0;
7802
+ for (const l of requestedLines) requestedBytes += Buffer.byteLength(lines[l - 1] ?? "", "utf8") + 1;
7803
+ const regions = [...regionBySpan.values()].sort((a, b) => a.start - b.start || a.end - b.end);
7804
+ const replacementBytes = regions.reduce((t, r) => t + bytesOf(r.start, r.end) + REGION_HEADER_BYTES, 0);
7805
+ const commands = regions.map(
7806
+ (r) => r.name === null ? `token-goat read "${hintPath}:${r.start}-${r.end}"` : `token-goat read "${hintPath}::${r.name}"`
7807
+ );
7808
+ return { requestedBytes, replacementBytes, commands };
7809
+ } catch {
7810
+ return null;
7811
+ }
7812
+ }
7813
+
7814
+ // src/bash_surgical_target.ts
7815
+ init_define_import_meta_env();
7816
+ import { statSync as statSync7 } from "node:fs";
7678
7817
 
7679
7818
  // src/bash_extractors.ts
7680
7819
  init_define_import_meta_env();
7681
- import { statSync as statSync4, openSync as openSync2, readSync, closeSync as closeSync2 } from "node:fs";
7682
- function surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml = false) {
7683
- return isXml ? 'Use `token-goat xml-outline "' + hintPath + '"` to inspect structure, or `token-goat xml-query "' + hintPath + '" "<selector>"` to query specific nodes.' : isEnv ? 'Use `token-goat config-get "' + hintPath + '" KEY_NAME` to read a specific variable.' : isConfig ? 'Use `token-goat config-get "' + hintPath + '" KEY_NAME` or `token-goat section "' + hintPath + '::sectionName"` to read a specific value.' : isDoc ? 'Use `token-goat section "' + hintPath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + hintPath + '::SymbolName"` to read one function or class.';
7820
+ import { statSync as statSync6, openSync as openSync2, readSync, closeSync as closeSync2 } from "node:fs";
7821
+ function genericSurgicalFallback(shown) {
7822
+ return '`token-goat outline "' + shown + '"`';
7823
+ }
7824
+ function surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml = false, target = null) {
7825
+ const key = target ?? "KEY_NAME";
7826
+ const section = target ?? "SectionHeading";
7827
+ return isXml ? 'Use `token-goat xml-outline "' + hintPath + '"` to inspect structure, or `token-goat xml-query "' + hintPath + '" "<selector>"` to query specific nodes.' : isEnv ? 'Use `token-goat config-get "' + hintPath + '" ' + key + "` to read a specific variable." : isConfig ? target === null ? 'Use `token-goat config-get "' + hintPath + '" KEY_NAME` or `token-goat section "' + hintPath + '::sectionName"` to read a specific value.' : 'Use `token-goat config-get "' + hintPath + '" ' + key + "` to read a specific value, or " + genericSurgicalFallback(hintPath) + " for every key with line ranges." : isDoc ? 'Use `token-goat section "' + hintPath + "::" + section + '"` to read one section' + (target === null ? "." : ", or " + genericSurgicalFallback(hintPath) + " for every heading with line ranges.") : "Use " + genericSurgicalFallback(hintPath) + " to read one function or class.";
7684
7828
  }
7685
7829
  function surgicalHintForConfigDoc(filePath, isConfig, isDoc, isSql, isXml = false) {
7686
- return isXml ? 'Use `token-goat xml-outline "' + filePath + '"` to inspect structure, or `token-goat xml-query "' + filePath + '" "<selector>"` to query specific nodes.' : isConfig ? 'Use `token-goat config-get "' + filePath + '" KEY_NAME` or `token-goat section "' + filePath + '::sectionName"` to read a specific value.' : isSql ? 'Use `token-goat section "' + filePath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.' : isDoc ? 'Use `token-goat section "' + filePath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + filePath + '::SymbolName"` or `token-goat skeleton "' + filePath + '"` to see the file structure.';
7830
+ return isXml ? 'Use `token-goat xml-outline "' + filePath + '"` to inspect structure, or `token-goat xml-query "' + filePath + '" "<selector>"` to query specific nodes.' : isConfig ? 'Use `token-goat config-get "' + filePath + '" KEY_NAME` or `token-goat section "' + filePath + '::sectionName"` to read a specific value.' : isSql ? 'Use `token-goat section "' + filePath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.' : isDoc ? 'Use `token-goat section "' + filePath + '::SectionHeading"` to read one section.' : "Use " + genericSurgicalFallback(filePath) + ' or `token-goat skeleton "' + filePath + '"` to see the file structure.';
7687
7831
  }
7688
7832
  function splitShellSegments(cmd) {
7689
7833
  const segments = [];
@@ -7782,7 +7926,7 @@ var PS_FILE_METHOD_RE = /\[(?:System\.)?IO\.File\]::(?:ReadAllText|ReadAllLines|
7782
7926
  var PS_TEMP_READ_FLOOD_BYTES = 16 * 1024;
7783
7927
  function isLargeFileOnDisk(filePath, floor) {
7784
7928
  try {
7785
- return statSync4(filePath).size >= floor;
7929
+ return statSync6(filePath).size >= floor;
7786
7930
  } catch {
7787
7931
  return false;
7788
7932
  }
@@ -8215,27 +8359,21 @@ function extractLineRangeReadsCompound(cmd) {
8215
8359
  }
8216
8360
  return [...merged.values()];
8217
8361
  }
8218
- function sedRangeHint(filePath, ranges, tool) {
8219
- const lang = detectLanguage(filePath);
8220
- const rangeReads = ranges.map(([s, e]) => '`token-goat read "' + filePath + "@" + s + "-" + e + '"`');
8221
- const allReads = rangeReads.length === 2 ? rangeReads.join(" and ") : rangeReads.length >= 3 ? rangeReads.slice(0, -1).join(", ") + ", and " + rangeReads[rangeReads.length - 1] : rangeReads[0];
8222
- const prefix = "`" + (tool === "awk" ? "awk" : "sed -n") + "` line-range reads bypass read hooks. ";
8223
- if (lang === "markdown") {
8224
- 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.";
8225
- }
8226
- if (lang === "toml" || lang === "json" || lang === "yaml" || lang === "ini") {
8227
- return prefix + 'For config, `token-goat config-get "' + filePath + '" <key>` or `token-goat section "' + filePath + '::<block>"` extracts one value; or ' + allReads + " for exactly those lines.";
8228
- }
8229
- if (languageHasFlag(lang, "symbolBearing")) {
8230
- return prefix + 'For a whole function/class, `token-goat symbol <name>` or `token-goat read "' + filePath + '::<Symbol>"` is robust to line shifts; or ' + allReads + " for exactly those lines.";
8231
- }
8232
- return prefix + "Use " + allReads + " to read exactly those lines.";
8362
+ function sedRangeHint(filePath, ranges, tool, sub) {
8363
+ return "`" + (tool === "awk" ? "awk" : "sed -n") + "` line-range reads bypass read hooks. " + substituteSentence(filePath, ranges, sub);
8233
8364
  }
8234
- function leadingLinesHint(lead, hintPath, start, end, flags, preHookCwd) {
8365
+ function substituteSentence(filePath, ranges, sub) {
8366
+ const spans = ranges.map(([s, e]) => s + "-" + e).join(", ");
8367
+ const reads = sub.commands.map((c) => "`" + c + "`");
8368
+ const allReads = reads.length === 2 ? reads.join(" and ") : reads.length >= 3 ? reads.slice(0, -1).join(", ") + ", and " + reads[reads.length - 1] : reads[0];
8369
+ return "Lines " + spans + " of " + filePath + " cost " + sub.requestedBytes + " bytes; " + allReads + " returns the same content as " + sub.replacementBytes + " bytes, resolved to whole regions and robust to line shifts.";
8370
+ }
8371
+ function leadingLinesHint(lead, hintPath, start, end, preHookCwd, substitute) {
8235
8372
  const key = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
8236
8373
  const prior = findRangeOverlap(getFileLineRanges(key), start, end);
8237
8374
  if (prior !== null) return sedOverlapHint(hintPath, prior, start, end);
8238
- return lead + surgicalHintForConfigDoc(hintPath, flags.isConfig, flags.isDoc, flags.isSql, flags.isXml ?? false);
8375
+ if (substitute === null) return null;
8376
+ return lead + substituteSentence(hintPath, [[start, end]], substitute);
8239
8377
  }
8240
8378
  function findRangeOverlap(prior, start, end) {
8241
8379
  let best = null;
@@ -8379,7 +8517,7 @@ function extractTasksOutput(cmd) {
8379
8517
  return null;
8380
8518
  }
8381
8519
  function extractToolResultsFile(cmd) {
8382
- const toolResultsRe = /[/\\]tool-results[/\\]([a-z0-9-]+)\.txt$/i;
8520
+ const toolResultsRe = new RegExp("[/\\\\]tool-results[/\\\\](" + TOOL_RESULTS_ID_CHARS + ")\\.txt$", "i");
8383
8521
  const catM = /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
8384
8522
  if (catM) {
8385
8523
  const fp = catM[1] ?? catM[2] ?? catM[3];
@@ -8733,7 +8871,43 @@ function extractTerminalXmlParsing(cmd) {
8733
8871
  return null;
8734
8872
  }
8735
8873
 
8874
+ // src/bash_surgical_target.ts
8875
+ var ALL_SYMBOLS_IN_FILE2 = -1;
8876
+ function quotableName(name2) {
8877
+ const safe = escapeHintName(name2);
8878
+ return safe === "" || safe.includes("::") ? null : safe;
8879
+ }
8880
+ function runnableTargetFor(hintPath, cwd, event) {
8881
+ try {
8882
+ if (!commandPathIsTouchable(hintPath, event)) return null;
8883
+ const resolved = resolveIndexPath(hintPath, cwd);
8884
+ if (!statSync7(resolved).isFile()) return null;
8885
+ const entry = getFileEntry(resolved);
8886
+ if (entry === null) return null;
8887
+ if (entry.sha !== "") {
8888
+ const diskSha = fingerprintFile(resolved);
8889
+ if (diskSha === null || diskSha !== entry.sha) return null;
8890
+ }
8891
+ const symbols = querySymbols({ filePath: resolved, limit: ALL_SYMBOLS_IN_FILE2 });
8892
+ if (symbols.length === 0) return null;
8893
+ const inLineOrder = [...symbols].sort((a, b) => a.lineStart - b.lineStart);
8894
+ for (const s of inLineOrder) {
8895
+ const safe = quotableName(s.name);
8896
+ if (safe !== null) return safe;
8897
+ }
8898
+ return null;
8899
+ } catch {
8900
+ return null;
8901
+ }
8902
+ }
8903
+
8904
+ // src/hooks_bash.ts
8905
+ import { statSync as statSync9, existsSync, readFileSync as readFileSync4 } from "node:fs";
8906
+ import * as path4 from "node:path";
8907
+
8736
8908
  // src/hooks_bash_commands.ts
8909
+ init_define_import_meta_env();
8910
+ import { statSync as statSync8 } from "node:fs";
8737
8911
  var CD_PREFIX_RE = /^(?:cd\s+(?:"[^"]*"|'[^']*'|\S+)[ \t]*(?:&&|;|\r?\n)\s*)+/;
8738
8912
  function stripCdPrefix(cmd) {
8739
8913
  const stripped = cmd.replace(CD_PREFIX_RE, "");
@@ -8949,7 +9123,7 @@ function workingTreeStatusPaths(gitDir) {
8949
9123
  function enqueueRewrittenPath(absPath) {
8950
9124
  if (/(?:^|[/\\])(?:NUL|nul)$/.test(absPath) || absPath.replace(/\\/g, "/").endsWith("/dev/null")) return;
8951
9125
  try {
8952
- if (!statSync5(absPath).isFile()) return;
9126
+ if (!statSync8(absPath).isFile()) return;
8953
9127
  } catch {
8954
9128
  return;
8955
9129
  }
@@ -9014,6 +9188,15 @@ function pipelineShapeFilter(cmd, cwd) {
9014
9188
  const detected = detectFromCommand(stripOutputPipeline(cmd), cwd ?? void 0);
9015
9189
  return detected === null ? null : detected.filter;
9016
9190
  }
9191
+ function isFullRecallCommand(cmd) {
9192
+ const forSplit = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
9193
+ if (hasBareBackgroundOrNewline(forSplit)) return false;
9194
+ const first2 = splitShellSegments(forSplit)[0];
9195
+ if (first2 === void 0 || !segmentCommandIs(first2, "token-goat")) return false;
9196
+ const tokens = safeShlexSplit(stripOutputPipeline(first2));
9197
+ if (tokens === null) return false;
9198
+ return Object.values(RECALL_COMMAND).includes(tokens[1] ?? "") && tokens.includes("--full");
9199
+ }
9017
9200
  function pureFileReadPath(cmd) {
9018
9201
  const single = extractCatFile(cmd)?.filePath ?? extractHeadFile(cmd)?.filePath ?? extractTailFile(cmd)?.filePath ?? extractLineRangeRead(cmd)?.filePath;
9019
9202
  if (single !== void 0) return single;
@@ -9218,6 +9401,7 @@ function detectUnbalancedShellSyntax(cmd) {
9218
9401
  function maybeCompressRewrite(event, rawCmd, cmd) {
9219
9402
  if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
9220
9403
  if (event.raw["_tg_harness"] === "vscode") return null;
9404
+ if ((event.raw["_tg_harness"] === "codex" || event.raw["_tg_harness"] === "copilot_cli") && process.platform === "win32") return null;
9221
9405
  let cfg;
9222
9406
  try {
9223
9407
  cfg = loadConfig().bash_compress;
@@ -9297,7 +9481,7 @@ function foldShellReadStructure(cmd, filePath, output, fileKey, cwd) {
9297
9481
  const originalBytes = Buffer.byteLength(output, "utf-8");
9298
9482
  let onDisk;
9299
9483
  try {
9300
- onDisk = statSync6(fileKey).size;
9484
+ onDisk = statSync9(fileKey).size;
9301
9485
  } catch {
9302
9486
  return null;
9303
9487
  }
@@ -9405,6 +9589,7 @@ async function maybeElideServedGenericOutput(cmd, output, exitCode, cwd, cacheMi
9405
9589
  async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes, isUnwrapped = false) {
9406
9590
  if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
9407
9591
  if (!isUnwrapped && isCompressibleSingleCommand(cmd)) return null;
9592
+ if (isFullRecallCommand(cmd)) return null;
9408
9593
  if (exitCode !== null && exitCode !== 0) return null;
9409
9594
  let cfg;
9410
9595
  try {
@@ -9423,7 +9608,7 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
9423
9608
  });
9424
9609
  const minNet = resolveMinNetSavingsBytes();
9425
9610
  if (!compressed.worthApplying(minNet)) return null;
9426
- const id = await commandHash(cmd, cwd);
9611
+ const id = bashOutputIdSync(cmd, output, cwd);
9427
9612
  const { body: untrusted, notices } = splitOwnTrailingNotices(compressed.text);
9428
9613
  const marker = compressed.withMarker(minNet).slice(compressed.text.length);
9429
9614
  const body = fenceUntrusted(untrusted, UNTRUSTED_TOOL_TAG) + notices + marker + "\n[token-goat] full output: bash-output " + id + " --full";
@@ -9502,6 +9687,15 @@ function preBashHandlerInner(event) {
9502
9687
  const cdStripped = cmd !== rawCmd;
9503
9688
  const preHookCwd = getCwd(event) ?? null;
9504
9689
  const hintCwd = preHookCwd ?? process.cwd();
9690
+ const pathHint = (paths, text) => contextOutput(text, typeof paths === "string" ? [paths] : paths);
9691
+ const pricedSubstitute = (hintPath, start, end) => {
9692
+ const sub = rangeSubstituteFor(hintPath, hintCwd, [[start, end]]);
9693
+ return sub !== null && meetsSavingsFloor(sub.requestedBytes - sub.replacementBytes) ? sub : null;
9694
+ };
9695
+ const declineUnpriced = (paths) => {
9696
+ for (const p of typeof paths === "string" ? [paths] : paths) logSuppressedDetection("bash_redirect", event.sessionId, p);
9697
+ return passOutput();
9698
+ };
9505
9699
  const cfg = loadConfig();
9506
9700
  if (cfg.hints.warn_unbalanced_shell_quoting) {
9507
9701
  const quoteError = detectUnbalancedShellSyntax(cmd);
@@ -9562,17 +9756,19 @@ function preBashHandlerInner(event) {
9562
9756
  const { filePath, toolOrScript } = terminalXml;
9563
9757
  recordStat("session_hint", 0, 0);
9564
9758
  const target = filePath ? displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath) : "<file>";
9565
- return contextOutput(
9759
+ return pathHint(
9760
+ filePath ? [target] : [],
9566
9761
  `token-goat available for this file type, consider 'token-goat xml-query "${target}" "<xpath>"' or 'token-goat xml-outline "${target}"' first instead of terminal XML parsing (${toolOrScript}).`
9567
9762
  );
9568
9763
  }
9569
9764
  const singleLineRangeRead = extractLineRangeRead(cmd);
9570
9765
  const sedReads = singleLineRangeRead !== null ? [singleLineRangeRead] : extractLineRangeReadsCompound(cmd);
9571
9766
  if (sedReads !== null) {
9572
- recordStat("session_hint", 0, 0);
9573
9767
  const hints = [];
9768
+ const hintPaths = [];
9574
9769
  for (const { filePath, ranges, tool } of sedReads) {
9575
9770
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9771
+ hintPaths.push(hintPath);
9576
9772
  const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
9577
9773
  const overlapHints = [];
9578
9774
  const freshRanges = [];
@@ -9586,23 +9782,30 @@ function preBashHandlerInner(event) {
9586
9782
  }
9587
9783
  }
9588
9784
  hints.push(...overlapHints);
9589
- if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
9785
+ const sub = freshRanges.length > 0 ? rangeSubstituteFor(hintPath, hintCwd, freshRanges) : null;
9786
+ if (sub !== null && meetsSavingsFloor(sub.requestedBytes - sub.replacementBytes)) {
9787
+ hints.push(sedRangeHint(hintPath, freshRanges, tool, sub));
9788
+ }
9590
9789
  }
9591
- return contextOutput(hints.join(" "));
9790
+ if (hints.length === 0) return declineUnpriced(hintPaths);
9791
+ recordStat("session_hint", 0, 0);
9792
+ return pathHint(hintPaths, hints.join(" "));
9592
9793
  }
9593
9794
  const gcTailResult = extractGetContentTail(cmd);
9594
9795
  if (gcTailResult !== null) {
9595
9796
  const { filePath, isDoc, isConfig, isSql, isXml } = gcTailResult;
9596
9797
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9597
9798
  recordStat("session_hint", 0, 0);
9598
- return contextOutput("`Get-Content -Tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql, isXml));
9799
+ return pathHint(hintPath, "`Get-Content -Tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql, isXml));
9599
9800
  }
9600
9801
  const gcSelectResult = extractGetContentSelectFirst(cmd);
9601
9802
  if (gcSelectResult !== null) {
9602
- const { filePath, isDoc, isConfig, isSql, isXml, n } = gcSelectResult;
9803
+ const { filePath, n } = gcSelectResult;
9603
9804
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9805
+ const gcSelectHint = leadingLinesHint("`Select-Object -First` bypasses read hooks. ", hintPath, 1, n, preHookCwd, pricedSubstitute(hintPath, 1, n));
9806
+ if (gcSelectHint === null) return declineUnpriced(hintPath);
9604
9807
  recordStat("session_hint", 0, 0);
9605
- return contextOutput(leadingLinesHint("`Select-Object -First` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql, isXml }, preHookCwd));
9808
+ return pathHint(hintPath, gcSelectHint);
9606
9809
  }
9607
9810
  const catJsonPipe = extractCatJsonPipe(cmd);
9608
9811
  if (catJsonPipe !== null) {
@@ -9610,7 +9813,8 @@ function preBashHandlerInner(event) {
9610
9813
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9611
9814
  recordStat("session_hint", 0, 0);
9612
9815
  const label = isDirectJq ? "`jq`" : "`cat | jq`";
9613
- return contextOutput(
9816
+ return pathHint(
9817
+ hintPath,
9614
9818
  label + ' loads the whole file. Use `token-goat json-query "' + hintPath + '" "<key>"` or `token-goat config-get "' + hintPath + '" KEY_NAME` to slice one value.'
9615
9819
  );
9616
9820
  }
@@ -9619,7 +9823,8 @@ function preBashHandlerInner(event) {
9619
9823
  recordStat("session_hint", 0, 0);
9620
9824
  if (psJsonPipe.filePath) {
9621
9825
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, psJsonPipe.filePath, hintCwd) : psJsonPipe.filePath);
9622
- return contextOutput(
9826
+ return pathHint(
9827
+ hintPath,
9623
9828
  'PowerShell `ConvertFrom-Json` pipeline detected. Use `token-goat json-query "' + hintPath + '" "<selector>"` to extract fields directly without shell conversion scripts.'
9624
9829
  );
9625
9830
  }
@@ -9633,12 +9838,13 @@ function preBashHandlerInner(event) {
9633
9838
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9634
9839
  recordStat("session_hint", 0, 0);
9635
9840
  if (isSql) {
9636
- return contextOutput(
9841
+ return pathHint(
9842
+ hintPath,
9637
9843
  "`" + cmd0 + '` loads the entire file into context. Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.'
9638
9844
  );
9639
9845
  }
9640
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9641
- return cdStripped || advisoryOnly ? contextOutput("`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
9846
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml, runnableTargetFor(hintPath, hintCwd, event));
9847
+ return cdStripped || advisoryOnly ? pathHint(hintPath, "`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
9642
9848
  }
9643
9849
  const catMulti = extractCatFilesMulti(cmd);
9644
9850
  if (catMulti !== null) {
@@ -9650,7 +9856,7 @@ function preBashHandlerInner(event) {
9650
9856
  return " " + hintPath + " -> `" + how + "`";
9651
9857
  });
9652
9858
  const msg = "`" + cmd0 + "` on multiple files loads them all into context. Read each surgically instead:\n" + perPath.join("\n");
9653
- return cdStripped ? contextOutput(msg) : denyOutput(msg);
9859
+ return cdStripped ? pathHint(catMulti.map((m) => displaySafePath(resolveCdHintPath(rawCmd, m.filePath, hintCwd))), msg) : denyOutput(msg);
9654
9860
  }
9655
9861
  const psGetContentResult = extractPowerShellWrappedGetContent(cmd, event);
9656
9862
  if (psGetContentResult !== null) {
@@ -9659,10 +9865,10 @@ function preBashHandlerInner(event) {
9659
9865
  recordStat("session_hint", 0, 0);
9660
9866
  const lead = "`Get-Content` via a `powershell -Command` wrapper bypasses read hooks and loads the entire file into context. ";
9661
9867
  if (isSql) {
9662
- return contextOutput(lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9868
+ return pathHint(hintPath, lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9663
9869
  }
9664
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9665
- return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9870
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml, runnableTargetFor(hintPath, hintCwd, event));
9871
+ return cdStripped ? pathHint(hintPath, lead + hint) : denyOutput(lead + hint);
9666
9872
  }
9667
9873
  const wslCatResult = extractWslCatFile(cmd);
9668
9874
  if (wslCatResult !== null) {
@@ -9670,12 +9876,13 @@ function preBashHandlerInner(event) {
9670
9876
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9671
9877
  recordStat("session_hint", 0, 0);
9672
9878
  if (isSql) {
9673
- return contextOutput(
9879
+ return pathHint(
9880
+ hintPath,
9674
9881
  '`cat` loads the entire file into context. Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.'
9675
9882
  );
9676
9883
  }
9677
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9678
- return cdStripped ? contextOutput("`cat` loads the entire file into context. " + hint) : denyOutput("`cat` loads the entire file into context. " + hint);
9884
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml, runnableTargetFor(hintPath, hintCwd, event));
9885
+ return cdStripped ? pathHint(hintPath, "`cat` loads the entire file into context. " + hint) : denyOutput("`cat` loads the entire file into context. " + hint);
9679
9886
  }
9680
9887
  const pyRead = extractPythonFileRead(cmd);
9681
9888
  if (pyRead !== null) {
@@ -9685,33 +9892,37 @@ function preBashHandlerInner(event) {
9685
9892
  if (isOutputFile) {
9686
9893
  if (taskOutputIsJsonlTranscript(filePath)) {
9687
9894
  const tHint = 'This `.output` file is a JSONL agent transcript. Use `token-goat bash-output --file "' + hintPath + '" --transcript` to read the assistant text, then narrow with `--grep PATTERN` or `--tail N`, instead of hand-parsing the JSONL.';
9688
- return cdStripped ? contextOutput(tHint) : denyOutput(tHint);
9895
+ return cdStripped ? pathHint(hintPath, tHint) : denyOutput(tHint);
9689
9896
  }
9690
- return contextOutput(
9897
+ return pathHint(
9898
+ hintPath,
9691
9899
  "This `.output` file is a background command's stdout. Use `token-goat bash-output --file \"" + hintPath + '"` to narrow it with `--grep PATTERN`, `--tail N` or `--head N`, instead of reading the whole file.'
9692
9900
  );
9693
9901
  }
9694
9902
  if (isSql) {
9695
- return contextOutput(
9903
+ return pathHint(
9904
+ hintPath,
9696
9905
  'Python `open()` file reads bypass read hooks. Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.'
9697
9906
  );
9698
9907
  }
9699
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9700
- return cdStripped ? contextOutput("Python `open()` file reads bypass read hooks. " + hint) : denyOutput("Python `open()` file reads bypass read hooks. " + hint);
9908
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml, runnableTargetFor(hintPath, hintCwd, event));
9909
+ return cdStripped ? pathHint(hintPath, "Python `open()` file reads bypass read hooks. " + hint) : denyOutput("Python `open()` file reads bypass read hooks. " + hint);
9701
9910
  }
9702
9911
  const tailResult = extractTailFile(cmd);
9703
9912
  if (tailResult !== null) {
9704
9913
  const { filePath, isDoc, isConfig, isSql, isXml } = tailResult;
9705
9914
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9706
9915
  recordStat("session_hint", 0, 0);
9707
- return contextOutput("`tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql, isXml));
9916
+ return pathHint(hintPath, "`tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql, isXml));
9708
9917
  }
9709
9918
  const headResult = extractHeadFile(cmd);
9710
9919
  if (headResult !== null) {
9711
- const { filePath, isDoc, isConfig, isSql, isXml, n } = headResult;
9920
+ const { filePath, n } = headResult;
9712
9921
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9922
+ const headHint = leadingLinesHint("`head` bypasses read hooks. ", hintPath, 1, n, preHookCwd, pricedSubstitute(hintPath, 1, n));
9923
+ if (headHint === null) return declineUnpriced(hintPath);
9713
9924
  recordStat("session_hint", 0, 0);
9714
- return contextOutput(leadingLinesHint("`head` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql, isXml }, preHookCwd));
9925
+ return pathHint(hintPath, headHint);
9715
9926
  }
9716
9927
  const nodeRead = extractNodeFileRead(cmd);
9717
9928
  if (nodeRead !== null) {
@@ -9720,11 +9931,11 @@ function preBashHandlerInner(event) {
9720
9931
  const lead = "Node.js `fs.readFileSync()` bypasses read hooks. ";
9721
9932
  if (isSql) {
9722
9933
  recordStat("session_hint", 0, 0);
9723
- return contextOutput(lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9934
+ return pathHint(hintPath, lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9724
9935
  }
9725
- const hint = isDoc ? 'Use `token-goat section "' + hintPath + '::SectionHeading"` to read one section.' : isConfig ? 'Use `token-goat config-get "' + hintPath + '" KEY_NAME` or `token-goat section "' + hintPath + '::sectionName"` to read a specific value.' : 'Use `token-goat read "' + hintPath + '::SymbolName"` to extract a specific symbol.';
9936
+ const hint = surgicalHintFor(hintPath, false, isConfig, isDoc, false, runnableTargetFor(hintPath, hintCwd, event));
9726
9937
  recordStat("session_hint", 0, 0);
9727
- return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9938
+ return cdStripped ? pathHint(hintPath, lead + hint) : denyOutput(lead + hint);
9728
9939
  }
9729
9940
  const psMethodRead = extractPowerShellFileMethodRead(cmd, event);
9730
9941
  if (psMethodRead !== null) {
@@ -9733,11 +9944,15 @@ function preBashHandlerInner(event) {
9733
9944
  const lead = "PowerShell `[IO.File]::ReadAllText()` bypasses read hooks. ";
9734
9945
  if (isSql) {
9735
9946
  recordStat("session_hint", 0, 0);
9736
- return contextOutput(lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9947
+ return pathHint(hintPath, lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9737
9948
  }
9738
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9949
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml, runnableTargetFor(hintPath, hintCwd, event));
9739
9950
  recordStat("session_hint", 0, 0);
9740
- return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9951
+ return cdStripped ? pathHint(hintPath, lead + hint) : denyOutput(lead + hint);
9952
+ }
9953
+ const structuralRewrite = detectStructuralIndexRewrite(rawCmd, hintCwd);
9954
+ if (structuralRewrite !== null) {
9955
+ return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: structuralRewrite.command } };
9741
9956
  }
9742
9957
  if (extractGrepPipeChain(cmd)) {
9743
9958
  recordStat("session_hint", 0, 0);
@@ -9750,7 +9965,8 @@ function preBashHandlerInner(event) {
9750
9965
  const { filePath } = mdHeadingGrep;
9751
9966
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9752
9967
  recordStat("session_hint", 0, 0);
9753
- return contextOutput(
9968
+ return pathHint(
9969
+ hintPath,
9754
9970
  'Use `token-goat outline "' + hintPath + '"` to get all headings with line ranges \u2014 then `token-goat section "' + hintPath + '::Heading"` to read one section.'
9755
9971
  );
9756
9972
  }
@@ -9759,7 +9975,8 @@ function preBashHandlerInner(event) {
9759
9975
  const { identifier } = rgSymbol;
9760
9976
  recordStat("session_hint", 0, 0);
9761
9977
  return contextOutput(
9762
- "Use `token-goat symbol " + identifier + "` to jump directly to the definition without scanning the file."
9978
+ "Use `token-goat symbol " + identifier + "` to jump directly to the definition without scanning the file.",
9979
+ [identifier]
9763
9980
  );
9764
9981
  }
9765
9982
  const rgStructural = extractRgStructuralSearch(cmd);
@@ -9767,7 +9984,8 @@ function preBashHandlerInner(event) {
9767
9984
  const { filePath } = rgStructural;
9768
9985
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9769
9986
  recordStat("session_hint", 0, 0);
9770
- return contextOutput(
9987
+ return pathHint(
9988
+ hintPath,
9771
9989
  'Searching for code definitions with `rg`/`grep` is slower than surgical reads. Use `token-goat skeleton "' + hintPath + '"` to see all symbols with line numbers, or `token-goat outline "' + hintPath + '"` for symbols with docstrings and line ranges.'
9772
9990
  );
9773
9991
  }
@@ -9799,7 +10017,7 @@ function preBashHandlerInner(event) {
9799
10017
  const prevResolvedPath = prevPath !== null && commandPathIsTouchable(prevPath, event) ? resolveIndexPath(prevPath, preHookCwd ?? process.cwd()) : null;
9800
10018
  if (prevPath !== null && prevResolvedPath !== null && !existsSync(prevResolvedPath)) {
9801
10019
  clearCurlDownload(curlDl.url);
9802
- } else if (prevPath !== null && prevResolvedPath !== null && statSync6(prevResolvedPath).size >= loadConfig().hints.bash_dedup_min_bytes) {
10020
+ } else if (prevPath !== null && prevResolvedPath !== null && statSync9(prevResolvedPath).size >= loadConfig().hints.bash_dedup_min_bytes) {
9803
10021
  recordStat("session_hint", 0, 0);
9804
10022
  return denyOutput(
9805
10023
  "Already downloaded to " + prevPath + " earlier this session. Use `rg '<pattern>' " + prevPath + '` to search it, or `token-goat read "' + prevPath + '::SectionName"` to read a part of it.'
@@ -9861,7 +10079,7 @@ function preBashHandlerInner(event) {
9861
10079
  }
9862
10080
  if (notes.length > 0) {
9863
10081
  recordStat("session_hint", 0, 0);
9864
- return contextOutput(notes.join(" "));
10082
+ return pathHint(tgRead.filePath !== null ? [tgRead.filePath] : [], notes.join(" "));
9865
10083
  }
9866
10084
  return passOutput();
9867
10085
  }
@@ -9893,7 +10111,28 @@ function preBashHandler(event) {
9893
10111
  return applyHintTracking(event, preBashHandlerInner(event), classifyBashHint);
9894
10112
  }
9895
10113
  registerHook("pre_tool_use", preBashHandler, { toolName: "Bash" });
9896
- function extractBashOutput(raw) {
10114
+ function readPersistedBashOutput(resp, cwd, sessionId) {
10115
+ const persistedPath = resp["persistedOutputPath"];
10116
+ if (typeof persistedPath !== "string" || persistedPath === "" || sessionId === "") return null;
10117
+ const root = path4.join(projectTranscriptsDir(cwd ?? process.cwd()), sessionId, "tool-results");
10118
+ if (!isInsideRoot(persistedPath, root)) return null;
10119
+ try {
10120
+ const stat = statSync9(persistedPath);
10121
+ if (!stat.isFile() || stat.size > MAX_CAPTURE_BYTES) return null;
10122
+ const persistedSize = resp["persistedOutputSize"];
10123
+ if (typeof persistedSize === "number" && Math.abs(stat.size - persistedSize) > 8) return null;
10124
+ return readFileSync4(persistedPath, "utf-8");
10125
+ } catch {
10126
+ return null;
10127
+ }
10128
+ }
10129
+ function extractBashOutput(event) {
10130
+ const raw = event.raw;
10131
+ const resp = raw["tool_response"];
10132
+ if (resp !== null && typeof resp === "object") {
10133
+ const persisted = readPersistedBashOutput(resp, getCwd(event) ?? null, event.sessionId);
10134
+ if (persisted !== null) return persisted;
10135
+ }
9897
10136
  return extractToolResponseField(raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
9898
10137
  }
9899
10138
  function extractExitCode(raw) {
@@ -10021,7 +10260,7 @@ async function postBashHandler(event) {
10021
10260
  const isUnwrapped = unwrapCompressCommand(rawCmdRaw) === null;
10022
10261
  const rawCmd = unwrapCompressCommand(rawCmdRaw) ?? rawCmdRaw;
10023
10262
  const cmd = stripCdPrefix(rawCmd);
10024
- const output = extractBashOutput(event.raw);
10263
+ const output = extractBashOutput(event);
10025
10264
  const exitCode = extractExitCode(event.raw);
10026
10265
  const cwd = getCwd(event) ?? null;
10027
10266
  const cacheMinBytes = loadConfig().bash_compress.cache_min_bytes;
@@ -10208,9 +10447,11 @@ function postBashOutputHandler(event) {
10208
10447
  const delta = output.slice(prior.output.length);
10209
10448
  storePollSnapshot(event.sessionId, bashId, output);
10210
10449
  if (Buffer.byteLength(delta, "utf-8") < minBytes) return passOutput();
10450
+ const compressedDelta = looksLikePytestOutput(delta) ? pytestFilter.apply(delta, "", 0, []) : null;
10451
+ const deltaBody = compressedDelta !== null && compressedDelta.worthApplying(resolveMinNetSavingsBytes()) ? compressedDelta.withMarker(resolveMinNetSavingsBytes()) : delta;
10211
10452
  const deltaNotice = `[token-goat: bash_id ${bashId} delta since last poll]
10212
10453
  `;
10213
- const fenced = fenceUntrusted(delta, UNTRUSTED_TOOL_TAG);
10454
+ const fenced = fenceUntrusted(deltaBody, UNTRUSTED_TOOL_TAG);
10214
10455
  const body = `${deltaNotice}${fenced}`;
10215
10456
  if (!isRewriteWorthwhile({
10216
10457
  originalBytes: Buffer.byteLength(output, "utf-8"),
@@ -10332,7 +10573,7 @@ registerHook("post_tool_use", postTaskOutputHandler, { toolName: "TaskOutput" })
10332
10573
 
10333
10574
  // src/hooks_tool_failure.ts
10334
10575
  init_define_import_meta_env();
10335
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync as statSync7, writeFileSync as writeFileSync2 } from "node:fs";
10576
+ import { existsSync as existsSync2, readFileSync as readFileSync5, statSync as statSync10, writeFileSync as writeFileSync2 } from "node:fs";
10336
10577
  import { dirname as dirname2, relative as relative2 } from "node:path";
10337
10578
  var FAILURE_SUFFIX = ".tool-failures.json";
10338
10579
  var MAX_TRACKED_FAILURES = 64;
@@ -10360,7 +10601,7 @@ function ledgerPath(sessionId) {
10360
10601
  }
10361
10602
  function readLedger(target) {
10362
10603
  try {
10363
- const parsed = JSON.parse(readFileSync3(target, "utf8"));
10604
+ const parsed = JSON.parse(readFileSync5(target, "utf8"));
10364
10605
  if (parsed !== null && typeof parsed === "object") {
10365
10606
  const seen = parsed.seen;
10366
10607
  const order = parsed.order;
@@ -10401,9 +10642,9 @@ function diagnoseEditFailure(event, errorText) {
10401
10642
  const absPath = normalizePath(filePath);
10402
10643
  if (!existsSync2(absPath)) return null;
10403
10644
  try {
10404
- const stat = statSync7(absPath);
10645
+ const stat = statSync10(absPath);
10405
10646
  if (stat.size > MAX_EDIT_DIAGNOSE_BYTES) return null;
10406
- const fileContent = readFileSync3(absPath, "utf8");
10647
+ const fileContent = readFileSync5(absPath, "utf8");
10407
10648
  const relDisplay = displaySafeText(relative2(process.cwd(), absPath).replace(/\\/g, "/") || absPath);
10408
10649
  if (isMultiple) {
10409
10650
  const matchLines = [];
@@ -11051,8 +11292,8 @@ The full payload was cached to prevent harness context spill. Slicing commands:
11051
11292
  }
11052
11293
  return passOrFence();
11053
11294
  }
11054
- registerHook("pre_tool_use", preMcpHandler, { toolPattern: "^mcp__" });
11055
- registerHook("post_tool_use", postMcpHandler, { toolPattern: "^mcp__" });
11295
+ registerHook("pre_tool_use", preMcpHandler, { toolPattern: MCP_TOOL_PATTERN });
11296
+ registerHook("post_tool_use", postMcpHandler, { toolPattern: MCP_TOOL_PATTERN });
11056
11297
 
11057
11298
  // src/hooks_websearch.ts
11058
11299
  init_define_import_meta_env();
@@ -11253,8 +11494,7 @@ registerHook("post_tool_use", postBrowserImageHandler, { toolPattern: "^mcp__" }
11253
11494
  init_define_import_meta_env();
11254
11495
  import { createHash as createHash2 } from "node:crypto";
11255
11496
  import * as fs2 from "node:fs";
11256
- import * as os2 from "node:os";
11257
- import * as path4 from "node:path";
11497
+ import * as path5 from "node:path";
11258
11498
  var BRIEFING_TARGET_TOKENS = 550;
11259
11499
  function buildSubagentBriefing() {
11260
11500
  try {
@@ -11479,8 +11719,8 @@ function parseAgentDefinition(text, fallbackName) {
11479
11719
  return { name: name2, restricted: false };
11480
11720
  }
11481
11721
  function findRestrictedAgentNames(roots) {
11482
- const projectAgentsRoot = path4.join(process.cwd(), ".claude", "agents");
11483
- const scanRoots = roots ?? [path4.join(os2.homedir(), ".claude", "agents"), projectAgentsRoot];
11722
+ const projectAgentsRoot = path5.join(process.cwd(), ".claude", "agents");
11723
+ const scanRoots = roots ?? [path5.join(claudeConfigDir(), "agents"), projectAgentsRoot];
11484
11724
  const names = /* @__PURE__ */ new Set();
11485
11725
  const visited = /* @__PURE__ */ new Set();
11486
11726
  let filesSeen = 0;
@@ -11504,7 +11744,7 @@ function findRestrictedAgentNames(roots) {
11504
11744
  }
11505
11745
  for (const entry of entries) {
11506
11746
  if (filesSeen >= ROSTER_WALK_MAX_FILES) return;
11507
- const full = path4.join(dir, entry.name);
11747
+ const full = path5.join(dir, entry.name);
11508
11748
  let stat;
11509
11749
  try {
11510
11750
  stat = fs2.statSync(full);
@@ -11523,12 +11763,12 @@ function findRestrictedAgentNames(roots) {
11523
11763
  } catch {
11524
11764
  continue;
11525
11765
  }
11526
- const parsed = parseAgentDefinition(text, path4.basename(entry.name, path4.extname(entry.name)));
11766
+ const parsed = parseAgentDefinition(text, path5.basename(entry.name, path5.extname(entry.name)));
11527
11767
  if (parsed !== null && parsed.restricted) names.add(parsed.name);
11528
11768
  }
11529
11769
  };
11530
11770
  for (const root of scanRoots) {
11531
- const isProjectRoot = roots === void 0 && path4.resolve(root) === path4.resolve(projectAgentsRoot);
11771
+ const isProjectRoot = roots === void 0 && path5.resolve(root) === path5.resolve(projectAgentsRoot);
11532
11772
  if (!isProjectRoot) {
11533
11773
  walk(root, 0, null);
11534
11774
  continue;
@@ -11658,11 +11898,11 @@ function safeSuggestions(output) {
11658
11898
  if (output.hookType === "context") return { hookType: "context", context: stripUnsafeSuggestions(output.context) };
11659
11899
  return output;
11660
11900
  }
11661
- async function relayInProcess(eventName, rawPayload) {
11901
+ async function relayInProcess(eventName, rawPayload, harnessWaitMs) {
11902
+ if (!isHookEventName(eventName)) {
11903
+ return "{}";
11904
+ }
11662
11905
  try {
11663
- if (!isHookEventName(eventName)) {
11664
- return "{}";
11665
- }
11666
11906
  const harness = detectHarness();
11667
11907
  const payload = eventName === "pre_tool_use" || eventName === "post_tool_use" ? normalizePayload(rawPayload, harnessForNormalization()) : rawPayload;
11668
11908
  const event = buildEvent(eventName, payload);
@@ -11693,6 +11933,8 @@ async function relayInProcess(eventName, rawPayload) {
11693
11933
  return serializeOutput(output, event.eventName, harness, event);
11694
11934
  } catch {
11695
11935
  return "{}";
11936
+ } finally {
11937
+ recordStat(`hook:${eventName}`, 0, 0, void 0, void 0, void 0, harnessWaitMs ?? performance.now());
11696
11938
  }
11697
11939
  }
11698
11940
  async function relay(eventName) {