token-goat 2.9.17 → 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-HRUGS6CO.mjs → token-goat-chunk-2GTB5RBZ.mjs} +365 -26
  3. package/dist/{token-goat-chunk-W2GUUQAQ.mjs → token-goat-chunk-3W5P673V.mjs} +1 -1
  4. package/dist/{token-goat-chunk-5WB7GDH4.mjs → token-goat-chunk-62VJ47WN.mjs} +6 -6
  5. package/dist/{token-goat-chunk-C26ZBGBC.mjs → token-goat-chunk-7DGTVBWP.mjs} +58 -44
  6. package/dist/{token-goat-chunk-GPCCYQSS.mjs → token-goat-chunk-AVRD7SBO.mjs} +60 -8
  7. package/dist/{token-goat-chunk-66224KCZ.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-YFGEUIEO.mjs → token-goat-chunk-DA6TIDLI.mjs} +21 -16
  10. package/dist/{token-goat-chunk-CPHVXKNU.mjs → token-goat-chunk-DHIXE6GV.mjs} +2 -2
  11. package/dist/{token-goat-chunk-73GXYYZR.mjs → token-goat-chunk-EP7TCWHM.mjs} +304 -46
  12. package/dist/{token-goat-chunk-EKGVCAES.mjs → token-goat-chunk-EXLLLIC3.mjs} +4 -4
  13. package/dist/{token-goat-chunk-CICJFVIH.mjs → token-goat-chunk-F24NE3FO.mjs} +3 -3
  14. package/dist/{token-goat-chunk-THBLW56G.mjs → token-goat-chunk-FJZ5MZK7.mjs} +3 -3
  15. package/dist/{token-goat-chunk-PKNOHVWP.mjs → token-goat-chunk-FQYMZASI.mjs} +632 -350
  16. package/dist/{token-goat-chunk-RFFUCQK7.mjs → token-goat-chunk-FX5DHKGF.mjs} +5 -5
  17. package/dist/{token-goat-chunk-RB24DBBS.mjs → token-goat-chunk-GYC34EGE.mjs} +7 -8
  18. package/dist/{token-goat-chunk-2M33Q3O4.mjs → token-goat-chunk-HPMMLV37.mjs} +369 -120
  19. package/dist/{token-goat-chunk-BK3RPPTK.mjs → token-goat-chunk-IBTFGZBX.mjs} +19 -14
  20. package/dist/{token-goat-chunk-MWSVUKVV.mjs → token-goat-chunk-JNIFSVWM.mjs} +36 -10
  21. package/dist/{token-goat-chunk-KZPO4DWC.mjs → token-goat-chunk-KWM6KBPA.mjs} +2 -2
  22. package/dist/{token-goat-chunk-G75WIVFM.mjs → token-goat-chunk-L2UOJBV4.mjs} +21 -20
  23. package/dist/{token-goat-chunk-2XA5ZW4T.mjs → token-goat-chunk-LACH3WKS.mjs} +4 -4
  24. package/dist/token-goat-chunk-MU6I4IDI.mjs +337 -0
  25. package/dist/{token-goat-chunk-SWRKVA3J.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-CKIJWTRQ.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-IDHWKNKZ.mjs → token-goat-chunk-PIAESKVU.mjs} +139 -23
  31. package/dist/{token-goat-chunk-26YUO7MP.mjs → token-goat-chunk-S7RFIX4Y.mjs} +180 -50
  32. package/dist/{token-goat-chunk-2VASS5VR.mjs → token-goat-chunk-SOKAPOOC.mjs} +23 -4
  33. package/dist/token-goat-chunk-VJQBYSXG.mjs +20 -0
  34. package/dist/{token-goat-chunk-7VQMOBPQ.mjs → token-goat-chunk-WKO5TVNA.mjs} +948 -618
  35. package/dist/{token-goat-chunk-UBZCON6N.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 +4 -3
  39. package/docs/security.md +1 -1
  40. package/package.json +1 -1
  41. package/dist/token-goat-chunk-7EGKXBWT.mjs +0 -173
  42. package/dist/token-goat-chunk-GLOM5W5C.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-GLOM5W5C.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-HRUGS6CO.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-PKNOHVWP.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-C26ZBGBC.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-26YUO7MP.mjs";
189
+ } from "./token-goat-chunk-S7RFIX4Y.mjs";
178
190
  import {
179
191
  extractMarkdownHeadings,
180
192
  formatHeadingTreeParts
181
- } from "./token-goat-chunk-73GXYYZR.mjs";
193
+ } from "./token-goat-chunk-EP7TCWHM.mjs";
182
194
  import {
183
- countSymbols
184
- } from "./token-goat-chunk-BK3RPPTK.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-MWSVUKVV.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-2VASS5VR.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-UBZCON6N.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 path4 = 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
- path4,
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 path4 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
567
- return JSON.stringify([pattern, path4]);
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",
@@ -718,6 +734,7 @@ registerHook("pre_tool_use", preWriteRewriteHandler, { toolName: "Write" });
718
734
  // src/hooks_compact.ts
719
735
  init_define_import_meta_env();
720
736
  import { spawnSync } from "node:child_process";
737
+ import * as path3 from "node:path";
721
738
  var MEM_EPOCH_TIMEOUT_MS = 800;
722
739
  var MAX_ROWS = 40;
723
740
  function appendCappedSection(lines, header, rows, cap) {
@@ -920,8 +937,14 @@ function postCompactHandler(event) {
920
937
  const summary = typeof raw === "string" ? raw : "";
921
938
  const bytes = Buffer.byteLength(summary, "utf-8");
922
939
  const sample = manifestPathSample(event.sessionId);
923
- const haystack = foldPath(summary);
924
- const survived = sample.filter((p) => haystack.includes(foldPath(p))).length;
940
+ const cwd = normalizePath(getCwd(event) ?? process.cwd());
941
+ const spell = (p) => foldPath(p.replaceAll("\\", "/"));
942
+ const haystack = spell(summary);
943
+ const survived = sample.filter((p) => {
944
+ const rel = path3.relative(cwd, p);
945
+ const forms = rel !== "" && !rel.startsWith("..") && !path3.isAbsolute(rel) ? [p, rel] : [p];
946
+ return forms.some((f) => haystack.includes(spell(f)));
947
+ }).length;
925
948
  const trigger = typeof event.raw["trigger"] === "string" ? event.raw["trigger"] : "unknown";
926
949
  const budget = loadConfig().compact_assist.summary_budget_chars;
927
950
  const over = budget > 0 && summary.length > budget ? 1 : 0;
@@ -946,7 +969,7 @@ init_define_import_meta_env();
946
969
  var TRACKED_SKILL = "token-goat";
947
970
  var MAX_COMMANDS_SHOWN = 8;
948
971
  async function currentCommandNames() {
949
- const { buildProgram } = await import("./token-goat-chunk-G75WIVFM.mjs");
972
+ const { buildProgram } = await import("./token-goat-chunk-L2UOJBV4.mjs");
950
973
  return flattenCommandNames(buildCommandManifest(buildProgram()));
951
974
  }
952
975
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -1252,7 +1275,8 @@ function reconcileNote(cwd, indexed) {
1252
1275
  if (drifted > 0) clauses.push(`${countNoun(drifted, "file")} that changed outside this session${breakdown}`);
1253
1276
  if (upgraded > 0) clauses.push(`${countNoun(upgraded, "file")} unchanged on disk but indexed by an older version of token-goat`);
1254
1277
  return `token-goat: reindexing ${clauses.join(" and ")}${truncated}. Symbol lookups may be briefly stale.`;
1255
- } catch {
1278
+ } catch (e) {
1279
+ recordStat("reconcile_note_failed", 0, 0, void 0, extractErrorMessage(e));
1256
1280
  return null;
1257
1281
  }
1258
1282
  }
@@ -1863,11 +1887,11 @@ function mapInner(r, f) {
1863
1887
  function mapOuter(r, f) {
1864
1888
  return r.matched ? f(r) : r;
1865
1889
  }
1866
- function ab(pa, pb, join3) {
1867
- 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)));
1868
1892
  }
1869
- function abc(pa, pb, pc, join3) {
1870
- 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))));
1871
1895
  }
1872
1896
  function ahead(p) {
1873
1897
  return (data, i) => mapOuter(p(data, i), (m1) => ({
@@ -5844,8 +5868,8 @@ function trimCharacterEnd(str, char) {
5844
5868
  function unicodeEscape(str) {
5845
5869
  return str.replace(/[\s\S]/g, (c) => "\\u" + c.charCodeAt().toString(16).padStart(4, "0"));
5846
5870
  }
5847
- function get(obj, path4) {
5848
- for (const key of path4) {
5871
+ function get(obj, path6) {
5872
+ for (const key of path6) {
5849
5873
  if (!obj) {
5850
5874
  return void 0;
5851
5875
  }
@@ -7046,8 +7070,8 @@ function withBrackets(str, brackets) {
7046
7070
  const rbr = typeof brackets[1] === "string" ? brackets[1] : "]";
7047
7071
  return lbr + str + rbr;
7048
7072
  }
7049
- function pathRewrite(path4, rewriter, baseUrl, metadata, elem) {
7050
- const modifiedPath = typeof rewriter === "function" ? rewriter(path4, metadata, elem) : path4;
7073
+ function pathRewrite(path6, rewriter, baseUrl, metadata, elem) {
7074
+ const modifiedPath = typeof rewriter === "function" ? rewriter(path6, metadata, elem) : path6;
7051
7075
  return modifiedPath[0] === "/" && baseUrl ? trimCharacterEnd(baseUrl, "/") + modifiedPath : modifiedPath;
7052
7076
  }
7053
7077
  function formatImage(elem, walk, builder, formatOptions) {
@@ -7371,9 +7395,9 @@ function handleDeprecatedOptions(options) {
7371
7395
  options.selectors.push(...tagDefinitions);
7372
7396
  options.selectors = mergeDuplicatesPreferLast(options.selectors, ((s) => s.selector));
7373
7397
  }
7374
- function set(obj, path4, value) {
7375
- const valueKey = path4.pop();
7376
- for (const key of path4) {
7398
+ function set(obj, path6, value) {
7399
+ const valueKey = path6.pop();
7400
+ for (const key of path6) {
7377
7401
  let nested = obj[key];
7378
7402
  if (!nested) {
7379
7403
  nested = {};
@@ -7663,20 +7687,147 @@ registerHook("post_tool_use", postSkillHandler, { toolName: "Skill" });
7663
7687
 
7664
7688
  // src/hooks_bash.ts
7665
7689
  init_define_import_meta_env();
7666
- import { statSync as statSync6, existsSync } from "node:fs";
7667
7690
 
7668
- // 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
7669
7767
  init_define_import_meta_env();
7670
- 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";
7671
7817
 
7672
7818
  // src/bash_extractors.ts
7673
7819
  init_define_import_meta_env();
7674
- import { statSync as statSync4, openSync as openSync2, readSync, closeSync as closeSync2 } from "node:fs";
7675
- function surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml = false) {
7676
- 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.";
7677
7828
  }
7678
7829
  function surgicalHintForConfigDoc(filePath, isConfig, isDoc, isSql, isXml = false) {
7679
- 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.';
7680
7831
  }
7681
7832
  function splitShellSegments(cmd) {
7682
7833
  const segments = [];
@@ -7775,7 +7926,7 @@ var PS_FILE_METHOD_RE = /\[(?:System\.)?IO\.File\]::(?:ReadAllText|ReadAllLines|
7775
7926
  var PS_TEMP_READ_FLOOD_BYTES = 16 * 1024;
7776
7927
  function isLargeFileOnDisk(filePath, floor) {
7777
7928
  try {
7778
- return statSync4(filePath).size >= floor;
7929
+ return statSync6(filePath).size >= floor;
7779
7930
  } catch {
7780
7931
  return false;
7781
7932
  }
@@ -8208,27 +8359,21 @@ function extractLineRangeReadsCompound(cmd) {
8208
8359
  }
8209
8360
  return [...merged.values()];
8210
8361
  }
8211
- function sedRangeHint(filePath, ranges, tool) {
8212
- const lang = detectLanguage(filePath);
8213
- const rangeReads = ranges.map(([s, e]) => '`token-goat read "' + filePath + "@" + s + "-" + e + '"`');
8214
- const allReads = rangeReads.length === 2 ? rangeReads.join(" and ") : rangeReads.length >= 3 ? rangeReads.slice(0, -1).join(", ") + ", and " + rangeReads[rangeReads.length - 1] : rangeReads[0];
8215
- const prefix = "`" + (tool === "awk" ? "awk" : "sed -n") + "` line-range reads bypass read hooks. ";
8216
- if (lang === "markdown") {
8217
- 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.";
8218
- }
8219
- if (lang === "toml" || lang === "json" || lang === "yaml" || lang === "ini") {
8220
- 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.";
8221
- }
8222
- if (languageHasFlag(lang, "symbolBearing")) {
8223
- 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.";
8224
- }
8225
- 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);
8364
+ }
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.";
8226
8370
  }
8227
- function leadingLinesHint(lead, hintPath, start, end, flags, preHookCwd) {
8371
+ function leadingLinesHint(lead, hintPath, start, end, preHookCwd, substitute) {
8228
8372
  const key = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
8229
8373
  const prior = findRangeOverlap(getFileLineRanges(key), start, end);
8230
8374
  if (prior !== null) return sedOverlapHint(hintPath, prior, start, end);
8231
- 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);
8232
8377
  }
8233
8378
  function findRangeOverlap(prior, start, end) {
8234
8379
  let best = null;
@@ -8372,7 +8517,7 @@ function extractTasksOutput(cmd) {
8372
8517
  return null;
8373
8518
  }
8374
8519
  function extractToolResultsFile(cmd) {
8375
- const toolResultsRe = /[/\\]tool-results[/\\]([a-z0-9-]+)\.txt$/i;
8520
+ const toolResultsRe = new RegExp("[/\\\\]tool-results[/\\\\](" + TOOL_RESULTS_ID_CHARS + ")\\.txt$", "i");
8376
8521
  const catM = /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
8377
8522
  if (catM) {
8378
8523
  const fp = catM[1] ?? catM[2] ?? catM[3];
@@ -8726,7 +8871,43 @@ function extractTerminalXmlParsing(cmd) {
8726
8871
  return null;
8727
8872
  }
8728
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
+
8729
8908
  // src/hooks_bash_commands.ts
8909
+ init_define_import_meta_env();
8910
+ import { statSync as statSync8 } from "node:fs";
8730
8911
  var CD_PREFIX_RE = /^(?:cd\s+(?:"[^"]*"|'[^']*'|\S+)[ \t]*(?:&&|;|\r?\n)\s*)+/;
8731
8912
  function stripCdPrefix(cmd) {
8732
8913
  const stripped = cmd.replace(CD_PREFIX_RE, "");
@@ -8942,7 +9123,7 @@ function workingTreeStatusPaths(gitDir) {
8942
9123
  function enqueueRewrittenPath(absPath) {
8943
9124
  if (/(?:^|[/\\])(?:NUL|nul)$/.test(absPath) || absPath.replace(/\\/g, "/").endsWith("/dev/null")) return;
8944
9125
  try {
8945
- if (!statSync5(absPath).isFile()) return;
9126
+ if (!statSync8(absPath).isFile()) return;
8946
9127
  } catch {
8947
9128
  return;
8948
9129
  }
@@ -9007,6 +9188,15 @@ function pipelineShapeFilter(cmd, cwd) {
9007
9188
  const detected = detectFromCommand(stripOutputPipeline(cmd), cwd ?? void 0);
9008
9189
  return detected === null ? null : detected.filter;
9009
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
+ }
9010
9200
  function pureFileReadPath(cmd) {
9011
9201
  const single = extractCatFile(cmd)?.filePath ?? extractHeadFile(cmd)?.filePath ?? extractTailFile(cmd)?.filePath ?? extractLineRangeRead(cmd)?.filePath;
9012
9202
  if (single !== void 0) return single;
@@ -9211,6 +9401,7 @@ function detectUnbalancedShellSyntax(cmd) {
9211
9401
  function maybeCompressRewrite(event, rawCmd, cmd) {
9212
9402
  if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
9213
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;
9214
9405
  let cfg;
9215
9406
  try {
9216
9407
  cfg = loadConfig().bash_compress;
@@ -9290,7 +9481,7 @@ function foldShellReadStructure(cmd, filePath, output, fileKey, cwd) {
9290
9481
  const originalBytes = Buffer.byteLength(output, "utf-8");
9291
9482
  let onDisk;
9292
9483
  try {
9293
- onDisk = statSync6(fileKey).size;
9484
+ onDisk = statSync9(fileKey).size;
9294
9485
  } catch {
9295
9486
  return null;
9296
9487
  }
@@ -9398,6 +9589,7 @@ async function maybeElideServedGenericOutput(cmd, output, exitCode, cwd, cacheMi
9398
9589
  async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes, isUnwrapped = false) {
9399
9590
  if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
9400
9591
  if (!isUnwrapped && isCompressibleSingleCommand(cmd)) return null;
9592
+ if (isFullRecallCommand(cmd)) return null;
9401
9593
  if (exitCode !== null && exitCode !== 0) return null;
9402
9594
  let cfg;
9403
9595
  try {
@@ -9416,7 +9608,7 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
9416
9608
  });
9417
9609
  const minNet = resolveMinNetSavingsBytes();
9418
9610
  if (!compressed.worthApplying(minNet)) return null;
9419
- const id = await commandHash(cmd, cwd);
9611
+ const id = bashOutputIdSync(cmd, output, cwd);
9420
9612
  const { body: untrusted, notices } = splitOwnTrailingNotices(compressed.text);
9421
9613
  const marker = compressed.withMarker(minNet).slice(compressed.text.length);
9422
9614
  const body = fenceUntrusted(untrusted, UNTRUSTED_TOOL_TAG) + notices + marker + "\n[token-goat] full output: bash-output " + id + " --full";
@@ -9495,6 +9687,15 @@ function preBashHandlerInner(event) {
9495
9687
  const cdStripped = cmd !== rawCmd;
9496
9688
  const preHookCwd = getCwd(event) ?? null;
9497
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
+ };
9498
9699
  const cfg = loadConfig();
9499
9700
  if (cfg.hints.warn_unbalanced_shell_quoting) {
9500
9701
  const quoteError = detectUnbalancedShellSyntax(cmd);
@@ -9555,17 +9756,19 @@ function preBashHandlerInner(event) {
9555
9756
  const { filePath, toolOrScript } = terminalXml;
9556
9757
  recordStat("session_hint", 0, 0);
9557
9758
  const target = filePath ? displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath) : "<file>";
9558
- return contextOutput(
9759
+ return pathHint(
9760
+ filePath ? [target] : [],
9559
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}).`
9560
9762
  );
9561
9763
  }
9562
9764
  const singleLineRangeRead = extractLineRangeRead(cmd);
9563
9765
  const sedReads = singleLineRangeRead !== null ? [singleLineRangeRead] : extractLineRangeReadsCompound(cmd);
9564
9766
  if (sedReads !== null) {
9565
- recordStat("session_hint", 0, 0);
9566
9767
  const hints = [];
9768
+ const hintPaths = [];
9567
9769
  for (const { filePath, ranges, tool } of sedReads) {
9568
9770
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9771
+ hintPaths.push(hintPath);
9569
9772
  const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
9570
9773
  const overlapHints = [];
9571
9774
  const freshRanges = [];
@@ -9579,23 +9782,30 @@ function preBashHandlerInner(event) {
9579
9782
  }
9580
9783
  }
9581
9784
  hints.push(...overlapHints);
9582
- 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
+ }
9583
9789
  }
9584
- return contextOutput(hints.join(" "));
9790
+ if (hints.length === 0) return declineUnpriced(hintPaths);
9791
+ recordStat("session_hint", 0, 0);
9792
+ return pathHint(hintPaths, hints.join(" "));
9585
9793
  }
9586
9794
  const gcTailResult = extractGetContentTail(cmd);
9587
9795
  if (gcTailResult !== null) {
9588
9796
  const { filePath, isDoc, isConfig, isSql, isXml } = gcTailResult;
9589
9797
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9590
9798
  recordStat("session_hint", 0, 0);
9591
- 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));
9592
9800
  }
9593
9801
  const gcSelectResult = extractGetContentSelectFirst(cmd);
9594
9802
  if (gcSelectResult !== null) {
9595
- const { filePath, isDoc, isConfig, isSql, isXml, n } = gcSelectResult;
9803
+ const { filePath, n } = gcSelectResult;
9596
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);
9597
9807
  recordStat("session_hint", 0, 0);
9598
- return contextOutput(leadingLinesHint("`Select-Object -First` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql, isXml }, preHookCwd));
9808
+ return pathHint(hintPath, gcSelectHint);
9599
9809
  }
9600
9810
  const catJsonPipe = extractCatJsonPipe(cmd);
9601
9811
  if (catJsonPipe !== null) {
@@ -9603,7 +9813,8 @@ function preBashHandlerInner(event) {
9603
9813
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9604
9814
  recordStat("session_hint", 0, 0);
9605
9815
  const label = isDirectJq ? "`jq`" : "`cat | jq`";
9606
- return contextOutput(
9816
+ return pathHint(
9817
+ hintPath,
9607
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.'
9608
9819
  );
9609
9820
  }
@@ -9612,7 +9823,8 @@ function preBashHandlerInner(event) {
9612
9823
  recordStat("session_hint", 0, 0);
9613
9824
  if (psJsonPipe.filePath) {
9614
9825
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, psJsonPipe.filePath, hintCwd) : psJsonPipe.filePath);
9615
- return contextOutput(
9826
+ return pathHint(
9827
+ hintPath,
9616
9828
  'PowerShell `ConvertFrom-Json` pipeline detected. Use `token-goat json-query "' + hintPath + '" "<selector>"` to extract fields directly without shell conversion scripts.'
9617
9829
  );
9618
9830
  }
@@ -9626,12 +9838,13 @@ function preBashHandlerInner(event) {
9626
9838
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9627
9839
  recordStat("session_hint", 0, 0);
9628
9840
  if (isSql) {
9629
- return contextOutput(
9841
+ return pathHint(
9842
+ hintPath,
9630
9843
  "`" + cmd0 + '` loads the entire file into context. Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.'
9631
9844
  );
9632
9845
  }
9633
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9634
- 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);
9635
9848
  }
9636
9849
  const catMulti = extractCatFilesMulti(cmd);
9637
9850
  if (catMulti !== null) {
@@ -9643,7 +9856,7 @@ function preBashHandlerInner(event) {
9643
9856
  return " " + hintPath + " -> `" + how + "`";
9644
9857
  });
9645
9858
  const msg = "`" + cmd0 + "` on multiple files loads them all into context. Read each surgically instead:\n" + perPath.join("\n");
9646
- return cdStripped ? contextOutput(msg) : denyOutput(msg);
9859
+ return cdStripped ? pathHint(catMulti.map((m) => displaySafePath(resolveCdHintPath(rawCmd, m.filePath, hintCwd))), msg) : denyOutput(msg);
9647
9860
  }
9648
9861
  const psGetContentResult = extractPowerShellWrappedGetContent(cmd, event);
9649
9862
  if (psGetContentResult !== null) {
@@ -9652,10 +9865,10 @@ function preBashHandlerInner(event) {
9652
9865
  recordStat("session_hint", 0, 0);
9653
9866
  const lead = "`Get-Content` via a `powershell -Command` wrapper bypasses read hooks and loads the entire file into context. ";
9654
9867
  if (isSql) {
9655
- 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.');
9656
9869
  }
9657
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9658
- 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);
9659
9872
  }
9660
9873
  const wslCatResult = extractWslCatFile(cmd);
9661
9874
  if (wslCatResult !== null) {
@@ -9663,12 +9876,13 @@ function preBashHandlerInner(event) {
9663
9876
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9664
9877
  recordStat("session_hint", 0, 0);
9665
9878
  if (isSql) {
9666
- return contextOutput(
9879
+ return pathHint(
9880
+ hintPath,
9667
9881
  '`cat` loads the entire file into context. Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.'
9668
9882
  );
9669
9883
  }
9670
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9671
- 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);
9672
9886
  }
9673
9887
  const pyRead = extractPythonFileRead(cmd);
9674
9888
  if (pyRead !== null) {
@@ -9678,33 +9892,37 @@ function preBashHandlerInner(event) {
9678
9892
  if (isOutputFile) {
9679
9893
  if (taskOutputIsJsonlTranscript(filePath)) {
9680
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.';
9681
- return cdStripped ? contextOutput(tHint) : denyOutput(tHint);
9895
+ return cdStripped ? pathHint(hintPath, tHint) : denyOutput(tHint);
9682
9896
  }
9683
- return contextOutput(
9897
+ return pathHint(
9898
+ hintPath,
9684
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.'
9685
9900
  );
9686
9901
  }
9687
9902
  if (isSql) {
9688
- return contextOutput(
9903
+ return pathHint(
9904
+ hintPath,
9689
9905
  'Python `open()` file reads bypass read hooks. Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.'
9690
9906
  );
9691
9907
  }
9692
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9693
- 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);
9694
9910
  }
9695
9911
  const tailResult = extractTailFile(cmd);
9696
9912
  if (tailResult !== null) {
9697
9913
  const { filePath, isDoc, isConfig, isSql, isXml } = tailResult;
9698
9914
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9699
9915
  recordStat("session_hint", 0, 0);
9700
- 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));
9701
9917
  }
9702
9918
  const headResult = extractHeadFile(cmd);
9703
9919
  if (headResult !== null) {
9704
- const { filePath, isDoc, isConfig, isSql, isXml, n } = headResult;
9920
+ const { filePath, n } = headResult;
9705
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);
9706
9924
  recordStat("session_hint", 0, 0);
9707
- return contextOutput(leadingLinesHint("`head` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql, isXml }, preHookCwd));
9925
+ return pathHint(hintPath, headHint);
9708
9926
  }
9709
9927
  const nodeRead = extractNodeFileRead(cmd);
9710
9928
  if (nodeRead !== null) {
@@ -9713,11 +9931,11 @@ function preBashHandlerInner(event) {
9713
9931
  const lead = "Node.js `fs.readFileSync()` bypasses read hooks. ";
9714
9932
  if (isSql) {
9715
9933
  recordStat("session_hint", 0, 0);
9716
- 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.');
9717
9935
  }
9718
- 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));
9719
9937
  recordStat("session_hint", 0, 0);
9720
- return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9938
+ return cdStripped ? pathHint(hintPath, lead + hint) : denyOutput(lead + hint);
9721
9939
  }
9722
9940
  const psMethodRead = extractPowerShellFileMethodRead(cmd, event);
9723
9941
  if (psMethodRead !== null) {
@@ -9726,11 +9944,15 @@ function preBashHandlerInner(event) {
9726
9944
  const lead = "PowerShell `[IO.File]::ReadAllText()` bypasses read hooks. ";
9727
9945
  if (isSql) {
9728
9946
  recordStat("session_hint", 0, 0);
9729
- 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.');
9730
9948
  }
9731
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9949
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml, runnableTargetFor(hintPath, hintCwd, event));
9732
9950
  recordStat("session_hint", 0, 0);
9733
- 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 } };
9734
9956
  }
9735
9957
  if (extractGrepPipeChain(cmd)) {
9736
9958
  recordStat("session_hint", 0, 0);
@@ -9743,7 +9965,8 @@ function preBashHandlerInner(event) {
9743
9965
  const { filePath } = mdHeadingGrep;
9744
9966
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9745
9967
  recordStat("session_hint", 0, 0);
9746
- return contextOutput(
9968
+ return pathHint(
9969
+ hintPath,
9747
9970
  'Use `token-goat outline "' + hintPath + '"` to get all headings with line ranges \u2014 then `token-goat section "' + hintPath + '::Heading"` to read one section.'
9748
9971
  );
9749
9972
  }
@@ -9752,7 +9975,8 @@ function preBashHandlerInner(event) {
9752
9975
  const { identifier } = rgSymbol;
9753
9976
  recordStat("session_hint", 0, 0);
9754
9977
  return contextOutput(
9755
- "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]
9756
9980
  );
9757
9981
  }
9758
9982
  const rgStructural = extractRgStructuralSearch(cmd);
@@ -9760,7 +9984,8 @@ function preBashHandlerInner(event) {
9760
9984
  const { filePath } = rgStructural;
9761
9985
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9762
9986
  recordStat("session_hint", 0, 0);
9763
- return contextOutput(
9987
+ return pathHint(
9988
+ hintPath,
9764
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.'
9765
9990
  );
9766
9991
  }
@@ -9792,7 +10017,7 @@ function preBashHandlerInner(event) {
9792
10017
  const prevResolvedPath = prevPath !== null && commandPathIsTouchable(prevPath, event) ? resolveIndexPath(prevPath, preHookCwd ?? process.cwd()) : null;
9793
10018
  if (prevPath !== null && prevResolvedPath !== null && !existsSync(prevResolvedPath)) {
9794
10019
  clearCurlDownload(curlDl.url);
9795
- } 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) {
9796
10021
  recordStat("session_hint", 0, 0);
9797
10022
  return denyOutput(
9798
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.'
@@ -9854,7 +10079,7 @@ function preBashHandlerInner(event) {
9854
10079
  }
9855
10080
  if (notes.length > 0) {
9856
10081
  recordStat("session_hint", 0, 0);
9857
- return contextOutput(notes.join(" "));
10082
+ return pathHint(tgRead.filePath !== null ? [tgRead.filePath] : [], notes.join(" "));
9858
10083
  }
9859
10084
  return passOutput();
9860
10085
  }
@@ -9886,7 +10111,28 @@ function preBashHandler(event) {
9886
10111
  return applyHintTracking(event, preBashHandlerInner(event), classifyBashHint);
9887
10112
  }
9888
10113
  registerHook("pre_tool_use", preBashHandler, { toolName: "Bash" });
9889
- 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
+ }
9890
10136
  return extractToolResponseField(raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
9891
10137
  }
9892
10138
  function extractExitCode(raw) {
@@ -10014,7 +10260,7 @@ async function postBashHandler(event) {
10014
10260
  const isUnwrapped = unwrapCompressCommand(rawCmdRaw) === null;
10015
10261
  const rawCmd = unwrapCompressCommand(rawCmdRaw) ?? rawCmdRaw;
10016
10262
  const cmd = stripCdPrefix(rawCmd);
10017
- const output = extractBashOutput(event.raw);
10263
+ const output = extractBashOutput(event);
10018
10264
  const exitCode = extractExitCode(event.raw);
10019
10265
  const cwd = getCwd(event) ?? null;
10020
10266
  const cacheMinBytes = loadConfig().bash_compress.cache_min_bytes;
@@ -10201,9 +10447,11 @@ function postBashOutputHandler(event) {
10201
10447
  const delta = output.slice(prior.output.length);
10202
10448
  storePollSnapshot(event.sessionId, bashId, output);
10203
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;
10204
10452
  const deltaNotice = `[token-goat: bash_id ${bashId} delta since last poll]
10205
10453
  `;
10206
- const fenced = fenceUntrusted(delta, UNTRUSTED_TOOL_TAG);
10454
+ const fenced = fenceUntrusted(deltaBody, UNTRUSTED_TOOL_TAG);
10207
10455
  const body = `${deltaNotice}${fenced}`;
10208
10456
  if (!isRewriteWorthwhile({
10209
10457
  originalBytes: Buffer.byteLength(output, "utf-8"),
@@ -10325,8 +10573,8 @@ registerHook("post_tool_use", postTaskOutputHandler, { toolName: "TaskOutput" })
10325
10573
 
10326
10574
  // src/hooks_tool_failure.ts
10327
10575
  init_define_import_meta_env();
10328
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync as statSync7, writeFileSync as writeFileSync2 } from "node:fs";
10329
- import { dirname as dirname2, relative } from "node:path";
10576
+ import { existsSync as existsSync2, readFileSync as readFileSync5, statSync as statSync10, writeFileSync as writeFileSync2 } from "node:fs";
10577
+ import { dirname as dirname2, relative as relative2 } from "node:path";
10330
10578
  var FAILURE_SUFFIX = ".tool-failures.json";
10331
10579
  var MAX_TRACKED_FAILURES = 64;
10332
10580
  var SIGNATURE_ERROR_CHARS = 200;
@@ -10353,7 +10601,7 @@ function ledgerPath(sessionId) {
10353
10601
  }
10354
10602
  function readLedger(target) {
10355
10603
  try {
10356
- const parsed = JSON.parse(readFileSync3(target, "utf8"));
10604
+ const parsed = JSON.parse(readFileSync5(target, "utf8"));
10357
10605
  if (parsed !== null && typeof parsed === "object") {
10358
10606
  const seen = parsed.seen;
10359
10607
  const order = parsed.order;
@@ -10394,10 +10642,10 @@ function diagnoseEditFailure(event, errorText) {
10394
10642
  const absPath = normalizePath(filePath);
10395
10643
  if (!existsSync2(absPath)) return null;
10396
10644
  try {
10397
- const stat = statSync7(absPath);
10645
+ const stat = statSync10(absPath);
10398
10646
  if (stat.size > MAX_EDIT_DIAGNOSE_BYTES) return null;
10399
- const fileContent = readFileSync3(absPath, "utf8");
10400
- const relDisplay = displaySafeText(relative(process.cwd(), absPath).replace(/\\/g, "/") || absPath);
10647
+ const fileContent = readFileSync5(absPath, "utf8");
10648
+ const relDisplay = displaySafeText(relative2(process.cwd(), absPath).replace(/\\/g, "/") || absPath);
10401
10649
  if (isMultiple) {
10402
10650
  const matchLines = [];
10403
10651
  let pos = 0;
@@ -11044,8 +11292,8 @@ The full payload was cached to prevent harness context spill. Slicing commands:
11044
11292
  }
11045
11293
  return passOrFence();
11046
11294
  }
11047
- registerHook("pre_tool_use", preMcpHandler, { toolPattern: "^mcp__" });
11048
- 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 });
11049
11297
 
11050
11298
  // src/hooks_websearch.ts
11051
11299
  init_define_import_meta_env();
@@ -11246,8 +11494,7 @@ registerHook("post_tool_use", postBrowserImageHandler, { toolPattern: "^mcp__" }
11246
11494
  init_define_import_meta_env();
11247
11495
  import { createHash as createHash2 } from "node:crypto";
11248
11496
  import * as fs2 from "node:fs";
11249
- import * as os2 from "node:os";
11250
- import * as path3 from "node:path";
11497
+ import * as path5 from "node:path";
11251
11498
  var BRIEFING_TARGET_TOKENS = 550;
11252
11499
  function buildSubagentBriefing() {
11253
11500
  try {
@@ -11472,8 +11719,8 @@ function parseAgentDefinition(text, fallbackName) {
11472
11719
  return { name: name2, restricted: false };
11473
11720
  }
11474
11721
  function findRestrictedAgentNames(roots) {
11475
- const projectAgentsRoot = path3.join(process.cwd(), ".claude", "agents");
11476
- const scanRoots = roots ?? [path3.join(os2.homedir(), ".claude", "agents"), projectAgentsRoot];
11722
+ const projectAgentsRoot = path5.join(process.cwd(), ".claude", "agents");
11723
+ const scanRoots = roots ?? [path5.join(claudeConfigDir(), "agents"), projectAgentsRoot];
11477
11724
  const names = /* @__PURE__ */ new Set();
11478
11725
  const visited = /* @__PURE__ */ new Set();
11479
11726
  let filesSeen = 0;
@@ -11497,7 +11744,7 @@ function findRestrictedAgentNames(roots) {
11497
11744
  }
11498
11745
  for (const entry of entries) {
11499
11746
  if (filesSeen >= ROSTER_WALK_MAX_FILES) return;
11500
- const full = path3.join(dir, entry.name);
11747
+ const full = path5.join(dir, entry.name);
11501
11748
  let stat;
11502
11749
  try {
11503
11750
  stat = fs2.statSync(full);
@@ -11516,12 +11763,12 @@ function findRestrictedAgentNames(roots) {
11516
11763
  } catch {
11517
11764
  continue;
11518
11765
  }
11519
- const parsed = parseAgentDefinition(text, path3.basename(entry.name, path3.extname(entry.name)));
11766
+ const parsed = parseAgentDefinition(text, path5.basename(entry.name, path5.extname(entry.name)));
11520
11767
  if (parsed !== null && parsed.restricted) names.add(parsed.name);
11521
11768
  }
11522
11769
  };
11523
11770
  for (const root of scanRoots) {
11524
- const isProjectRoot = roots === void 0 && path3.resolve(root) === path3.resolve(projectAgentsRoot);
11771
+ const isProjectRoot = roots === void 0 && path5.resolve(root) === path5.resolve(projectAgentsRoot);
11525
11772
  if (!isProjectRoot) {
11526
11773
  walk(root, 0, null);
11527
11774
  continue;
@@ -11651,11 +11898,11 @@ function safeSuggestions(output) {
11651
11898
  if (output.hookType === "context") return { hookType: "context", context: stripUnsafeSuggestions(output.context) };
11652
11899
  return output;
11653
11900
  }
11654
- async function relayInProcess(eventName, rawPayload) {
11901
+ async function relayInProcess(eventName, rawPayload, harnessWaitMs) {
11902
+ if (!isHookEventName(eventName)) {
11903
+ return "{}";
11904
+ }
11655
11905
  try {
11656
- if (!isHookEventName(eventName)) {
11657
- return "{}";
11658
- }
11659
11906
  const harness = detectHarness();
11660
11907
  const payload = eventName === "pre_tool_use" || eventName === "post_tool_use" ? normalizePayload(rawPayload, harnessForNormalization()) : rawPayload;
11661
11908
  const event = buildEvent(eventName, payload);
@@ -11686,6 +11933,8 @@ async function relayInProcess(eventName, rawPayload) {
11686
11933
  return serializeOutput(output, event.eventName, harness, event);
11687
11934
  } catch {
11688
11935
  return "{}";
11936
+ } finally {
11937
+ recordStat(`hook:${eventName}`, 0, 0, void 0, void 0, void 0, harnessWaitMs ?? performance.now());
11689
11938
  }
11690
11939
  }
11691
11940
  async function relay(eventName) {