token-goat 2.9.5 → 2.9.8

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.
@@ -18,12 +18,12 @@ import {
18
18
  storeWebOutput,
19
19
  summarizeResidentContext,
20
20
  taskListPruneHint
21
- } from "./token-goat-chunk-JO5JX72D.mjs";
21
+ } from "./token-goat-chunk-VEEHPNBV.mjs";
22
22
  import {
23
23
  BASH_OUTPUT_SUBDIR,
24
+ GENERIC_SERVED_OUTPUT_KEY,
24
25
  OUTLINE_MAX_REPLACEMENT_RATIO,
25
26
  OUTLINE_MIN_HEADINGS,
26
- UNTRUSTED_TOOL_TAG,
27
27
  WEB_FETCH_KEY_SEP,
28
28
  appendDirtyPath,
29
29
  applyHintTracking,
@@ -42,7 +42,6 @@ import {
42
42
  extractCompactFromMarker,
43
43
  extractMarkdownHeadings,
44
44
  fenceUntrusted,
45
- fenceUntrustedFileContent,
46
45
  fenceWithMatches,
47
46
  foldDelivery,
48
47
  foldDetail,
@@ -126,18 +125,19 @@ import {
126
125
  wasCliReadThisSession,
127
126
  wasFileReadThisSession,
128
127
  wasHintShown
129
- } from "./token-goat-chunk-U4FTM2SB.mjs";
128
+ } from "./token-goat-chunk-TXZY7C24.mjs";
130
129
  import {
131
130
  bashOutputCapBytes,
132
131
  canRunWrappedShell,
133
132
  deliveredOutputBytes
134
- } from "./token-goat-chunk-HTJP6FHK.mjs";
133
+ } from "./token-goat-chunk-REHUP2OE.mjs";
135
134
  import {
136
135
  BODY_FIRST_TOOL_RESPONSE_KEYS,
137
136
  ENV_KEYS,
138
137
  IDENTICAL_READ_MIN_BODY_BYTES,
139
138
  OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
140
139
  PER_FILE_COUNTERFACTUAL_CEILING,
140
+ UNTRUSTED_TOOL_TAG,
141
141
  VERSION,
142
142
  compressOutput,
143
143
  containsLineRun,
@@ -149,6 +149,7 @@ import {
149
149
  detectHarness,
150
150
  detectLanguage,
151
151
  displaySafePath,
152
+ displaySafeText,
152
153
  emitRewrite,
153
154
  emitRewriteIfChanged,
154
155
  envBool,
@@ -156,6 +157,7 @@ import {
156
157
  extractErrorMessage,
157
158
  extractToolResponseField,
158
159
  extractToolResultText,
160
+ fenceUntrustedFileContent,
159
161
  filterByName,
160
162
  findProject,
161
163
  foldPath,
@@ -167,11 +169,13 @@ import {
167
169
  getToolName,
168
170
  globalDbPath,
169
171
  hasBareBackgroundOrNewline,
172
+ hasUnquotedOperator,
170
173
  isMcpErrorResponse,
171
174
  isRewriteWorthwhile,
172
175
  isUnderSystemTemp,
173
176
  loadConfig,
174
177
  makeDedupHintHandlers,
178
+ neutralizeSpokenMarkers,
175
179
  normalizePath,
176
180
  passOutput,
177
181
  recordStat,
@@ -190,7 +194,7 @@ import {
190
194
  stripAnsiEscapes,
191
195
  toDisplayPath,
192
196
  toKB
193
- } from "./token-goat-chunk-ZOKNDG6V.mjs";
197
+ } from "./token-goat-chunk-JZYJH76S.mjs";
194
198
  import {
195
199
  init_define_import_meta_env
196
200
  } from "./token-goat-chunk-A37V4PBF.mjs";
@@ -351,17 +355,66 @@ function foldGrepContentHandler(event) {
351
355
  return passOutput();
352
356
  }
353
357
  }
358
+ var DOC_EXT_RE = /\.(?:md|mdx|rst|txt)$/i;
359
+ var SOURCE_EXT_RE = /\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|css|scss|sass|less)$/i;
360
+ var STRUCTURAL_DOC_PATTERN_RE = /^(?:\^)?#+\s*/;
361
+ var STRUCTURAL_SOURCE_PATTERN_RE = /^(?:\^|\s)*(?:def|class|function|async\s+def|async\s+function|export\s+(?:default\s+)?(?:class|function|interface|type|const|enum)|func|fn|struct|interface|impl|type)\b/i;
362
+ function extractGrepStructuralSearch(toolInput) {
363
+ const pattern = typeof toolInput["pattern"] === "string" ? toolInput["pattern"].trim() : "";
364
+ if (!pattern) return null;
365
+ let rawPath = null;
366
+ if (typeof toolInput["path"] === "string" && toolInput["path"].trim() !== "") {
367
+ rawPath = toolInput["path"].trim();
368
+ } else if (typeof toolInput["paths"] === "string" && toolInput["paths"].trim() !== "") {
369
+ rawPath = toolInput["paths"].trim();
370
+ } else if (Array.isArray(toolInput["paths"]) && toolInput["paths"].length === 1 && typeof toolInput["paths"][0] === "string") {
371
+ rawPath = toolInput["paths"][0].trim();
372
+ }
373
+ if (!rawPath) return null;
374
+ if (/[*?[{]/.test(rawPath)) return null;
375
+ if (DOC_EXT_RE.test(rawPath) && STRUCTURAL_DOC_PATTERN_RE.test(pattern)) {
376
+ return { filePath: rawPath, isDoc: true, isSource: false };
377
+ }
378
+ if (SOURCE_EXT_RE.test(rawPath) && STRUCTURAL_SOURCE_PATTERN_RE.test(pattern)) {
379
+ return { filePath: rawPath, isDoc: false, isSource: true };
380
+ }
381
+ return null;
382
+ }
383
+ function preGrepHandler(event) {
384
+ try {
385
+ if (getToolName(event) !== "Grep") return passOutput();
386
+ const toolInput = getToolInput(event);
387
+ const structSearch = extractGrepStructuralSearch(toolInput);
388
+ if (structSearch !== null) {
389
+ recordStat("session_hint", 0, 0);
390
+ const { isDoc } = structSearch;
391
+ const filePath = displaySafePath(structSearch.filePath);
392
+ const hint = isDoc ? 'Scanning a document for headings loads large match output. Use `token-goat section "' + filePath + '::SectionHeading"` to read one section or `token-goat outline "' + filePath + '"` to see the document outline.' : 'Scanning a source file for symbols loads large match output. Use `token-goat skeleton "' + filePath + '"` to see the file structure or `token-goat read "' + filePath + '::SymbolName"` to inspect a specific symbol.';
393
+ return contextOutput(hint);
394
+ }
395
+ return preGrepDedupHandler(event);
396
+ } catch {
397
+ return passOutput();
398
+ }
399
+ }
354
400
  function postGrepHandler(event) {
355
401
  const dedupResult = dedupPostHandler(event);
356
402
  const foldResult = foldGrepContentHandler(event);
357
403
  if (foldResult.hookType === "rewriteOutput") return foldResult;
358
404
  return dedupResult;
359
405
  }
360
- registerHook("pre_tool_use", preGrepDedupHandler, { toolName: "Grep" });
406
+ registerHook("pre_tool_use", preGrepHandler, { toolName: "Grep" });
361
407
  registerHook("post_tool_use", postGrepHandler, { toolName: "Grep" });
362
408
 
363
409
  // src/hooks_glob.ts
364
410
  init_define_import_meta_env();
411
+ function isBroadCatchAllGlob(pattern, pathArg) {
412
+ const p = pattern.trim();
413
+ const isBroad = p === "*" || p === "**/*" || p === "**" || p === "*.*" || p === "**/*.*" || p === "**/*.**";
414
+ if (!isBroad) return false;
415
+ const target = (pathArg ?? "").trim().replace(/[\\/]+$/, "");
416
+ return target === "" || target === "." || target === "./" || target === ".\\" || !target.includes("/") && !target.includes("\\");
417
+ }
365
418
  function globSignature(toolInput) {
366
419
  const pattern = toolInput["pattern"];
367
420
  if (typeof pattern !== "string" || pattern === "") return null;
@@ -376,7 +429,24 @@ var { post: postGlobHandler, pre: preGlobDedupHandler } = makeDedupHintHandlers(
376
429
  minMatchesConfigKey: "glob_dedup_min_matches",
377
430
  statName: "glob_dedup_hint"
378
431
  });
379
- registerHook("pre_tool_use", preGlobDedupHandler, { toolName: "Glob" });
432
+ function preGlobHandler(event) {
433
+ try {
434
+ if (getToolName(event) !== "Glob") return passOutput();
435
+ const toolInput = getToolInput(event);
436
+ const pattern = typeof toolInput["pattern"] === "string" ? toolInput["pattern"] : "";
437
+ const pathArg = typeof toolInput["path"] === "string" ? toolInput["path"] : typeof toolInput["paths"] === "string" ? toolInput["paths"] : Array.isArray(toolInput["paths"]) && toolInput["paths"].length === 1 && typeof toolInput["paths"][0] === "string" ? toolInput["paths"][0] : void 0;
438
+ if (pattern && isBroadCatchAllGlob(pattern, pathArg)) {
439
+ recordStat("session_hint", 0, 0);
440
+ return contextOutput(
441
+ 'Broad recursive glob pattern "' + displaySafeText(pattern) + '" traverses entire directory trees and may dump thousands of paths into context. Use `token-goat map --compact` to inspect project directory structure efficiently, or narrow the glob path.'
442
+ );
443
+ }
444
+ return preGlobDedupHandler(event);
445
+ } catch {
446
+ return passOutput();
447
+ }
448
+ }
449
+ registerHook("pre_tool_use", preGlobHandler, { toolName: "Glob" });
380
450
  registerHook("post_tool_use", postGlobHandler, { toolName: "Glob" });
381
451
 
382
452
  // src/hooks_edit.ts
@@ -515,10 +585,10 @@ function renderReadRow(entry) {
515
585
  const kb = Math.max(1, toKB(entry.sizeBytes));
516
586
  const plural = entry.readCount === 1 ? "read" : "reads";
517
587
  const edited = entry.wasEdited ? ", edited" : "";
518
- return `- ${entry.path} (${kb}kb, ${entry.readCount} ${plural}${edited})`;
588
+ return `- ${displaySafePath(entry.path)} (${kb}kb, ${entry.readCount} ${plural}${edited})`;
519
589
  }
520
590
  function renderSymbolReadRow(entry) {
521
- return `- ${entry.path} (symbols: ${(entry.symbols_read ?? []).join(", ")})`;
591
+ return `- ${displaySafePath(entry.path)} (symbols: ${(entry.symbols_read ?? []).map(displaySafeText).join(", ")})`;
522
592
  }
523
593
  function mergeManifestFiles(parent, siblingFiles) {
524
594
  const byPath = /* @__PURE__ */ new Map();
@@ -557,7 +627,7 @@ function buildManifest(sessionId, cwd) {
557
627
  appendCappedSection(
558
628
  lines,
559
629
  "### Edited files",
560
- editedFiles.map((entry) => `- ${entry.path}`),
630
+ editedFiles.map((entry) => `- ${displaySafePath(entry.path)}`),
561
631
  MAX_ROWS
562
632
  );
563
633
  appendCappedSection(lines, "### Surgically read files (symbol/section reads, never read whole)", symbolOnlyFiles.map(renderSymbolReadRow), MAX_ROWS);
@@ -566,8 +636,8 @@ function buildManifest(sessionId, cwd) {
566
636
  "### Web URLs fetched",
567
637
  webFetches.map(([key, cacheId]) => {
568
638
  const [url = key, prompt = ""] = key.split(WEB_FETCH_KEY_SEP);
569
- const promptSuffix = prompt ? `, prompt: ${JSON.stringify(prompt)}` : "";
570
- return `- ${url} (cacheId: ${cacheId}${promptSuffix})`;
639
+ const promptSuffix = prompt ? `, prompt: ${neutralizeSpokenMarkers(JSON.stringify(prompt))}` : "";
640
+ return `- ${displaySafeText(url)} (cacheId: ${cacheId}${promptSuffix})`;
571
641
  }),
572
642
  MAX_ROWS
573
643
  );
@@ -672,15 +742,20 @@ function buildMemEpochSection() {
672
742
  ];
673
743
  }
674
744
  var MANIFEST_PREAMBLE = "When summarizing this session, keep the file paths and symbol names below exactly as written -- they are the handles the next turn needs to resume work. Do not paraphrase them into prose.";
745
+ var BUDGET_ESCALATION_MARKER = "TG-BUDGET-ESCALATION:";
746
+ function summaryBudgetDirective(budgetChars) {
747
+ if (budgetChars <= 0) return "";
748
+ return ` Aim for at most ${budgetChars} characters. Prefer a shorter summary that keeps every path, symbol, command and decision over a longer one that reproduces tool output verbatim: cite the recall id (\`token-goat bash-output <id> --full\`) rather than quoting the output again. If this session genuinely cannot be summarized within that target without losing state the next turn needs, exceed it and open the summary with a single line reading \`${BUDGET_ESCALATION_MARKER} <one-sentence reason>\`.`;
749
+ }
675
750
  function preCompactHandler(event) {
676
- const out = loadConfig().compact_assist.enabled ? contextOutput(`${MANIFEST_PREAMBLE}
751
+ const out = loadConfig().compact_assist.enabled ? contextOutput(`${MANIFEST_PREAMBLE}${summaryBudgetDirective(loadConfig().compact_assist.summary_budget_chars)}
677
752
 
678
753
  ${buildManifest(event.sessionId, getCwd(event))}`) : passOutput();
679
754
  markCompacted();
680
755
  return out;
681
756
  }
682
757
  registerHook("pre_compact", preCompactHandler);
683
- var MANIFEST_SURVIVAL_SAMPLE = 12;
758
+ var MANIFEST_SURVIVAL_SAMPLE = 64;
684
759
  function manifestPathSample(sessionId) {
685
760
  const ownFiles = [...getSessionFiles().values()];
686
761
  const siblingFiles = sessionId !== void 0 ? listSiblingSessionStates(sessionId).flatMap((s) => s.files) : [];
@@ -700,12 +775,15 @@ function postCompactHandler(event) {
700
775
  const haystack = foldPath(summary);
701
776
  const survived = sample.filter((p) => haystack.includes(foldPath(p))).length;
702
777
  const trigger = typeof event.raw["trigger"] === "string" ? event.raw["trigger"] : "unknown";
778
+ const budget = loadConfig().compact_assist.summary_budget_chars;
779
+ const over = budget > 0 && summary.length > budget ? 1 : 0;
780
+ const escalated = summary.includes(BUDGET_ESCALATION_MARKER) ? 1 : 0;
703
781
  recordStat(
704
782
  "compact_summary",
705
783
  0,
706
784
  0,
707
785
  void 0,
708
- `trigger=${trigger} bytes=${bytes} est_tokens=${savedTokensFromBytes(bytes)} manifest_paths=${survived}/${sample.length}`
786
+ `trigger=${trigger} bytes=${bytes} est_tokens=${savedTokensFromBytes(bytes)} manifest_paths=${survived}/${sample.length} budget=${budget} over=${over} escalated=${escalated}`
709
787
  );
710
788
  return passOutput();
711
789
  }
@@ -720,7 +798,7 @@ init_define_import_meta_env();
720
798
  var TRACKED_SKILL = "token-goat";
721
799
  var MAX_COMMANDS_SHOWN = 8;
722
800
  async function currentCommandNames() {
723
- const { buildProgram } = await import("./token-goat-chunk-2FVSKFZU.mjs");
801
+ const { buildProgram } = await import("./token-goat-chunk-3YANZKER.mjs");
724
802
  return flattenCommandNames(buildCommandManifest(buildProgram()));
725
803
  }
726
804
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -7320,6 +7398,16 @@ function resolveSkillContext(event) {
7320
7398
  }
7321
7399
  return { skillName };
7322
7400
  }
7401
+ function planHeadingTree(body, bodyBytes, skillName) {
7402
+ const headings = extractMarkdownHeadings(body);
7403
+ if (headings.length < OUTLINE_MIN_HEADINGS) return null;
7404
+ const { sectionsList } = formatHeadingTreeParts(headings, skillName);
7405
+ const treeBytes = Buffer.byteLength(sectionsList, "utf-8");
7406
+ if (treeBytes > bodyBytes * OUTLINE_MAX_REPLACEMENT_RATIO) return null;
7407
+ const totalHeadings = extractMarkdownHeadings(body, Number.MAX_SAFE_INTEGER).length;
7408
+ const phrase = totalHeadings > headings.length ? "its heading tree shows " + headings.length + " of " + totalHeadings + " headings below instead of the full body; the remaining sections are reachable only through `token-goat skill-body " + skillName + "`." : "its heading tree (" + headings.length + " headings) is inlined below instead of the full body.";
7409
+ return { sectionsList, treeBytes, phrase };
7410
+ }
7323
7411
  async function preSkillHandler(event) {
7324
7412
  try {
7325
7413
  const ctx = resolveSkillContext(event);
@@ -7333,7 +7421,7 @@ async function preSkillHandler(event) {
7333
7421
  if (await hasSessionOutput(event.sessionId, skillName)) {
7334
7422
  const cachedBytes = await sessionOutputBodyBytes(event.sessionId, skillName);
7335
7423
  const denyCredit = cachedBytes !== null ? Math.min(cachedBytes, PER_FILE_COUNTERFACTUAL_CEILING) : 0;
7336
- recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit));
7424
+ recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit), void 0, "skill-reload-deny");
7337
7425
  return denyOutput(
7338
7426
  "Skill `" + skillName + "` was already loaded this session and is cached. Use `token-goat skill-section " + skillName + " '<heading>'` to recall a section, `token-goat skill-body " + skillName + " --compact` to recall the compact slice, or `token-goat skill-body " + skillName + "` for the full body instead of re-loading it."
7339
7427
  );
@@ -7343,35 +7431,33 @@ async function preSkillHandler(event) {
7343
7431
  try {
7344
7432
  const body = await readFile(sourcePath, "utf-8");
7345
7433
  const bodyBytes = Buffer.byteLength(body, "utf-8");
7346
- const compact = bodyBytes > OVERSIZED_FIRST_LOAD_THRESHOLD_BYTES ? extractCompactFromMarker(body) : null;
7347
- if (compact !== null) {
7348
- const compactBytes = Buffer.byteLength(compact, "utf-8");
7349
- if (compactBytes * 2 <= bodyBytes && compactBytes <= COMPACT_INLINE_MAX_BYTES) {
7350
- const savedBytes = bodyBytes - compactBytes;
7351
- recordStat("skill_compact_inlined", savedBytes, savedTokensFromBytes(savedBytes));
7352
- return denyOutput(
7353
- "Skill `" + skillName + "` is large (" + bodyBytes + " bytes); its compact slice (" + compactBytes + " bytes) is inlined below instead of the full body. For a specific section, run `token-goat skill-section " + skillName + " '<heading>'`, or `token-goat skill-body " + skillName + "` if you need the full body.\n\n" + compact
7354
- );
7355
- }
7356
- recordStat("skill_oversized_first_load");
7357
- return denyOutput(
7358
- "Skill `" + skillName + "` is large (" + bodyBytes + " bytes) and has a compact slice available. Use `token-goat skill-section " + skillName + " '<heading>'` to load a specific section, `token-goat skill-body " + skillName + " --compact` to load the compact slice, or `token-goat skill-body " + skillName + "` for the full body."
7359
- );
7360
- } else if (bodyBytes > OVERSIZED_FIRST_LOAD_THRESHOLD_BYTES) {
7361
- const headings = extractMarkdownHeadings(body);
7362
- if (headings.length >= OUTLINE_MIN_HEADINGS) {
7363
- const { sectionsList } = formatHeadingTreeParts(headings, skillName);
7364
- const treeBytes = Buffer.byteLength(sectionsList, "utf-8");
7365
- if (treeBytes <= bodyBytes * OUTLINE_MAX_REPLACEMENT_RATIO) {
7366
- const savedBytes = bodyBytes - treeBytes;
7367
- recordStat("skill_heading_tree_inlined", savedBytes, savedTokensFromBytes(savedBytes));
7368
- const totalHeadings = extractMarkdownHeadings(body, Number.MAX_SAFE_INTEGER).length;
7369
- const headingCountPhrase = totalHeadings > headings.length ? "its heading tree shows " + headings.length + " of " + totalHeadings + " headings below instead of the full body; the remaining sections are reachable only through `token-goat skill-body " + skillName + "`." : "its heading tree (" + headings.length + " headings) is inlined below instead of the full body.";
7434
+ if (bodyBytes > OVERSIZED_FIRST_LOAD_THRESHOLD_BYTES) {
7435
+ const compact = extractCompactFromMarker(body);
7436
+ if (compact !== null) {
7437
+ const compactBytes = Buffer.byteLength(compact, "utf-8");
7438
+ if (compactBytes * 2 <= bodyBytes && compactBytes <= COMPACT_INLINE_MAX_BYTES) {
7439
+ const savedBytes = bodyBytes - compactBytes;
7440
+ recordStat("skill_compact_inlined", savedBytes, savedTokensFromBytes(savedBytes));
7370
7441
  return denyOutput(
7371
- "Skill `" + skillName + "` is large (" + bodyBytes + " bytes) with no compact slice; " + headingCountPhrase + " Use `token-goat skill-section " + skillName + " '<heading>'` to load a specific section, or `token-goat skill-body " + skillName + "` for the full body.\n\n" + fenceUntrustedFileContent(sectionsList)
7442
+ "Skill `" + skillName + "` is large (" + bodyBytes + " bytes); its compact slice (" + compactBytes + " bytes) is inlined below instead of the full body. For a specific section, run `token-goat skill-section " + skillName + " '<heading>'`, or `token-goat skill-body " + skillName + "` if you need the full body.\n\n" + compact
7372
7443
  );
7373
7444
  }
7374
7445
  }
7446
+ const tree = planHeadingTree(body, bodyBytes, skillName);
7447
+ if (tree !== null) {
7448
+ const replaced = compact !== null ? Buffer.byteLength(compact, "utf-8") : bodyBytes;
7449
+ const savedBytes = Math.max(0, replaced - tree.treeBytes);
7450
+ recordStat("skill_heading_tree_inlined", savedBytes, savedTokensFromBytes(savedBytes), void 0, "skill=" + skillName + " marker=" + (compact !== null ? "unusable" : "none"));
7451
+ return denyOutput(
7452
+ "Skill `" + skillName + "` is large (" + bodyBytes + " bytes)" + (compact !== null ? ", and its compact slice is too large to inline; " : " with no compact slice; ") + tree.phrase + " Use `token-goat skill-section " + skillName + " '<heading>'` to load a specific section" + (compact !== null ? ", `token-goat skill-body " + skillName + " --compact` to load the compact slice" : "") + ", or `token-goat skill-body " + skillName + "` for the full body.\n\n" + fenceUntrustedFileContent(tree.sectionsList)
7453
+ );
7454
+ }
7455
+ if (compact !== null) {
7456
+ recordStat("skill_oversized_first_load");
7457
+ return denyOutput(
7458
+ "Skill `" + skillName + "` is large (" + bodyBytes + " bytes) and has a compact slice available. Use `token-goat skill-section " + skillName + " '<heading>'` to load a specific section, `token-goat skill-body " + skillName + " --compact` to load the compact slice, or `token-goat skill-body " + skillName + "` for the full body."
7459
+ );
7460
+ }
7375
7461
  }
7376
7462
  } catch {
7377
7463
  }
@@ -7742,6 +7828,7 @@ function extractCatFilesMulti(cmd) {
7742
7828
  }
7743
7829
  var POWERSHELL_WRAP_RE = /^(?:powershell|pwsh)(?:\.exe)?(?:\s+-[a-zA-Z]+(?:\s+\S+)?)*\s+(?:-Command|-c|-EncodedCommand)\s+(?:"([^"]*)"|'([^']*)')\s*$/i;
7744
7830
  var PS_GETCONTENT_INNER_RE = /^(?:Get-Content|gc|cat|type)(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+-[a-zA-Z].*)?\s*$/i;
7831
+ var PS_FILE_METHOD_RE = /\[(?:System\.)?IO\.File\]::(?:ReadAllText|ReadAllLines|ReadAllBytes|ReadLines|OpenText)\(\s*['"]([^'"]+)['"]/i;
7745
7832
  var PS_TEMP_READ_FLOOD_BYTES = 16 * 1024;
7746
7833
  function isLargeFileOnDisk(filePath, floor) {
7747
7834
  try {
@@ -7764,6 +7851,24 @@ function extractPowerShellWrappedGetContent(cmd) {
7764
7851
  if (isTempPath(filePath) && !isLargeFileOnDisk(filePath, PS_TEMP_READ_FLOOD_BYTES)) return null;
7765
7852
  return { filePath, ...flags };
7766
7853
  }
7854
+ function extractPowerShellFileMethodRead(cmd) {
7855
+ let inner = cmd.trim();
7856
+ const w = POWERSHELL_WRAP_RE.exec(inner);
7857
+ if (w) {
7858
+ inner = (w[1] ?? w[2] ?? "").trim();
7859
+ }
7860
+ const m = PS_FILE_METHOD_RE.exec(inner);
7861
+ if (!m?.[1]) return null;
7862
+ const filePath = m[1];
7863
+ if (isOrchestratorStateFile(filePath)) return null;
7864
+ if (isTempPath(filePath) && !isLargeFileOnDisk(filePath, PS_TEMP_READ_FLOOD_BYTES)) return null;
7865
+ const flags = classifyFileExtensions(filePath);
7866
+ if (flags === null) {
7867
+ const { isDoc, isConfig, isSql } = classifyDocConfig(filePath);
7868
+ return { filePath, isDoc, isConfig, isSql };
7869
+ }
7870
+ return { filePath, isDoc: flags.isDoc, isConfig: flags.isConfig, isSql: flags.isSql };
7871
+ }
7767
7872
  function extractRgSymbolSearch(cmd) {
7768
7873
  if (!/^(?:rg|grep)\s+/.test(cmd)) return null;
7769
7874
  if (!/-n\b/.test(cmd)) return null;
@@ -7917,57 +8022,75 @@ function pythonOpenPathsAreAllLiteral(text) {
7917
8022
  const calls = pythonOpenCalls(text);
7918
8023
  return calls.length > 0 && calls.every((call) => call.pathLiteral !== null);
7919
8024
  }
8025
+ var KNOWN_PYTHON_EXT_STR = "java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|css|scss|sass|less|md|mdx|rst|txt|json|yaml|yml|toml|xml|html|htm|conf|cfg|ini|properties|sql|ps1|psm1|env";
8026
+ var OPEN_EXT = new RegExp(`\\.(?:${KNOWN_PYTHON_EXT_STR})$`, "i");
7920
8027
  function extractPythonFileRead(cmd) {
7921
- if (!/^python3?\b/.test(cmd)) return null;
7922
- if (pythonOpenWritesAFile(cmd) || pythonWritesThroughFileObject(cmd)) return null;
7923
- const outputOpen = /open\s*\(\s*r?['"]([^'"]+\.output)['"]/i.exec(cmd);
8028
+ let inner = cmd.trim();
8029
+ const w = POWERSHELL_WRAP_RE.exec(inner);
8030
+ if (w) {
8031
+ inner = (w[1] ?? w[2] ?? "").trim();
8032
+ }
8033
+ const psHereMatch = /^@'([\s\S]*?)'@\s*\|\s*(?:python3?|py)(?:\.exe)?(?:\s+-\S*|\s+-)?\s*$/i.exec(inner) ?? /^@"([\s\S]*?)"@\s*\|\s*(?:python3?|py)(?:\.exe)?(?:\s+-\S*|\s+-)?\s*$/i.exec(inner);
8034
+ let pythonBody = null;
8035
+ if (psHereMatch) {
8036
+ pythonBody = (psHereMatch[1] ?? "").trim();
8037
+ } else if (/^(?:python3?|py)(?:\.exe)?\b/i.test(inner)) {
8038
+ pythonBody = inner;
8039
+ }
8040
+ if (pythonBody === null) return null;
8041
+ if (pythonOpenWritesAFile(pythonBody) || pythonWritesThroughFileObject(pythonBody)) return null;
8042
+ const outputOpen = /open\s*\(\s*r?['"]([^'"]+\.output)['"]/i.exec(pythonBody);
7924
8043
  if (outputOpen?.[1]) {
7925
8044
  const filePath = outputOpen[1];
7926
8045
  if (isOrchestratorStateFile(filePath)) return null;
7927
- return { filePath, isDoc: false, isOutputFile: true };
8046
+ return { filePath, isDoc: false, isConfig: false, isEnv: false, isSql: false, isOutputFile: true };
7928
8047
  }
7929
- const OPEN_EXT = /\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties|ps1|psm1)/i;
7930
- const heredocMatch = /^python3?\s+-\s+<<\s*'?(\w+)'?\s*\n([\s\S]*?)\n\1\s*$/.exec(cmd);
8048
+ const classifyResult = (filePath) => {
8049
+ const flags = classifyFileExtensions(filePath);
8050
+ if (flags !== null) {
8051
+ return { filePath, isDoc: flags.isDoc, isConfig: flags.isConfig, isEnv: flags.isEnv, isSql: flags.isSql, isOutputFile: false };
8052
+ }
8053
+ const { isDoc, isConfig, isSql } = classifyDocConfig(filePath);
8054
+ const isEnv = /\.env(\.\w+)?$/i.test(filePath);
8055
+ return { filePath, isDoc, isConfig, isEnv, isSql, isOutputFile: false };
8056
+ };
8057
+ const heredocMatch = /^python3?\s+-\s+<<\s*'?(\w+)'?\s*\n([\s\S]*?)\n\1\s*$/.exec(pythonBody);
7931
8058
  if (heredocMatch) {
7932
8059
  const body = heredocMatch[2] ?? "";
7933
8060
  if (pythonOpenWritesAFile(body) || pythonWritesThroughFileObject(body)) return null;
7934
- const heredocOpen = /open\s*\(\s*r?['"]([^'"]+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties))['"]/i.exec(body);
8061
+ const heredocOpen = new RegExp(`open\\s*\\(\\s*r?['"]([^'"]+\\.(?:${KNOWN_PYTHON_EXT_STR}))['"]`, "i").exec(body);
7935
8062
  if (heredocOpen?.[1]) {
7936
8063
  const filePath = heredocOpen[1];
7937
8064
  if (isOrchestratorStateFile(filePath)) return null;
7938
- const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7939
- return { filePath, isDoc, isOutputFile: false };
8065
+ return classifyResult(filePath);
7940
8066
  }
7941
8067
  if (/open\s*\(/.test(body) && !pythonOpenPathsAreAllLiteral(body)) {
7942
- const literal2 = /['"]([^'"]+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties))['"]/i.exec(body);
8068
+ const literal2 = new RegExp(`['"]([^'"]+\\.(?:${KNOWN_PYTHON_EXT_STR}))['"]`, "i").exec(body);
7943
8069
  if (literal2?.[1]) {
7944
8070
  const filePath = literal2[1];
7945
8071
  if (isOrchestratorStateFile(filePath)) return null;
7946
8072
  if (OPEN_EXT.test(filePath)) {
7947
- const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7948
- return { filePath, isDoc, isOutputFile: false };
8073
+ return classifyResult(filePath);
7949
8074
  }
7950
8075
  }
7951
8076
  }
7952
8077
  return null;
7953
8078
  }
7954
- const direct = /open\(['"]([^'"]+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties))['"]/i.exec(cmd);
8079
+ const direct = new RegExp(`open\\s*\\(\\s*r?['"]([^'"]+\\.(?:${KNOWN_PYTHON_EXT_STR}))['"]`, "i").exec(pythonBody);
7955
8080
  if (direct) {
7956
8081
  const filePath = direct[1] ?? "";
7957
8082
  if (!filePath) return null;
7958
8083
  if (isOrchestratorStateFile(filePath)) return null;
7959
- const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7960
- return { filePath, isDoc, isOutputFile: false };
8084
+ return classifyResult(filePath);
7961
8085
  }
7962
- if (/open\s*\(/.test(cmd) && !pythonOpenPathsAreAllLiteral(cmd)) {
7963
- const literal2 = /['"]([^'"]+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties))['"]/i.exec(cmd);
8086
+ if (/open\s*\(/.test(pythonBody) && !pythonOpenPathsAreAllLiteral(pythonBody)) {
8087
+ const literal2 = new RegExp(`['"]([^'"]+\\.(?:${KNOWN_PYTHON_EXT_STR}))['"]`, "i").exec(pythonBody);
7964
8088
  if (literal2) {
7965
8089
  const filePath = literal2[1] ?? "";
7966
8090
  if (filePath) {
7967
8091
  if (isOrchestratorStateFile(filePath)) return null;
7968
8092
  if (OPEN_EXT.test(filePath)) {
7969
- const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7970
- return { filePath, isDoc, isOutputFile: false };
8093
+ return classifyResult(filePath);
7971
8094
  }
7972
8095
  }
7973
8096
  }
@@ -8290,7 +8413,7 @@ function extractToolResultsFile(cmd) {
8290
8413
  return null;
8291
8414
  }
8292
8415
  function extractDirectoryListing(cmd) {
8293
- return /^eza\s+.*--long\s+\S+/.test(cmd) || /^eza\s+.*--tree/.test(cmd) || /^tree(\s|$)/.test(cmd) || /^ls\s+(?:\S+\s+)*-[a-zA-Z]*R[a-zA-Z]*(?:\s|$)/.test(cmd) || /^ls\s+(?:-[la]+\s+)?(\S+)\s*[|]\s*head/.test(cmd) || /^ls\s+(?:-[la]+\s+)?(\S+)\s*[|]\s*grep/.test(cmd) || /^ls\s+(?:-[la]+\s+)?(\S+)\s*[|]\s*wc/.test(cmd);
8416
+ return /^eza\s+.*--long\s+\S+/.test(cmd) || /^eza\s+.*--tree/.test(cmd) || /^tree(\s|$)/.test(cmd) || /^ls\s+(?:\S+\s+)*-[a-zA-Z]*R[a-zA-Z]*(?:\s|$)/.test(cmd) || /^ls\s+(?:-[la]+\s+)?(\S+)\s*[|]\s*head/.test(cmd) || /^ls\s+(?:-[la]+\s+)?(\S+)\s*[|]\s*grep/.test(cmd) || /^ls\s+(?:-[la]+\s+)?(\S+)\s*[|]\s*wc/.test(cmd) || /^(?:Get-ChildItem|gci|dir)\b.*(?:-Recurse|-r\b|\/s\b)/i.test(cmd);
8294
8417
  }
8295
8418
  function extractForLoopWcL(cmd) {
8296
8419
  return /^for\s+\w+\s+in\s+.*;\s*do\s+wc\s+-l/.test(cmd);
@@ -8605,6 +8728,73 @@ function maybeCompressRewrite(event, rawCmd, cmd) {
8605
8728
  const wrapped = `token-goat compress -f ${filterName} --timeout ${cfg.timeout_seconds} -c ${shellQuoteSingle(rawCmd)}`;
8606
8729
  return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: wrapped } };
8607
8730
  }
8731
+ var PIPELINE_PASSTHROUGH_HEADS = /* @__PURE__ */ new Set(["head", "tail", "cat", "tee", "less", "more"]);
8732
+ var CI_BUILD_TEST_FILTER_NAMES = /* @__PURE__ */ new Set([
8733
+ "generic-ci",
8734
+ "jest",
8735
+ "vitest",
8736
+ "pytest",
8737
+ "go_test",
8738
+ "cargo_test",
8739
+ "cargo",
8740
+ "go",
8741
+ "make",
8742
+ "cmake",
8743
+ "gradle",
8744
+ "maven",
8745
+ "dotnet",
8746
+ "turbo",
8747
+ "nx",
8748
+ "lerna",
8749
+ "webpack",
8750
+ "eslint",
8751
+ "ruff",
8752
+ "clippy",
8753
+ "flake8",
8754
+ "mypy",
8755
+ "prettier",
8756
+ "tsc"
8757
+ ]);
8758
+ function isCiBuildTestSegment(cleaned, cwd) {
8759
+ if (isTestRunnerCommand(cleaned) || isBuildCommand(cleaned) || isTscCommand(cleaned)) return true;
8760
+ if (/^\s*(npm|pnpm|yarn|bun)\s+(run\s+)?(test|build|lint|typecheck|check|ci|guards)\b/i.test(cleaned)) return true;
8761
+ if (/^\s*(cargo|go|dotnet|make|gradle|mvn|pytest|vitest|jest|eslint|ruff)\b/i.test(cleaned)) return true;
8762
+ const detected = detectFromCommand(cleaned, cwd ?? void 0);
8763
+ if (detected !== null && CI_BUILD_TEST_FILTER_NAMES.has(detected.filter.name)) return true;
8764
+ return false;
8765
+ }
8766
+ function pipelineShapeFilter(cmd, cwd) {
8767
+ if (hasUnquotedOperator(cmd, ["&&", "||", ";"])) {
8768
+ const forSplit2 = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
8769
+ if (hasBareBackgroundOrNewline(forSplit2)) return null;
8770
+ const segments2 = splitShellSegments(forSplit2);
8771
+ if (segments2.length >= 2) {
8772
+ let recognizedCiCount = 0;
8773
+ for (const segment of segments2) {
8774
+ const cleaned = stripOutputPipeline(segment.trim());
8775
+ if (cleaned.length === 0) continue;
8776
+ if (isCiBuildTestSegment(cleaned, cwd)) {
8777
+ recognizedCiCount++;
8778
+ }
8779
+ }
8780
+ if (recognizedCiCount > 0) {
8781
+ return filterByName("generic-ci");
8782
+ }
8783
+ }
8784
+ return null;
8785
+ }
8786
+ const forSplit = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
8787
+ if (hasBareBackgroundOrNewline(forSplit)) return null;
8788
+ const segments = splitShellSegments(forSplit);
8789
+ if (segments.length < 2) return null;
8790
+ for (const segment of segments.slice(1)) {
8791
+ const head = safeShlexSplit(segment)?.[0];
8792
+ if (head === void 0) return null;
8793
+ if (!PIPELINE_PASSTHROUGH_HEADS.has(head.replace(/^.*[/\\]/, ""))) return null;
8794
+ }
8795
+ const detected = detectFromCommand(stripOutputPipeline(cmd), cwd ?? void 0);
8796
+ return detected === null ? null : detected.filter;
8797
+ }
8608
8798
  function pureFileReadPath(cmd) {
8609
8799
  const single = extractCatFile(cmd)?.filePath ?? extractHeadFile(cmd)?.filePath ?? extractTailFile(cmd)?.filePath ?? extractLineRangeRead(cmd)?.filePath;
8610
8800
  if (single !== void 0) return single;
@@ -8628,10 +8818,10 @@ function deliveredLineNumbers(cmd, lineCount) {
8628
8818
  if (singleFileCompoundReadPath(cmd) !== null) return Array.from({ length: lineCount }, () => null);
8629
8819
  return null;
8630
8820
  }
8631
- function elideServedShellLines(cmd, output, priorIds) {
8821
+ function elideServedShellLines(cmd, output, priorIds, unknownLineNumbers = false) {
8632
8822
  if (priorIds.length === 0) return null;
8633
8823
  const lines = output.split("\n");
8634
- const numbers = deliveredLineNumbers(cmd, lines.length);
8824
+ const numbers = unknownLineNumbers ? Array.from({ length: lines.length }, () => null) : deliveredLineNumbers(cmd, lines.length);
8635
8825
  if (numbers === null) return null;
8636
8826
  const rows = lines.map((text, i) => ({ no: numbers[i] ?? null, text, raw: text }));
8637
8827
  const bodies = [];
@@ -8749,6 +8939,26 @@ async function maybeCollapseIdenticalRead(cmd, rawCmd, output, exitCode, cwd, ca
8749
8939
  if (!isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(pointer, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) return null;
8750
8940
  return emitRewrite(pointer, identical ? "identical file re-read collapsed" : "already-served file lines collapsed", { kind: identical ? "bash_compress:identical-reread" : "bash_compress:contained-reread", originalBytes: deliveredOutputBytes(originalBytes) });
8751
8941
  }
8942
+ async function maybeElideServedGenericOutput(cmd, output, exitCode, cwd, cacheMinBytes) {
8943
+ if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
8944
+ if (!loadConfig().bash_compress.elide_served_shell_output) return null;
8945
+ if (exitCode !== null && exitCode !== 0) return null;
8946
+ if (redactSecrets(output).count > 0) return null;
8947
+ const originalBytes = Buffer.byteLength(output, "utf-8");
8948
+ if (originalBytes < cacheMinBytes) return null;
8949
+ const priorIds = getFileServedOutputs(GENERIC_SERVED_OUTPUT_KEY).filter((id) => getBashOutput(id)?.command !== cmd);
8950
+ let rewrittenText = null;
8951
+ if (priorIds.length > 0) {
8952
+ const elided = elideServedShellLines(cmd, output, priorIds, true);
8953
+ if (elided !== null && isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(elided, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) {
8954
+ rewrittenText = elided;
8955
+ }
8956
+ }
8957
+ const storedId = await storeBashOutput(cmd, rewrittenText ?? output, exitCode ?? 0, cwd);
8958
+ recordFileServedOutput(GENERIC_SERVED_OUTPUT_KEY, storedId);
8959
+ if (rewrittenText === null) return null;
8960
+ return emitRewrite(rewrittenText, "already-served shell output collapsed", { kind: "bash_compress:generic-served-elision", originalBytes: deliveredOutputBytes(originalBytes) });
8961
+ }
8752
8962
  async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes) {
8753
8963
  if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
8754
8964
  if (isCompressibleSingleCommand(cmd)) return null;
@@ -8761,7 +8971,8 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
8761
8971
  }
8762
8972
  if (!cfg.enabled || cfg.disabled_filters.includes("generic")) return null;
8763
8973
  if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return null;
8764
- const filter3 = filterByName("generic");
8974
+ const shaped = pipelineShapeFilter(cmd, cwd);
8975
+ const filter3 = shaped !== null && !cfg.disabled_filters.includes(shaped.name) ? shaped : filterByName("generic");
8765
8976
  if (filter3 === null) return null;
8766
8977
  const compressed = compressOutput(filter3, output, "", exitCode ?? 0, [], {
8767
8978
  maxLines: cfg.max_lines,
@@ -8783,7 +8994,7 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
8783
8994
  return null;
8784
8995
  }
8785
8996
  await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8786
- return emitRewrite(body, "bash", { kind: "bash_compress:generic", originalBytes: deliveredOutputBytes(compressed.originalBytes) });
8997
+ return emitRewrite(body, "bash", { kind: `bash_compress:${filter3.name}`, originalBytes: deliveredOutputBytes(compressed.originalBytes) });
8787
8998
  }
8788
8999
  function maybeStripAnsiOnly(output) {
8789
9000
  if (!output.includes("\x1B")) return null;
@@ -9078,7 +9289,7 @@ function preBashHandlerInner(event) {
9078
9289
  recordStat("session_hint", 0, 0);
9079
9290
  const hints = [];
9080
9291
  for (const { filePath, ranges, tool } of sedReads) {
9081
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9292
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9082
9293
  const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
9083
9294
  const overlapHints = [];
9084
9295
  const freshRanges = [];
@@ -9099,7 +9310,7 @@ function preBashHandlerInner(event) {
9099
9310
  const catJsonPipe = extractCatJsonPipe(cmd);
9100
9311
  if (catJsonPipe !== null) {
9101
9312
  const { filePath } = catJsonPipe;
9102
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9313
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9103
9314
  recordStat("session_hint", 0, 0);
9104
9315
  return contextOutput(
9105
9316
  '`cat | jq` loads the whole file. Use `token-goat config-get "' + hintPath + '" KEY_NAME` or `token-goat section "' + hintPath + '::sectionName"` to slice one value.'
@@ -9108,7 +9319,7 @@ function preBashHandlerInner(event) {
9108
9319
  const catResult = extractCatFile(cmd);
9109
9320
  if (catResult !== null) {
9110
9321
  const { filePath, isDoc, isEnv, isConfig, isSql, cmd0, advisoryOnly } = catResult;
9111
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9322
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9112
9323
  recordStat("session_hint", 0, 0);
9113
9324
  if (isSql) {
9114
9325
  return contextOutput(
@@ -9123,7 +9334,7 @@ function preBashHandlerInner(event) {
9123
9334
  recordStat("session_hint", 0, 0);
9124
9335
  const cmd0 = catMulti[0].cmd0;
9125
9336
  const perPath = catMulti.map(({ filePath, isDoc, isEnv, isConfig, isSql }) => {
9126
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9337
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9127
9338
  const how = isSql ? 'token-goat section "' + hintPath + '::table_name"' : isEnv || isConfig ? 'token-goat config-get "' + hintPath + '" KEY_NAME' : isDoc ? 'token-goat section "' + hintPath + '::SectionHeading"' : 'token-goat read "' + hintPath + '::SymbolName"';
9128
9339
  return " " + hintPath + " -> `" + how + "`";
9129
9340
  });
@@ -9133,7 +9344,7 @@ function preBashHandlerInner(event) {
9133
9344
  const psGetContentResult = extractPowerShellWrappedGetContent(cmd);
9134
9345
  if (psGetContentResult !== null) {
9135
9346
  const { filePath, isDoc, isEnv, isConfig, isSql } = psGetContentResult;
9136
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9347
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9137
9348
  recordStat("session_hint", 0, 0);
9138
9349
  const lead = "`Get-Content` via a `powershell -Command` wrapper bypasses read hooks and loads the entire file into context. ";
9139
9350
  if (isSql) {
@@ -9145,7 +9356,7 @@ function preBashHandlerInner(event) {
9145
9356
  const wslCatResult = extractWslCatFile(cmd);
9146
9357
  if (wslCatResult !== null) {
9147
9358
  const { filePath, isDoc, isEnv, isConfig, isSql } = wslCatResult;
9148
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9359
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9149
9360
  recordStat("session_hint", 0, 0);
9150
9361
  if (isSql) {
9151
9362
  return contextOutput(
@@ -9157,8 +9368,8 @@ function preBashHandlerInner(event) {
9157
9368
  }
9158
9369
  const pyRead = extractPythonFileRead(cmd);
9159
9370
  if (pyRead !== null) {
9160
- const { filePath, isDoc, isOutputFile } = pyRead;
9161
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9371
+ const { filePath, isDoc, isConfig, isEnv, isSql, isOutputFile } = pyRead;
9372
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9162
9373
  recordStat("session_hint", 0, 0);
9163
9374
  if (isOutputFile) {
9164
9375
  if (taskOutputIsJsonlTranscript(filePath)) {
@@ -9169,41 +9380,46 @@ function preBashHandlerInner(event) {
9169
9380
  "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.'
9170
9381
  );
9171
9382
  }
9172
- const hint = isDoc ? 'Use `token-goat section "' + hintPath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + hintPath + '::SymbolName"` to extract a specific symbol.';
9383
+ if (isSql) {
9384
+ return contextOutput(
9385
+ 'Python `open()` file reads bypass read hooks. Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.'
9386
+ );
9387
+ }
9388
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc);
9173
9389
  return cdStripped ? contextOutput("Python `open()` file reads bypass read hooks. " + hint) : denyOutput("Python `open()` file reads bypass read hooks. " + hint);
9174
9390
  }
9175
9391
  const tailResult = extractTailFile(cmd);
9176
9392
  if (tailResult !== null) {
9177
9393
  const { filePath, isDoc, isConfig, isSql } = tailResult;
9178
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9394
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9179
9395
  recordStat("session_hint", 0, 0);
9180
9396
  return contextOutput("`tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql));
9181
9397
  }
9182
9398
  const headResult = extractHeadFile(cmd);
9183
9399
  if (headResult !== null) {
9184
9400
  const { filePath, isDoc, isConfig, isSql, n } = headResult;
9185
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9401
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9186
9402
  recordStat("session_hint", 0, 0);
9187
9403
  return contextOutput(leadingLinesHint("`head` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql }, preHookCwd));
9188
9404
  }
9189
9405
  const gcTailResult = extractGetContentTail(cmd);
9190
9406
  if (gcTailResult !== null) {
9191
9407
  const { filePath, isDoc, isConfig, isSql } = gcTailResult;
9192
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9408
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9193
9409
  recordStat("session_hint", 0, 0);
9194
9410
  return contextOutput("`Get-Content -Tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql));
9195
9411
  }
9196
9412
  const gcSelectResult = extractGetContentSelectFirst(cmd);
9197
9413
  if (gcSelectResult !== null) {
9198
9414
  const { filePath, isDoc, isConfig, isSql, n } = gcSelectResult;
9199
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9415
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9200
9416
  recordStat("session_hint", 0, 0);
9201
9417
  return contextOutput(leadingLinesHint("`Select-Object -First` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql }, preHookCwd));
9202
9418
  }
9203
9419
  const nodeRead = extractNodeFileRead(cmd);
9204
9420
  if (nodeRead !== null) {
9205
9421
  const { filePath, isDoc, isConfig, isSql } = nodeRead;
9206
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9422
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9207
9423
  const lead = "Node.js `fs.readFileSync()` bypasses read hooks. ";
9208
9424
  if (isSql) {
9209
9425
  recordStat("session_hint", 0, 0);
@@ -9213,6 +9429,19 @@ function preBashHandlerInner(event) {
9213
9429
  recordStat("session_hint", 0, 0);
9214
9430
  return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9215
9431
  }
9432
+ const psMethodRead = extractPowerShellFileMethodRead(cmd);
9433
+ if (psMethodRead !== null) {
9434
+ const { filePath, isDoc, isConfig, isSql } = psMethodRead;
9435
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9436
+ const lead = "PowerShell `[IO.File]::ReadAllText()` bypasses read hooks. ";
9437
+ if (isSql) {
9438
+ recordStat("session_hint", 0, 0);
9439
+ return contextOutput(lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9440
+ }
9441
+ 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.';
9442
+ recordStat("session_hint", 0, 0);
9443
+ return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9444
+ }
9216
9445
  if (extractGrepPipeChain(cmd)) {
9217
9446
  recordStat("session_hint", 0, 0);
9218
9447
  return contextOutput(
@@ -9222,7 +9451,7 @@ function preBashHandlerInner(event) {
9222
9451
  const mdHeadingGrep = extractMarkdownHeadingGrep(cmd);
9223
9452
  if (mdHeadingGrep !== null) {
9224
9453
  const { filePath } = mdHeadingGrep;
9225
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9454
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9226
9455
  recordStat("session_hint", 0, 0);
9227
9456
  return contextOutput(
9228
9457
  'Use `token-goat outline "' + hintPath + '"` to get all headings with line ranges \u2014 then `token-goat section "' + hintPath + '::Heading"` to read one section.'
@@ -9239,7 +9468,7 @@ function preBashHandlerInner(event) {
9239
9468
  const rgStructural = extractRgStructuralSearch(cmd);
9240
9469
  if (rgStructural !== null) {
9241
9470
  const { filePath } = rgStructural;
9242
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9471
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9243
9472
  recordStat("session_hint", 0, 0);
9244
9473
  return contextOutput(
9245
9474
  '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.'
@@ -9443,6 +9672,21 @@ function recordBashFileReadsForSessionCache(cmd, cwd) {
9443
9672
  recordFileRead(resolve(wslCat.filePath));
9444
9673
  return;
9445
9674
  }
9675
+ const nodeRead = extractNodeFileRead(cmd);
9676
+ if (nodeRead !== null) {
9677
+ recordFileRead(resolve(nodeRead.filePath));
9678
+ return;
9679
+ }
9680
+ const psMethod = extractPowerShellFileMethodRead(cmd);
9681
+ if (psMethod !== null) {
9682
+ recordFileRead(resolve(psMethod.filePath));
9683
+ return;
9684
+ }
9685
+ const pyRead = extractPythonFileRead(cmd);
9686
+ if (pyRead !== null && !pyRead.isOutputFile) {
9687
+ recordFileRead(resolve(pyRead.filePath));
9688
+ return;
9689
+ }
9446
9690
  }
9447
9691
  async function postBashHandler(event) {
9448
9692
  try {
@@ -9538,6 +9782,10 @@ async function postBashHandler(event) {
9538
9782
  if (identical !== null) return identical;
9539
9783
  const compound = await maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes);
9540
9784
  if (compound !== null) return compound;
9785
+ if (!isFileRead && extractLineRangeReadsCompound(cmd) === null) {
9786
+ const genericElision = await maybeElideServedGenericOutput(cmd, output, exitCode, cwd, cacheMinBytes);
9787
+ if (genericElision !== null) return genericElision;
9788
+ }
9541
9789
  return maybeStripAnsiOnly(output) ?? passOutput();
9542
9790
  }
9543
9791
  if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return passOutput();
@@ -9794,7 +10042,7 @@ function writeLedger(target, ledger) {
9794
10042
  }
9795
10043
  }
9796
10044
  function repeatFailureNotice(toolName) {
9797
- const tool = toolName === void 0 || toolName === "" ? "This tool" : toolName;
10045
+ const tool = toolName === void 0 || toolName === "" ? "This tool" : displaySafeText(toolName);
9798
10046
  return `[token-goat] ${tool} just failed with the same error as an earlier call this session. Retrying it unchanged will fail the same way -- change the arguments, the tool, or the approach.`;
9799
10047
  }
9800
10048
  function postToolUseFailureHandler(event) {
@@ -10722,13 +10970,16 @@ var SPAWN_RESTRICT_HINT_KEY = "agent-spawn-restrict-hint";
10722
10970
  var SPAWN_RESTRICT_MAX_NAMES = 3;
10723
10971
  var ROSTER_WALK_MAX_DEPTH = 4;
10724
10972
  var ROSTER_WALK_MAX_FILES = 400;
10973
+ var AGENT_NAME_RE = /^[A-Za-z0-9._:-]{1,64}$/;
10725
10974
  function parseAgentDefinition(text, fallbackName) {
10726
10975
  const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
10727
10976
  if (!fmMatch) return null;
10728
10977
  const fm = fmMatch[1];
10729
10978
  const nameMatch = /^name:(.*)$/m.exec(fm);
10730
10979
  const rawName = nameMatch ? nameMatch[1].trim().replace(/^["']|["']$/g, "") : "";
10731
- const name2 = rawName !== "" ? rawName : fallbackName;
10980
+ const resolved = AGENT_NAME_RE.test(rawName) ? rawName : fallbackName;
10981
+ if (!AGENT_NAME_RE.test(resolved)) return null;
10982
+ const name2 = resolved;
10732
10983
  const toolsMatch = /^tools:(.*)$/m.exec(fm);
10733
10984
  if (!toolsMatch) return { name: name2, restricted: false };
10734
10985
  const inline = toolsMatch[1].trim();
@@ -10742,7 +10993,7 @@ function parseAgentDefinition(text, fallbackName) {
10742
10993
  return { name: name2, restricted: false };
10743
10994
  }
10744
10995
  function findRestrictedAgentNames(roots) {
10745
- const scanRoots = roots ?? [path2.join(os.homedir(), ".claude", "agents")];
10996
+ const scanRoots = roots ?? [path2.join(os.homedir(), ".claude", "agents"), path2.join(process.cwd(), ".claude", "agents")];
10746
10997
  const names = /* @__PURE__ */ new Set();
10747
10998
  const visited = /* @__PURE__ */ new Set();
10748
10999
  let filesSeen = 0;
@@ -10802,7 +11053,7 @@ function buildUnrestrictedSpawnAdvisory(toolInput) {
10802
11053
  if (names.length === 0) return "";
10803
11054
  markHintShown(SPAWN_RESTRICT_HINT_KEY);
10804
11055
  recordStat("session_hint", 0, 0, void 0, "agent-spawn-restrict");
10805
- const shown = names.slice(0, SPAWN_RESTRICT_MAX_NAMES).join(", ");
11056
+ const shown = neutralizeSpokenMarkers(names.slice(0, SPAWN_RESTRICT_MAX_NAMES).join(", "));
10806
11057
  const more = names.length > SPAWN_RESTRICT_MAX_NAMES ? ` and ${names.length - SPAWN_RESTRICT_MAX_NAMES} more` : "";
10807
11058
  return `[token-goat] This spawn ran as general-purpose (the default when subagent_type is omitted), which is unrestricted: its lane starts by paying for every tool and MCP schema on the machine. Tools-restricted agent definitions exist here: ${shown}${more}. A future spawn that fits one of them can pass that name as subagent_type to start with a much smaller prefix. Advisory only: this spawn has already run, and this notice saved nothing.`;
10808
11059
  } catch {