token-goat 2.8.6 → 2.9.2

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.
@@ -1,20 +1,23 @@
1
1
  import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
+ import {
4
+ deliveredOutputBytes,
5
+ wrappedShell
6
+ } from "./token-goat-chunk-CGWACYYZ.mjs";
3
7
  import {
4
8
  ToolFilter,
5
9
  capTokens,
6
10
  combineStreams,
7
11
  compressOutput,
12
+ compressedTokensSaved,
13
+ dispatchArgv,
8
14
  filterByName,
15
+ loadConfig,
16
+ recordStat,
9
17
  resolveMinNetSavingsBytes,
10
18
  selectFilter,
11
- shlexSplit,
12
- wrappedShell
13
- } from "./token-goat-chunk-GUNYAGOZ.mjs";
14
- import {
15
- loadConfig,
16
- recordStat
17
- } from "./token-goat-chunk-2JZ66BBE.mjs";
19
+ shlexSplit
20
+ } from "./token-goat-chunk-UZ2NFOOZ.mjs";
18
21
  import "./token-goat-chunk-AO2QD2AG.mjs";
19
22
  import "./token-goat-chunk-AEX54RUZ.mjs";
20
23
 
@@ -28,17 +31,19 @@ var DEFAULT_TIMEOUT_SECONDS = 600;
28
31
  var MAX_CAPTURE_BYTES = 32 * 1024 * 1024;
29
32
  var MIN_RECORD_STAT_BYTES = 32;
30
33
  function resolveFilter(command, filterName, cwd) {
31
- if (filterName) {
32
- const named = filterByName(filterName);
33
- if (named !== null) return named;
34
- }
35
- let argv;
34
+ let split;
36
35
  try {
37
- argv = shlexSplit(command);
36
+ split = shlexSplit(command);
38
37
  } catch {
39
- return null;
38
+ split = null;
39
+ }
40
+ const argv = split === null ? [command] : dispatchArgv(split, cwd).argv;
41
+ if (filterName) {
42
+ const named = filterByName(filterName);
43
+ if (named !== null) return { filter: named, argv };
40
44
  }
41
- return selectFilter(argv, cwd);
45
+ if (split === null) return { filter: null, argv };
46
+ return { filter: selectFilter(split, cwd), argv };
42
47
  }
43
48
  function baseSpawnOptions(timeout, cwd, env) {
44
49
  return { shell: wrappedShell(), timeout: timeout * 1e3, cwd, env };
@@ -66,14 +71,14 @@ function resolveCompressLimits() {
66
71
  }
67
72
  function run(command, opts = {}) {
68
73
  const timeout = opts.timeout ?? DEFAULT_TIMEOUT_SECONDS;
69
- const filter = resolveFilter(command, opts.filterName, opts.cwd);
74
+ const { filter, argv } = resolveFilter(command, opts.filterName, opts.cwd);
70
75
  if (filter === null) {
71
76
  if ((opts.maxTokens ?? 0) > 0) {
72
- return wrapAndCompress(command, new IdentityFilter(), timeout, resolveProfile(opts.compressionProfile), opts);
77
+ return wrapAndCompress(command, argv, new IdentityFilter(), timeout, resolveProfile(opts.compressionProfile), opts);
73
78
  }
74
79
  return passthrough(command, timeout, opts.cwd, opts.env);
75
80
  }
76
- return wrapAndCompress(command, filter, timeout, resolveProfile(opts.compressionProfile), opts);
81
+ return wrapAndCompress(command, argv, filter, timeout, resolveProfile(opts.compressionProfile), opts);
77
82
  }
78
83
  function runRaw(command, timeout = DEFAULT_TIMEOUT_SECONDS) {
79
84
  return passthrough(command, timeout, void 0, void 0);
@@ -92,7 +97,7 @@ function decode(buf) {
92
97
  if (buf == null) return "";
93
98
  return typeof buf === "string" ? buf : buf.toString("utf8");
94
99
  }
95
- function wrapAndCompress(command, filter, timeout, profile, opts) {
100
+ function wrapAndCompress(command, argv, filter, timeout, profile, opts) {
96
101
  const writeStdout = opts.writeStdout ?? ((s) => process.stdout.write(s));
97
102
  const result = spawnSync(command, {
98
103
  ...baseSpawnOptions(timeout, opts.cwd, opts.env),
@@ -118,12 +123,6 @@ function wrapAndCompress(command, filter, timeout, profile, opts) {
118
123
  } else {
119
124
  exitCode = 0;
120
125
  }
121
- let argv;
122
- try {
123
- argv = shlexSplit(command);
124
- } catch {
125
- argv = [command];
126
- }
127
126
  const limits = resolveCompressLimits();
128
127
  const compressed = compressOutput(filter, stdoutText, stderrText, exitCode, argv, {
129
128
  compressionProfile: profile,
@@ -142,8 +141,10 @@ function wrapAndCompress(command, filter, timeout, profile, opts) {
142
141
  return exitCode;
143
142
  }
144
143
  function recordSavings(result) {
145
- if (result.bytesSaved < MIN_RECORD_STAT_BYTES) return;
146
- recordStat(`bash_compress:${result.filterName}`, result.bytesSaved, result.tokensSaved);
144
+ const delivered = deliveredOutputBytes(result.originalBytes);
145
+ const bytesSaved = Math.max(0, delivered - result.compressedBytes);
146
+ if (bytesSaved < MIN_RECORD_STAT_BYTES) return;
147
+ recordStat(`bash_compress:${result.filterName}`, bytesSaved, compressedTokensSaved(bytesSaved));
147
148
  }
148
149
  export {
149
150
  DEFAULT_TIMEOUT_SECONDS,
@@ -1,32 +1,26 @@
1
1
  import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
- BASH_OUTPUT_SUBDIR,
4
+ DEFAULT_RECONCILE_BUDGET_MS,
5
5
  HOOK_EVENTS,
6
6
  accumulateResidentLines,
7
7
  buildCommandManifest,
8
8
  checkSymbolBodySize,
9
- commandHash,
10
9
  flattenCommandNames,
11
- getBashOutput,
12
10
  getWebOutput,
13
- indexRecallEntry,
14
- isBashEntryStale,
15
- isScopedGitStatusOrDiffStatCommand,
11
+ isReconcileClean,
16
12
  lineMayCarryResidentSignal,
17
13
  normalizePayload,
18
14
  readStdinJson,
19
15
  readTranscriptTail,
16
+ reconcileProject,
20
17
  repeatedSkillBodyHint,
21
- storeBashOutput,
22
18
  storeWebOutput,
23
- summarizeOutputDelta,
24
19
  summarizeResidentContext,
25
20
  taskListPruneHint
26
- } from "./token-goat-chunk-TELKICYU.mjs";
21
+ } from "./token-goat-chunk-VCNW7BGU.mjs";
27
22
  import {
28
- BODY_FIRST_TOOL_RESPONSE_KEYS,
29
- OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
23
+ BASH_OUTPUT_SUBDIR,
30
24
  UNTRUSTED_TOOL_TAG,
31
25
  WEB_FETCH_KEY_SEP,
32
26
  appendDirtyPath,
@@ -36,51 +30,45 @@ import {
36
30
  classifyBashHint,
37
31
  classifyEditHint,
38
32
  clearCurlDownload,
33
+ commandHash,
39
34
  compactPathFor,
40
35
  computeAdaptiveBudget,
41
- contextOutput,
42
36
  countSymbols,
43
- denyOutput,
44
- emitRewrite,
45
- emitRewriteIfChanged,
46
37
  enqueueDirtyPathSafe,
47
38
  ensureWorkerAlive,
48
39
  estimateTokens,
49
40
  extractCompactFromMarker,
50
- extractToolResponseField,
51
- extractToolResultText,
52
41
  fenceWithMatches,
53
42
  formatProjectMap,
54
43
  formatShrinkSummary,
44
+ getBashOutput,
55
45
  getBashOutputId,
56
46
  getContextPressure,
57
47
  getCurlDownloadPath,
58
- getCwd,
59
48
  getFileLineRanges,
60
- getFilePath,
49
+ getFileServedOutputs,
61
50
  getGlobMatchCount,
62
51
  getGrepMatchCount,
63
- getLastTabContext,
64
52
  getMonitoringRecallHint,
65
53
  getOutstandingAgentSpawns,
66
54
  getSessionBashOutputs,
67
55
  getSessionBashReruns,
68
56
  getSessionFiles,
69
57
  getSessionWebFetches,
70
- getToolInput,
71
- getToolName,
72
58
  hasSeenImage,
73
59
  hasSessionOutput,
74
60
  imageQualifiesForShrink,
75
61
  incrementSkillHit,
62
+ indexRecallEntry,
76
63
  installedSkillPath,
64
+ isBashEntryStale,
77
65
  isBuildCommand,
78
- isMcpErrorResponse,
66
+ isScopedGitStatusOrDiffStatCommand,
79
67
  isTestRunnerCommand,
68
+ lastTabContextMatches,
80
69
  listSiblingSessionStates,
81
70
  loadSessionCache,
82
71
  loadSessionState,
83
- makeDedupHintHandlers,
84
72
  markCompactStale,
85
73
  markCompacted,
86
74
  markFileTruncated,
@@ -88,7 +76,7 @@ import {
88
76
  matchesAllowPattern,
89
77
  matchesDenyPattern,
90
78
  meetsSavingsFloor,
91
- passOutput,
79
+ metadataEndpointRefusal,
92
80
  probeImageMeta,
93
81
  recordBashOutput,
94
82
  recordBashRerun,
@@ -97,6 +85,7 @@ import {
97
85
  recordFileEdit,
98
86
  recordFileLineRange,
99
87
  recordFileRead,
88
+ recordFileServedOutput,
100
89
  recordGlobQuery,
101
90
  recordGrepQuery,
102
91
  recordKnownRootThrottled,
@@ -105,58 +94,125 @@ import {
105
94
  recordSeenImage,
106
95
  recordSymbolRead,
107
96
  recordWebFetch,
108
- registerHook,
109
97
  removeOutstandingAgentSpawn,
110
- runHook,
111
98
  saveSessionState,
112
99
  scanAndRecord,
113
- serializeOutput,
114
100
  sessionOutputBodyBytes,
115
101
  sessionSidecarPath,
116
102
  setLastTabContext,
117
103
  shrinkImage,
104
+ storeBashOutput,
118
105
  storeBlob,
119
106
  storeOutput,
107
+ summarizeOutputDelta,
120
108
  takePendingLargeFileHint,
121
109
  visionTokens,
122
110
  visionTokensSaved,
123
111
  wasCliReadThisSession,
124
112
  wasFileReadThisSession,
125
113
  wasHintShown
126
- } from "./token-goat-chunk-DK4VLLYB.mjs";
114
+ } from "./token-goat-chunk-LJ3CHCTT.mjs";
127
115
  import {
128
116
  canRunWrappedShell,
129
- compressOutput,
130
- dedupeConsecutive,
131
- detectFromCommand,
132
- filterByName,
133
- hasBareBackgroundOrNewline,
134
- isRewriteWorthwhile,
135
- resolveMinNetSavingsBytes,
136
- shlexSplit
137
- } from "./token-goat-chunk-GUNYAGOZ.mjs";
117
+ deliveredOutputBytes
118
+ } from "./token-goat-chunk-CGWACYYZ.mjs";
138
119
  import {
120
+ BODY_FIRST_TOOL_RESPONSE_KEYS,
121
+ ENV_KEYS,
122
+ IDENTICAL_READ_MIN_BODY_BYTES,
123
+ OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
139
124
  PER_FILE_COUNTERFACTUAL_CEILING,
140
125
  VERSION,
126
+ compressOutput,
127
+ containsLineRun,
128
+ contextOutput,
129
+ countNoun,
130
+ dedupeConsecutive,
131
+ denyOutput,
132
+ detectFromCommand,
141
133
  detectHarness,
142
134
  detectLanguage,
143
135
  displaySafePath,
136
+ emitRewrite,
137
+ emitRewriteIfChanged,
138
+ envBool,
139
+ envInt,
144
140
  extractErrorMessage,
141
+ extractToolResponseField,
142
+ extractToolResultText,
143
+ filterByName,
145
144
  foldPath,
145
+ getCwd,
146
146
  getDb,
147
+ getFilePath,
147
148
  getHarnessName,
149
+ getToolInput,
150
+ getToolName,
148
151
  globalDbPath,
152
+ hasBareBackgroundOrNewline,
153
+ isMcpErrorResponse,
154
+ isRewriteWorthwhile,
149
155
  isUnderSystemTemp,
150
156
  loadConfig,
157
+ makeDedupHintHandlers,
151
158
  normalizePath,
159
+ passOutput,
152
160
  recordStat,
153
161
  redactSecrets,
162
+ registerHook,
154
163
  resolveIndexPath,
164
+ resolveMinNetSavingsBytes,
155
165
  runGit,
166
+ runHook,
156
167
  savedTokensFromBytes,
168
+ serializeOutput,
169
+ sessionStateKey,
170
+ shlexSplit,
157
171
  shortFingerprint,
172
+ stripAnsiEscapes,
158
173
  toKB
159
- } from "./token-goat-chunk-2JZ66BBE.mjs";
174
+ } from "./token-goat-chunk-UZ2NFOOZ.mjs";
175
+
176
+ // src/hint_suggestion_guard.ts
177
+ function looksLikeSuggestion(slice) {
178
+ return slice.includes('"');
179
+ }
180
+ var SAFE_OUTSIDE_QUOTES = /^[A-Za-z0-9 \t_./=:,@+-]*$/;
181
+ var CONTROL_OR_BIDI = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\u2066-\u2069\u061C\u200B-\u200D\u2028\u2029\uFE00-\uFE0F]|[\u{E0000}-\u{E007F}]/u;
182
+ function suggestionIsUnsafe(slice) {
183
+ if (slice.includes("$") || slice.includes("\n") || slice.includes("\r") || slice.includes("`")) return true;
184
+ if (CONTROL_OR_BIDI.test(slice)) return true;
185
+ const regions = slice.split('"');
186
+ if (regions.length % 2 === 0) return true;
187
+ for (let i = 2; i < regions.length; i += 2) {
188
+ if (!SAFE_OUTSIDE_QUOTES.test(regions[i] ?? "")) return true;
189
+ }
190
+ return false;
191
+ }
192
+ var OMITTED = "token-goat (command omitted: the path contains shell metacharacters)";
193
+ function stripUnsafeSuggestions(text) {
194
+ if (!text.includes("token-goat ")) return text;
195
+ let out = "";
196
+ let at = 0;
197
+ for (; ; ) {
198
+ const start = text.indexOf("token-goat ", at);
199
+ if (start === -1) return out + text.slice(at);
200
+ const lineBreak = text.slice(start).search(/[\r\n]/);
201
+ const line = lineBreak === -1 ? text.slice(start) : text.slice(start, start + lineBreak);
202
+ const firstTick = line.indexOf("`");
203
+ const narrow = firstTick === -1 ? line : line.slice(0, firstTick);
204
+ out += text.slice(at, start);
205
+ if (!looksLikeSuggestion(narrow) || !suggestionIsUnsafe(narrow)) {
206
+ out += narrow;
207
+ at = start + narrow.length;
208
+ continue;
209
+ }
210
+ const lastTick = line.lastIndexOf("`");
211
+ const wide = lastTick === -1 ? line : line.slice(0, lastTick);
212
+ out += OMITTED;
213
+ at = start + wide.length;
214
+ }
215
+ }
160
216
 
161
217
  // src/hooks_grep.ts
162
218
  function grepIntInput(toolInput, key) {
@@ -631,7 +687,7 @@ import crypto from "node:crypto";
631
687
  var TRACKED_SKILL = "token-goat";
632
688
  var MAX_COMMANDS_SHOWN = 8;
633
689
  async function currentCommandNames() {
634
- const { buildProgram } = await import("./token-goat-chunk-SHN4UTL4.mjs");
690
+ const { buildProgram } = await import("./token-goat-chunk-JMUBXBG7.mjs");
635
691
  return flattenCommandNames(buildCommandManifest(buildProgram()));
636
692
  }
637
693
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -887,23 +943,47 @@ registerHook("post_tool_use", pendingContextHandler, { advisory: true, followsMa
887
943
  // src/hooks_session_start.ts
888
944
  var GENERIC_REMINDER = 'token-goat: prefer surgical reads over the Read/Grep tools on this codebase; shell commands like `rg`, `grep`, `fd`, `sed`, `cat`, `find`, and `ls` are just commands, not tool names -- `token-goat symbol <name>`, `token-goat read "file::symbol"`, `token-goat section "file::Heading"`, `token-goat semantic "description"`, `token-goat outline <file>`. Run `token-goat index .` if this project is not indexed yet.';
889
945
  var INDEXED_REMINDER = 'token-goat: this project is indexed. Prefer `symbol <name>`, `read "file::symbol"`, `section "file::Heading"`, `semantic "description"`, or `outline <file>` over a full Read/Grep tool call; for JSON/YAML use `json-query file \'a.b.c\'` or `yaml-query` (nested keys are not symbols); shell commands like `rg`, `grep`, `fd`, `sed`, `cat`, `find`, and `ls` are still just commands.';
890
- function buildReminder(cwd) {
891
- if (cwd === void 0) return GENERIC_REMINDER;
892
- let symbolCount;
946
+ function isIndexedProject(cwd) {
947
+ if (cwd === void 0) return false;
893
948
  try {
894
- symbolCount = countSymbols({ rootDir: cwd }, globalDbPath());
949
+ return countSymbols({ rootDir: cwd }, globalDbPath()) > 0;
950
+ } catch {
951
+ return false;
952
+ }
953
+ }
954
+ function buildReminder(indexed) {
955
+ return indexed ? INDEXED_REMINDER : GENERIC_REMINDER;
956
+ }
957
+ function reconcileNote(cwd, indexed) {
958
+ if (!indexed) return null;
959
+ if (!envBool(ENV_KEYS.RECONCILE, true)) return null;
960
+ try {
961
+ const budgetMs = envInt(ENV_KEYS.RECONCILE_BUDGET_MS, DEFAULT_RECONCILE_BUDGET_MS, 0, 6e4);
962
+ const result = reconcileProject({ cwd, budgetMs });
963
+ if (isReconcileClean(result)) return null;
964
+ const parts = [];
965
+ if (result.changed.length > 0) parts.push(`${result.changed.length} changed`);
966
+ if (result.added.length > 0) parts.push(`${result.added.length} new`);
967
+ if (result.removed.length > 0) parts.push(`${result.removed.length} removed`);
968
+ const breakdown = parts.length > 1 ? ` (${parts.join(", ")})` : "";
969
+ const truncated = result.budgetExhausted ? ` (sweep stopped at its time budget with ${countNoun(result.unscanned, "file")} unchecked, so there may be more)` : "";
970
+ const total = result.changed.length + result.added.length + result.removed.length;
971
+ return `token-goat: reindexing ${countNoun(total, "file")} that changed outside this session${breakdown}${truncated}. Symbol lookups may be briefly stale.`;
895
972
  } catch {
896
- return GENERIC_REMINDER;
973
+ return null;
897
974
  }
898
- if (symbolCount <= 0) return GENERIC_REMINDER;
899
- return INDEXED_REMINDER;
900
975
  }
901
976
  function sessionStartHandler(event) {
902
977
  try {
903
978
  if (!loadConfig().hints.session_start_reminder) return passOutput();
904
979
  const cwd = getCwd(event);
905
- let context = buildReminder(cwd);
980
+ const indexed = isIndexedProject(cwd);
981
+ let context = buildReminder(indexed);
906
982
  if (cwd !== void 0) {
983
+ const drift = reconcileNote(cwd, indexed);
984
+ if (drift !== null) context += `
985
+
986
+ ${drift}`;
907
987
  const capsule = buildDeltaCapsule(cwd);
908
988
  if (capsule !== null) context += `
909
989
 
@@ -5180,7 +5260,7 @@ function resolveCyclicReferences(value, utils, meta) {
5180
5260
  }
5181
5261
  const hierarchy = getMetaDataHierarchy(meta);
5182
5262
  const depth = getCyclicReferenceDepth(value, hierarchy, 0);
5183
- if (depth > 0 && hierarchy !== void 0) {
5263
+ if (hierarchy !== void 0 && depth > 0) {
5184
5264
  return hierarchy[hierarchy.length - depth]?.result;
5185
5265
  }
5186
5266
  const type = getObjectType(value);
@@ -6997,6 +7077,10 @@ function preFetchHandler(event) {
6997
7077
  try {
6998
7078
  const urlOnlyCtx = resolveWebFetchUrl(event);
6999
7079
  if (urlOnlyCtx !== null) {
7080
+ const metadataRefusal = metadataEndpointRefusal(urlOnlyCtx.url);
7081
+ if (metadataRefusal !== null) {
7082
+ return denyOutput(`WebFetch blocked: ${metadataRefusal}.`);
7083
+ }
7000
7084
  const wfCfg = loadConfig().webfetch;
7001
7085
  if (wfCfg.deny.length > 0 && matchesDenyPattern(urlOnlyCtx.url, wfCfg.deny)) {
7002
7086
  return denyOutput(`WebFetch blocked: URL matches a configured webfetch.deny pattern.`);
@@ -7130,7 +7214,7 @@ async function preSkillHandler(event) {
7130
7214
  const denyCredit = cachedBytes !== null ? Math.min(cachedBytes, PER_FILE_COUNTERFACTUAL_CEILING) : 0;
7131
7215
  recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit));
7132
7216
  return denyOutput(
7133
- "Skill `" + skillName + "` was already loaded this session and is cached. Use `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."
7217
+ "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."
7134
7218
  );
7135
7219
  }
7136
7220
  const sourcePath = await installedSkillPath(skillName);
@@ -7145,12 +7229,12 @@ async function preSkillHandler(event) {
7145
7229
  const savedBytes = bodyBytes - compactBytes;
7146
7230
  recordStat("skill_compact_inlined", savedBytes, savedTokensFromBytes(savedBytes));
7147
7231
  return denyOutput(
7148
- "Skill `" + skillName + "` is large (" + bodyBytes + " bytes); its compact slice (" + compactBytes + " bytes) is inlined below instead of the full body. Run `token-goat skill-body " + skillName + "` if you need the full body.\n\n" + compact
7232
+ "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
7149
7233
  );
7150
7234
  }
7151
7235
  recordStat("skill_oversized_first_load");
7152
7236
  return denyOutput(
7153
- "Skill `" + skillName + "` is large (" + bodyBytes + " bytes) and has a compact slice available. Use `token-goat skill-body " + skillName + " --compact` to load the compact slice instead of the full body."
7237
+ "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."
7154
7238
  );
7155
7239
  }
7156
7240
  } catch {
@@ -8383,6 +8467,40 @@ function maybeCompressRewrite(event, rawCmd, cmd) {
8383
8467
  const wrapped = `token-goat compress -f ${filterName} --timeout ${cfg.timeout_seconds} -c ${shellQuoteSingle(rawCmd)}`;
8384
8468
  return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: wrapped } };
8385
8469
  }
8470
+ function pureFileReadPath(cmd) {
8471
+ return extractCatFile(cmd)?.filePath ?? extractHeadFile(cmd)?.filePath ?? extractTailFile(cmd)?.filePath ?? extractLineRangeRead(cmd)?.filePath ?? null;
8472
+ }
8473
+ async function maybeCollapseIdenticalRead(cmd, output, exitCode, cwd, cacheMinBytes) {
8474
+ if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
8475
+ if (exitCode !== null && exitCode !== 0) return null;
8476
+ const filePath = pureFileReadPath(cmd);
8477
+ if (filePath === null) return null;
8478
+ const originalBytes = Buffer.byteLength(output, "utf-8");
8479
+ if (originalBytes < Math.max(cacheMinBytes, IDENTICAL_READ_MIN_BODY_BYTES)) return null;
8480
+ const fileKey = resolveIndexPath(filePath, cwd ?? process.cwd());
8481
+ const priorIds = getFileServedOutputs(fileKey);
8482
+ let containerId = null;
8483
+ let identical = false;
8484
+ for (let i = priorIds.length - 1; i >= 0; i--) {
8485
+ const id = priorIds[i];
8486
+ if (id === void 0) continue;
8487
+ const prior = getBashOutput(id);
8488
+ if (prior === null || !containsLineRun(prior.output, output)) continue;
8489
+ containerId = id;
8490
+ identical = prior.output === output;
8491
+ break;
8492
+ }
8493
+ const sessionKey = shortFingerprint(stripOutputPipeline(cmd));
8494
+ if (containerId === null) {
8495
+ const storedId = await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8496
+ recordBashOutput(sessionKey, storedId, originalBytes);
8497
+ recordFileServedOutput(fileKey, storedId);
8498
+ return null;
8499
+ }
8500
+ const pointer = identical ? "[token-goat] Identical to an earlier run of this command in this session; the file has not changed since. " + originalBytes + " bytes withheld -- recall them with `token-goat bash-output " + containerId + "`." : "[token-goat] These " + originalBytes + " bytes already appear verbatim inside a wider read of " + filePath + " served earlier in this session. Withheld -- recall the full earlier output with `token-goat bash-output " + containerId + "`.";
8501
+ if (!isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(pointer, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) return null;
8502
+ 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) });
8503
+ }
8386
8504
  async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes) {
8387
8505
  if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
8388
8506
  if (isCompressibleSingleCommand(cmd)) return null;
@@ -8415,7 +8533,28 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
8415
8533
  return null;
8416
8534
  }
8417
8535
  await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8418
- return emitRewrite(body, "bash", { kind: "bash_compress:generic", originalBytes: compressed.originalBytes });
8536
+ return emitRewrite(body, "bash", { kind: "bash_compress:generic", originalBytes: deliveredOutputBytes(compressed.originalBytes) });
8537
+ }
8538
+ function maybeStripAnsiOnly(output) {
8539
+ if (!output.includes("\x1B")) return null;
8540
+ let cfg;
8541
+ try {
8542
+ cfg = loadConfig().bash_compress;
8543
+ } catch {
8544
+ return null;
8545
+ }
8546
+ if (!cfg.enabled || cfg.disabled_filters.includes("ansi")) return null;
8547
+ const stripped = stripAnsiEscapes(output);
8548
+ const originalBytes = Buffer.byteLength(output, "utf-8");
8549
+ if (!isRewriteWorthwhile({
8550
+ originalBytes,
8551
+ rewrittenBytes: Buffer.byteLength(stripped, "utf-8"),
8552
+ noticeBytes: 0,
8553
+ minNetSavingsBytes: resolveMinNetSavingsBytes()
8554
+ })) {
8555
+ return null;
8556
+ }
8557
+ return emitRewrite(stripped, "ansi escapes stripped", { kind: "bash_compress:ansi", originalBytes: deliveredOutputBytes(originalBytes) }, "counted-elsewhere");
8419
8558
  }
8420
8559
  function unwrapCompressCommand(executed) {
8421
8560
  const t = executed.trim();
@@ -9102,9 +9241,11 @@ async function postBashHandler(event) {
9102
9241
  }
9103
9242
  const isMonitoring = getMonitoringRecallHint(cmd) !== null;
9104
9243
  if (!isMonitoring && !isBuildCommand(cmd) && !isCurlGetCommand(cmd)) {
9244
+ const identical = await maybeCollapseIdenticalRead(cmd, output, exitCode, cwd, cacheMinBytes);
9245
+ if (identical !== null) return identical;
9105
9246
  const compound = await maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes);
9106
9247
  if (compound !== null) return compound;
9107
- return passOutput();
9248
+ return maybeStripAnsiOnly(output) ?? passOutput();
9108
9249
  }
9109
9250
  if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return passOutput();
9110
9251
  const cacheKey = isCurlGetCommand(cmd) ? extractCurlUrl(cmd) ?? cmd : stripOutputPipeline(cmd);
@@ -9121,6 +9262,8 @@ async function postBashHandler(event) {
9121
9262
  return contextOutput(delta + " \u2014 full output: bash-output " + id);
9122
9263
  }
9123
9264
  }
9265
+ const ansiOnly = maybeStripAnsiOnly(output);
9266
+ if (ansiOnly !== null) return ansiOnly;
9124
9267
  } catch {
9125
9268
  }
9126
9269
  return passOutput();
@@ -10007,7 +10150,7 @@ ${dataUrl}`, changed: true, savedBytes: saved, savedTokens };
10007
10150
  var TAB_CONTEXT_UNCHANGED_NOTICE = "(tabs unchanged since last check)";
10008
10151
  function dedupTabContext(text) {
10009
10152
  if (!TAB_CONTEXT_RE.test(text)) return { text, changed: false };
10010
- const isRepeat = getLastTabContext() === text;
10153
+ const isRepeat = lastTabContextMatches(text);
10011
10154
  setLastTabContext(text);
10012
10155
  if (!isRepeat) return { text, changed: false };
10013
10156
  const worthwhile = isRewriteWorthwhile({
@@ -10427,9 +10570,6 @@ function buildEvent(eventName, payload) {
10427
10570
  const tracestate = typeof rawTracestate === "string" && rawTracestate.trim() !== "" ? rawTracestate.trim() : void 0;
10428
10571
  return { eventName, toolName, toolInput, sessionId, agentId, traceparent, tracestate, raw: obj };
10429
10572
  }
10430
- function sessionStateKey(event) {
10431
- return event.agentId !== void 0 ? `${event.sessionId}:agent:${event.agentId}` : event.sessionId;
10432
- }
10433
10573
  function harnessForNormalization() {
10434
10574
  const detected = detectHarness();
10435
10575
  if (detected === "codex") return "codex";
@@ -10440,6 +10580,11 @@ function harnessForNormalization() {
10440
10580
  if (detected === "copilot_cli") return "copilot_cli";
10441
10581
  return "claude";
10442
10582
  }
10583
+ function safeSuggestions(output) {
10584
+ if (output.hookType === "deny") return { hookType: "deny", message: stripUnsafeSuggestions(output.message) };
10585
+ if (output.hookType === "context") return { hookType: "context", context: stripUnsafeSuggestions(output.context) };
10586
+ return output;
10587
+ }
10443
10588
  async function relayInProcess(eventName, rawPayload) {
10444
10589
  try {
10445
10590
  if (!isHookEventName(eventName)) {
@@ -10462,12 +10607,12 @@ async function relayInProcess(eventName, rawPayload) {
10462
10607
  loadSessionState(stateKey);
10463
10608
  } catch {
10464
10609
  }
10465
- const output = await runHook(event);
10610
+ const output = safeSuggestions(await runHook(event));
10466
10611
  try {
10467
10612
  saveSessionState(stateKey);
10468
10613
  } catch {
10469
10614
  }
10470
- return serializeOutput(output, event.eventName, harness);
10615
+ return serializeOutput(output, event.eventName, harness, event);
10471
10616
  } catch {
10472
10617
  return "{}";
10473
10618
  }