token-goat 2.8.2 → 2.8.4
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.
- package/README.md +26 -18
- package/dist/{token-goat-chunk-AM23GDIS.mjs → token-goat-chunk-4OTIB7SB.mjs} +579 -162
- package/dist/{token-goat-chunk-TF5NT3H5.mjs → token-goat-chunk-CZALRRGN.mjs} +192 -60
- package/dist/{token-goat-chunk-IVCTQPZD.mjs → token-goat-chunk-E76UNTVK.mjs} +407 -215
- package/dist/{token-goat-chunk-SE6V7LUZ.mjs → token-goat-chunk-HNODKNWO.mjs} +5 -5
- package/dist/{token-goat-chunk-5MLXFSRI.mjs → token-goat-chunk-IZRXU64B.mjs} +51 -4
- package/dist/{token-goat-chunk-JU7QESB3.mjs → token-goat-chunk-L6YP6FKQ.mjs} +5 -5
- package/dist/{token-goat-chunk-AL644JUV.mjs → token-goat-chunk-LZOAPGWR.mjs} +6 -5
- package/dist/{token-goat-chunk-BTZSUIYA.mjs → token-goat-chunk-NZFTWBGV.mjs} +2 -2
- package/dist/{token-goat-chunk-LILS6TIU.mjs → token-goat-chunk-RE7S7H26.mjs} +182 -65
- package/dist/{token-goat-chunk-2MWF3OGR.mjs → token-goat-chunk-U4UI5F3S.mjs} +1072 -218
- package/dist/{token-goat-chunk-KBVN4ELV.mjs → token-goat-chunk-ZQ3PUOP3.mjs} +379 -147
- package/dist/token-goat-hook.mjs +5 -5
- package/dist/token-goat.core.mjs +5 -5
- package/package.json +3 -1
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
summarizeOutputDelta,
|
|
24
24
|
summarizeResidentContext,
|
|
25
25
|
taskListPruneHint
|
|
26
|
-
} from "./token-goat-chunk-
|
|
26
|
+
} from "./token-goat-chunk-IZRXU64B.mjs";
|
|
27
27
|
import {
|
|
28
28
|
BODY_FIRST_TOOL_RESPONSE_KEYS,
|
|
29
29
|
OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
|
|
@@ -41,10 +41,10 @@ import {
|
|
|
41
41
|
contextOutput,
|
|
42
42
|
countSymbols,
|
|
43
43
|
denyOutput,
|
|
44
|
+
emitRewrite,
|
|
44
45
|
enqueueDirtyPathSafe,
|
|
45
46
|
ensureWorkerAlive,
|
|
46
47
|
estimateTokens,
|
|
47
|
-
estimateTokensFromLength,
|
|
48
48
|
extractCompactFromMarker,
|
|
49
49
|
extractToolResponseField,
|
|
50
50
|
extractToolResultText,
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
matchesDenyPattern,
|
|
89
89
|
meetsSavingsFloor,
|
|
90
90
|
passOutput,
|
|
91
|
+
probeImageMeta,
|
|
91
92
|
recordBashOutput,
|
|
92
93
|
recordBashRerun,
|
|
93
94
|
recordCliRead,
|
|
@@ -109,16 +110,19 @@ import {
|
|
|
109
110
|
saveSessionState,
|
|
110
111
|
scanForInjectionPatterns,
|
|
111
112
|
serializeOutput,
|
|
113
|
+
sessionOutputBodyBytes,
|
|
112
114
|
sessionSidecarPath,
|
|
113
115
|
setLastTabContext,
|
|
114
116
|
shrinkImage,
|
|
115
117
|
storeBlob,
|
|
116
118
|
storeOutput,
|
|
117
119
|
takePendingLargeFileHint,
|
|
120
|
+
visionTokens,
|
|
121
|
+
visionTokensSaved,
|
|
118
122
|
wasCliReadThisSession,
|
|
119
123
|
wasFileReadThisSession,
|
|
120
124
|
wasHintShown
|
|
121
|
-
} from "./token-goat-chunk-
|
|
125
|
+
} from "./token-goat-chunk-4OTIB7SB.mjs";
|
|
122
126
|
import {
|
|
123
127
|
canRunWrappedShell,
|
|
124
128
|
compressOutput,
|
|
@@ -129,8 +133,9 @@ import {
|
|
|
129
133
|
isRewriteWorthwhile,
|
|
130
134
|
resolveMinNetSavingsBytes,
|
|
131
135
|
shlexSplit
|
|
132
|
-
} from "./token-goat-chunk-
|
|
136
|
+
} from "./token-goat-chunk-CZALRRGN.mjs";
|
|
133
137
|
import {
|
|
138
|
+
PER_FILE_COUNTERFACTUAL_CEILING,
|
|
134
139
|
VERSION,
|
|
135
140
|
detectHarness,
|
|
136
141
|
detectLanguage,
|
|
@@ -147,9 +152,10 @@ import {
|
|
|
147
152
|
redactSecrets,
|
|
148
153
|
resolveIndexPath,
|
|
149
154
|
runGit,
|
|
155
|
+
savedTokensFromBytes,
|
|
150
156
|
shortFingerprint,
|
|
151
157
|
toKB
|
|
152
|
-
} from "./token-goat-chunk-
|
|
158
|
+
} from "./token-goat-chunk-E76UNTVK.mjs";
|
|
153
159
|
|
|
154
160
|
// src/hooks_grep.ts
|
|
155
161
|
function grepIntInput(toolInput, key) {
|
|
@@ -164,7 +170,7 @@ function grepIntInput(toolInput, key) {
|
|
|
164
170
|
function grepSignature(toolInput) {
|
|
165
171
|
const pattern = toolInput["pattern"];
|
|
166
172
|
if (typeof pattern !== "string" || pattern === "") return null;
|
|
167
|
-
const
|
|
173
|
+
const path3 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
|
|
168
174
|
const outputMode = typeof toolInput["output_mode"] === "string" ? toolInput["output_mode"] : "files_with_matches";
|
|
169
175
|
const glob = typeof toolInput["glob"] === "string" ? toolInput["glob"] : "";
|
|
170
176
|
const type = typeof toolInput["type"] === "string" ? toolInput["type"] : "";
|
|
@@ -179,7 +185,7 @@ function grepSignature(toolInput) {
|
|
|
179
185
|
const offset = grepIntInput(toolInput, "offset");
|
|
180
186
|
return JSON.stringify([
|
|
181
187
|
pattern,
|
|
182
|
-
|
|
188
|
+
path3,
|
|
183
189
|
outputMode,
|
|
184
190
|
glob,
|
|
185
191
|
type,
|
|
@@ -213,8 +219,9 @@ function foldGrepContentHandler(event) {
|
|
|
213
219
|
return passOutput();
|
|
214
220
|
}
|
|
215
221
|
if (toolInput["-n"] === false) return passOutput();
|
|
216
|
-
const
|
|
217
|
-
if (!
|
|
222
|
+
const rawText = extractToolResponseField(event.raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
|
|
223
|
+
if (!rawText) return passOutput();
|
|
224
|
+
const text = redactSecrets(rawText).text;
|
|
218
225
|
const rawLines = text.split(/\r\n|\r|\n/);
|
|
219
226
|
const parsed = [];
|
|
220
227
|
for (const line of rawLines) {
|
|
@@ -255,9 +262,7 @@ function foldGrepContentHandler(event) {
|
|
|
255
262
|
})) {
|
|
256
263
|
return passOutput();
|
|
257
264
|
}
|
|
258
|
-
|
|
259
|
-
recordStat("grep:fold", bytesDelta, Math.round(bytesDelta / 4));
|
|
260
|
-
return { hookType: "rewriteOutput", updatedOutput: rewritten };
|
|
265
|
+
return emitRewrite(rewritten, "grep", { kind: "grep:fold", originalBytes });
|
|
261
266
|
} catch {
|
|
262
267
|
return passOutput();
|
|
263
268
|
}
|
|
@@ -275,8 +280,8 @@ registerHook("post_tool_use", postGrepHandler, { toolName: "Grep" });
|
|
|
275
280
|
function globSignature(toolInput) {
|
|
276
281
|
const pattern = toolInput["pattern"];
|
|
277
282
|
if (typeof pattern !== "string" || pattern === "") return null;
|
|
278
|
-
const
|
|
279
|
-
return JSON.stringify([pattern,
|
|
283
|
+
const path3 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
|
|
284
|
+
return JSON.stringify([pattern, path3]);
|
|
280
285
|
}
|
|
281
286
|
var { post: postGlobHandler, pre: preGlobDedupHandler } = makeDedupHintHandlers({
|
|
282
287
|
toolName: "Glob",
|
|
@@ -424,6 +429,9 @@ function renderReadRow(entry) {
|
|
|
424
429
|
const edited = entry.wasEdited ? ", edited" : "";
|
|
425
430
|
return `- ${entry.path} (${kb}kb, ${entry.readCount} ${plural}${edited})`;
|
|
426
431
|
}
|
|
432
|
+
function renderSymbolReadRow(entry) {
|
|
433
|
+
return `- ${entry.path} (symbols: ${(entry.symbols_read ?? []).join(", ")})`;
|
|
434
|
+
}
|
|
427
435
|
function mergeManifestFiles(parent, siblingFiles) {
|
|
428
436
|
const byPath = /* @__PURE__ */ new Map();
|
|
429
437
|
for (const f of parent) byPath.set(foldPath(f.path), f);
|
|
@@ -451,6 +459,7 @@ function buildManifest(sessionId, cwd) {
|
|
|
451
459
|
const files = siblingFiles.length > 0 ? mergeManifestFiles(ownFiles, siblingFiles) : ownFiles;
|
|
452
460
|
const editedFiles = files.filter((f) => f.wasEdited);
|
|
453
461
|
const readFiles = files.filter((f) => f.readCount > 0 && !f.wasEdited);
|
|
462
|
+
const symbolOnlyFiles = files.filter((f) => f.readCount === 0 && !f.wasEdited && (f.symbols_read?.length ?? 0) > 0);
|
|
454
463
|
const webFetches = [...getSessionWebFetches().entries()];
|
|
455
464
|
const lines = [];
|
|
456
465
|
lines.push("## Session context");
|
|
@@ -463,6 +472,7 @@ function buildManifest(sessionId, cwd) {
|
|
|
463
472
|
editedFiles.map((entry) => `- ${entry.path}`),
|
|
464
473
|
MAX_ROWS
|
|
465
474
|
);
|
|
475
|
+
appendCappedSection(lines, "### Surgically read files (symbol/section reads, never read whole)", symbolOnlyFiles.map(renderSymbolReadRow), MAX_ROWS);
|
|
466
476
|
appendCappedSection(
|
|
467
477
|
lines,
|
|
468
478
|
"### Web URLs fetched",
|
|
@@ -607,7 +617,7 @@ function postCompactHandler(event) {
|
|
|
607
617
|
0,
|
|
608
618
|
0,
|
|
609
619
|
void 0,
|
|
610
|
-
`trigger=${trigger} bytes=${bytes} est_tokens=${
|
|
620
|
+
`trigger=${trigger} bytes=${bytes} est_tokens=${savedTokensFromBytes(bytes)} manifest_paths=${survived}/${sample.length}`
|
|
611
621
|
);
|
|
612
622
|
return passOutput();
|
|
613
623
|
}
|
|
@@ -620,7 +630,7 @@ import crypto from "node:crypto";
|
|
|
620
630
|
var TRACKED_SKILL = "token-goat";
|
|
621
631
|
var MAX_COMMANDS_SHOWN = 8;
|
|
622
632
|
async function currentCommandNames() {
|
|
623
|
-
const { buildProgram } = await import("./token-goat-chunk-
|
|
633
|
+
const { buildProgram } = await import("./token-goat-chunk-HNODKNWO.mjs");
|
|
624
634
|
return flattenCommandNames(buildCommandManifest(buildProgram()));
|
|
625
635
|
}
|
|
626
636
|
async function recordSkillVersionSnapshot(sessionId, skillName) {
|
|
@@ -1464,11 +1474,11 @@ function mapInner(r, f) {
|
|
|
1464
1474
|
function mapOuter(r, f) {
|
|
1465
1475
|
return r.matched ? f(r) : r;
|
|
1466
1476
|
}
|
|
1467
|
-
function ab(pa, pb,
|
|
1468
|
-
return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) =>
|
|
1477
|
+
function ab(pa, pb, join2) {
|
|
1478
|
+
return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join2(ma.value, vb, data, i, j)));
|
|
1469
1479
|
}
|
|
1470
|
-
function abc(pa, pb, pc,
|
|
1471
|
-
return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) =>
|
|
1480
|
+
function abc(pa, pb, pc, join2) {
|
|
1481
|
+
return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join2(ma.value, mb.value, vc, data, i, j))));
|
|
1472
1482
|
}
|
|
1473
1483
|
function ahead(p) {
|
|
1474
1484
|
return (data, i) => mapOuter(p(data, i), (m1) => ({
|
|
@@ -5385,8 +5395,8 @@ function trimCharacterEnd(str, char) {
|
|
|
5385
5395
|
function unicodeEscape(str) {
|
|
5386
5396
|
return str.replace(/[\s\S]/g, (c) => "\\u" + c.charCodeAt().toString(16).padStart(4, "0"));
|
|
5387
5397
|
}
|
|
5388
|
-
function get(obj,
|
|
5389
|
-
for (const key of
|
|
5398
|
+
function get(obj, path3) {
|
|
5399
|
+
for (const key of path3) {
|
|
5390
5400
|
if (!obj) {
|
|
5391
5401
|
return void 0;
|
|
5392
5402
|
}
|
|
@@ -6587,8 +6597,8 @@ function withBrackets(str, brackets) {
|
|
|
6587
6597
|
const rbr = typeof brackets[1] === "string" ? brackets[1] : "]";
|
|
6588
6598
|
return lbr + str + rbr;
|
|
6589
6599
|
}
|
|
6590
|
-
function pathRewrite(
|
|
6591
|
-
const modifiedPath = typeof rewriter === "function" ? rewriter(
|
|
6600
|
+
function pathRewrite(path3, rewriter, baseUrl, metadata, elem) {
|
|
6601
|
+
const modifiedPath = typeof rewriter === "function" ? rewriter(path3, metadata, elem) : path3;
|
|
6592
6602
|
return modifiedPath[0] === "/" && baseUrl ? trimCharacterEnd(baseUrl, "/") + modifiedPath : modifiedPath;
|
|
6593
6603
|
}
|
|
6594
6604
|
function formatImage(elem, walk, builder, formatOptions) {
|
|
@@ -6912,9 +6922,9 @@ function handleDeprecatedOptions(options) {
|
|
|
6912
6922
|
options.selectors.push(...tagDefinitions);
|
|
6913
6923
|
options.selectors = mergeDuplicatesPreferLast(options.selectors, ((s) => s.selector));
|
|
6914
6924
|
}
|
|
6915
|
-
function set(obj,
|
|
6916
|
-
const valueKey =
|
|
6917
|
-
for (const key of
|
|
6925
|
+
function set(obj, path3, value) {
|
|
6926
|
+
const valueKey = path3.pop();
|
|
6927
|
+
for (const key of path3) {
|
|
6918
6928
|
let nested = obj[key];
|
|
6919
6929
|
if (!nested) {
|
|
6920
6930
|
nested = {};
|
|
@@ -7005,7 +7015,7 @@ function preFetchHandler(event) {
|
|
|
7005
7015
|
if (cached !== null) {
|
|
7006
7016
|
const cachedBytes = Buffer.byteLength(cached, "utf-8");
|
|
7007
7017
|
if (cachedBytes >= loadConfig().hints.web_dedup_min_bytes) {
|
|
7008
|
-
recordStat("webfetch:recall", cachedBytes,
|
|
7018
|
+
recordStat("webfetch:recall", cachedBytes, savedTokensFromBytes(cachedBytes));
|
|
7009
7019
|
return denyOutput(
|
|
7010
7020
|
"Already fetched this URL with this prompt; the response is cached. Use `token-goat web-output " + cacheId + "` to recall it (append `--grep PATTERN` to filter or `--section Heading` for a markdown section) instead of re-fetching."
|
|
7011
7021
|
);
|
|
@@ -7030,15 +7040,22 @@ function postFetchHandler(event) {
|
|
|
7030
7040
|
if (injectionMatches.length > 0) {
|
|
7031
7041
|
recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
|
|
7032
7042
|
}
|
|
7043
|
+
const bodyRedacted = body ? redactSecrets(body) : { text: body, count: 0 };
|
|
7033
7044
|
if (!event.sessionId) {
|
|
7034
7045
|
if (injectionMatches.length > 0) {
|
|
7035
|
-
return
|
|
7046
|
+
return emitRewrite(fenceUntrustedContent(bodyRedacted.text, injectionMatches), "fetch");
|
|
7047
|
+
}
|
|
7048
|
+
if (bodyRedacted.count > 0) {
|
|
7049
|
+
return emitRewrite(bodyRedacted.text, "fetch");
|
|
7036
7050
|
}
|
|
7037
7051
|
return passOutput();
|
|
7038
7052
|
}
|
|
7039
7053
|
if (!body || body.length < 1024) {
|
|
7040
7054
|
if (injectionMatches.length > 0) {
|
|
7041
|
-
return
|
|
7055
|
+
return emitRewrite(fenceUntrustedContent(bodyRedacted.text, injectionMatches), "fetch");
|
|
7056
|
+
}
|
|
7057
|
+
if (bodyRedacted.count > 0) {
|
|
7058
|
+
return emitRewrite(bodyRedacted.text, "fetch");
|
|
7042
7059
|
}
|
|
7043
7060
|
return passOutput();
|
|
7044
7061
|
}
|
|
@@ -7054,8 +7071,9 @@ function postFetchHandler(event) {
|
|
|
7054
7071
|
}
|
|
7055
7072
|
const cacheId = storeWebOutput(url, storedBody, `${url}\0${prompt}`, storedBody !== body ? body : void 0);
|
|
7056
7073
|
recordWebFetch(url, prompt, cacheId);
|
|
7074
|
+
const storedRedacted = storedBody === body ? bodyRedacted : redactSecrets(storedBody);
|
|
7057
7075
|
if (injectionMatches.length > 0) {
|
|
7058
|
-
return
|
|
7076
|
+
return emitRewrite(fenceUntrustedContent(storedRedacted.text, injectionMatches), "fetch");
|
|
7059
7077
|
}
|
|
7060
7078
|
if (storedBody !== body) {
|
|
7061
7079
|
const noticeFor = (id) => `
|
|
@@ -7070,11 +7088,12 @@ function postFetchHandler(event) {
|
|
|
7070
7088
|
noticeBytes,
|
|
7071
7089
|
minNetSavingsBytes: resolveMinNetSavingsBytes()
|
|
7072
7090
|
})) {
|
|
7073
|
-
|
|
7074
|
-
recordStat("webfetch:compress", bytesDelta, Math.round(bytesDelta / 4));
|
|
7075
|
-
return { hookType: "rewriteOutput", updatedOutput: storedBody + notice };
|
|
7091
|
+
return emitRewrite(storedRedacted.text + notice, "fetch", { kind: "webfetch:compress", originalBytes });
|
|
7076
7092
|
}
|
|
7077
7093
|
}
|
|
7094
|
+
if (storedRedacted.count > 0) {
|
|
7095
|
+
return emitRewrite(storedRedacted.text, "fetch");
|
|
7096
|
+
}
|
|
7078
7097
|
return passOutput();
|
|
7079
7098
|
} catch {
|
|
7080
7099
|
return passOutput();
|
|
@@ -7127,7 +7146,9 @@ async function preSkillHandler(event) {
|
|
|
7127
7146
|
return passOutput();
|
|
7128
7147
|
}
|
|
7129
7148
|
if (await hasSessionOutput(event.sessionId, skillName)) {
|
|
7130
|
-
|
|
7149
|
+
const cachedBytes = await sessionOutputBodyBytes(event.sessionId, skillName);
|
|
7150
|
+
const denyCredit = cachedBytes !== null ? Math.min(cachedBytes, PER_FILE_COUNTERFACTUAL_CEILING) : 0;
|
|
7151
|
+
recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit));
|
|
7131
7152
|
return denyOutput(
|
|
7132
7153
|
"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."
|
|
7133
7154
|
);
|
|
@@ -7142,7 +7163,7 @@ async function preSkillHandler(event) {
|
|
|
7142
7163
|
const compactBytes = Buffer.byteLength(compact, "utf-8");
|
|
7143
7164
|
if (compactBytes * 2 <= bodyBytes && compactBytes <= COMPACT_INLINE_MAX_BYTES) {
|
|
7144
7165
|
const savedBytes = bodyBytes - compactBytes;
|
|
7145
|
-
recordStat("skill_compact_inlined", savedBytes,
|
|
7166
|
+
recordStat("skill_compact_inlined", savedBytes, savedTokensFromBytes(savedBytes));
|
|
7146
7167
|
return denyOutput(
|
|
7147
7168
|
"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
|
|
7148
7169
|
);
|
|
@@ -7466,16 +7487,16 @@ function isTempPath(fp) {
|
|
|
7466
7487
|
return /^\/tmp\//i.test(norm) || /\/var\/folders\//i.test(norm) || /AppData\/Local\/Temp\//i.test(norm) || norm.startsWith("/c/Users/") && norm.includes("/AppData/Local/Temp/") || isUnderSystemTemp(fp);
|
|
7467
7488
|
}
|
|
7468
7489
|
function isOrchestratorStateFile(filePath) {
|
|
7469
|
-
const
|
|
7470
|
-
return /^\.improve-state-/.test(
|
|
7490
|
+
const basename3 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
|
|
7491
|
+
return /^\.improve-state-/.test(basename3);
|
|
7471
7492
|
}
|
|
7472
7493
|
function extractCatSourceFile(cmd) {
|
|
7473
7494
|
const m = /^cat\s+(\S+\.(?: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))\s*$/.exec(cmd);
|
|
7474
7495
|
return m?.[1] ?? null;
|
|
7475
7496
|
}
|
|
7476
7497
|
function classifyFileExtensions(filePath) {
|
|
7477
|
-
const
|
|
7478
|
-
const isEnvFile = /^\.env(\.\w+)?$/i.test(
|
|
7498
|
+
const basename3 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
|
|
7499
|
+
const isEnvFile = /^\.env(\.\w+)?$/i.test(basename3);
|
|
7479
7500
|
const hasKnownExt = /\.(?: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|conf|cfg|ini|properties|sql|ps1|psm1|env)$/i.test(filePath);
|
|
7480
7501
|
if (!hasKnownExt && !isEnvFile) return null;
|
|
7481
7502
|
const isSql = /\.sql$/i.test(filePath);
|
|
@@ -7497,12 +7518,14 @@ function classifyCatPath(filePath, cmd0) {
|
|
|
7497
7518
|
return { filePath, ...flags, cmd0 };
|
|
7498
7519
|
}
|
|
7499
7520
|
function extractCatFile(cmd) {
|
|
7500
|
-
const m = /^(cat|bat|type|Get-Content|gc)(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+-[a-zA-Z]
|
|
7521
|
+
const m = /^(cat|bat|type|Get-Content|gc)(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+-[a-zA-Z].*?)?(?:\s+2>(&1|\/dev\/null))?\s*$/i.exec(cmd);
|
|
7501
7522
|
if (!m) return null;
|
|
7502
7523
|
const cmd0 = m[1];
|
|
7503
7524
|
const filePath = m[2] ?? m[3] ?? m[4];
|
|
7504
7525
|
if (filePath === void 0) return null;
|
|
7505
|
-
|
|
7526
|
+
const r = classifyCatPath(filePath, cmd0);
|
|
7527
|
+
if (r === null) return null;
|
|
7528
|
+
return { ...r, advisoryOnly: m[5] === "/dev/null" };
|
|
7506
7529
|
}
|
|
7507
7530
|
function extractCatFilesMulti(cmd) {
|
|
7508
7531
|
if (/[|<>;&`]/.test(cmd) || cmd.includes("$(")) return null;
|
|
@@ -7750,11 +7773,12 @@ function extractPythonFileRead(cmd) {
|
|
|
7750
7773
|
return null;
|
|
7751
7774
|
}
|
|
7752
7775
|
function extractHeadFile(cmd) {
|
|
7753
|
-
const
|
|
7754
|
-
|
|
7755
|
-
|
|
7776
|
+
const direct = /^head(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
|
|
7777
|
+
const piped = direct === null ? /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+2>(?:&1|\/dev\/null))?\s*\|\s*head(?:\s+-n\s+(\d+)|\s+-(\d+))?\s*$/.exec(cmd) : null;
|
|
7778
|
+
if (direct === null && piped === null) return null;
|
|
7779
|
+
const n = parseInt((direct !== null ? direct[1] ?? direct[2] : piped[4] ?? piped[5]) ?? "0", 10);
|
|
7756
7780
|
if (n <= 10) return null;
|
|
7757
|
-
const filePath =
|
|
7781
|
+
const filePath = direct !== null ? direct[3] ?? direct[4] ?? direct[5] : piped[1] ?? piped[2] ?? piped[3];
|
|
7758
7782
|
if (filePath === void 0) return null;
|
|
7759
7783
|
if (isTempPath(filePath)) return null;
|
|
7760
7784
|
if (!/\.(?:ts|tsx|js|jsx|py|go|java|rs|rb|cs|md|mdx|rst|txt|json|yaml|yml|toml|sql|sh)$/i.test(filePath)) return null;
|
|
@@ -7762,12 +7786,12 @@ function extractHeadFile(cmd) {
|
|
|
7762
7786
|
return { filePath, isDoc, isConfig, isSql, n };
|
|
7763
7787
|
}
|
|
7764
7788
|
function extractSedRange(cmd) {
|
|
7765
|
-
const m = /^sed\s+-n\s+['"](?:\d+,\d+p)(?:;\d+,\d+p)*['"]\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2
|
|
7789
|
+
const m = /^sed\s+-n\s+(?:['"]((?:\d+,\d+p)(?:;\d+,\d+p)*)['"]|(\d+,\d+p))\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>(?:\/dev\/null|&1))?\s*$/.exec(cmd);
|
|
7766
7790
|
if (!m) return null;
|
|
7767
|
-
const
|
|
7768
|
-
if (
|
|
7791
|
+
const addressList = m[1] ?? m[2];
|
|
7792
|
+
if (addressList === void 0) return null;
|
|
7769
7793
|
const ranges = [];
|
|
7770
|
-
for (const clause of
|
|
7794
|
+
for (const clause of addressList.split(";")) {
|
|
7771
7795
|
const cm = /^(\d+),(\d+)p$/.exec(clause ?? "");
|
|
7772
7796
|
if (!cm) return null;
|
|
7773
7797
|
const start = parseInt(cm[1], 10);
|
|
@@ -7776,19 +7800,21 @@ function extractSedRange(cmd) {
|
|
|
7776
7800
|
ranges.push([start, end]);
|
|
7777
7801
|
}
|
|
7778
7802
|
if (ranges.length === 0) return null;
|
|
7779
|
-
const filePath = m[
|
|
7803
|
+
const filePath = m[3] ?? m[4] ?? m[5];
|
|
7780
7804
|
if (filePath === void 0) return null;
|
|
7781
7805
|
if (isTempPath(filePath)) return null;
|
|
7782
7806
|
return { filePath, ranges };
|
|
7783
7807
|
}
|
|
7784
7808
|
function extractAwkRange(cmd) {
|
|
7785
|
-
const m = /^awk\s+(?:'([^']+)'|"([^"]+)")\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2
|
|
7809
|
+
const m = /^awk\s+(?:'([^']+)'|"([^"]+)")\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>(?:\/dev\/null|&1))?\s*$/.exec(cmd);
|
|
7786
7810
|
if (!m) return null;
|
|
7787
7811
|
const program = m[1] ?? m[2];
|
|
7788
7812
|
if (program === void 0) return null;
|
|
7789
|
-
const cmp = /^\s*NR\s*>=\s*(\d+)\s*&&\s*NR\s*<=\s*(\d+)\s*$/.exec(program);
|
|
7790
|
-
const rng = /^\s*NR\s*==\s*(\d+)\s*,\s*NR\s*==\s*(\d+)\s*$/.exec(program);
|
|
7813
|
+
const cmp = /^\s*NR\s*>=\s*(\d+)\s*&&\s*NR\s*<=\s*(\d+)\s*(\{.*\})?\s*$/.exec(program);
|
|
7814
|
+
const rng = /^\s*NR\s*==\s*(\d+)\s*,\s*NR\s*==\s*(\d+)\s*(\{.*\})?\s*$/.exec(program);
|
|
7791
7815
|
const hit = cmp ?? rng;
|
|
7816
|
+
const action = hit?.[3];
|
|
7817
|
+
if (action !== void 0 && !/^\{\s*(?:print(?:\s+NR\s*"[^"%]*"\s*\$0|\s+\$0)?|printf\s*"%d[^%"]*%s\\n"\s*,\s*NR\s*,\s*\$0)\s*\}$/.test(action)) return null;
|
|
7792
7818
|
if (!hit) return null;
|
|
7793
7819
|
const start = parseInt(hit[1], 10);
|
|
7794
7820
|
const end = parseInt(hit[2], 10);
|
|
@@ -7798,13 +7824,47 @@ function extractAwkRange(cmd) {
|
|
|
7798
7824
|
if (isTempPath(filePath)) return null;
|
|
7799
7825
|
return { filePath, ranges: [[start, end]] };
|
|
7800
7826
|
}
|
|
7827
|
+
function normalizeCatSedPipe(cmd) {
|
|
7828
|
+
const m = /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+2>(?:&1|\/dev\/null))?\s*\|\s*sed\s+-n\s+('[^']+'|"[^"]+"|\d+,\d+p)\s*$/.exec(cmd);
|
|
7829
|
+
if (!m) return null;
|
|
7830
|
+
const filePath = m[1] ?? m[2] ?? m[3];
|
|
7831
|
+
if (filePath === void 0 || filePath.includes('"')) return null;
|
|
7832
|
+
return "sed -n " + m[4] + ' "' + filePath + '"';
|
|
7833
|
+
}
|
|
7801
7834
|
function extractLineRangeRead(cmd) {
|
|
7835
|
+
const catSed = normalizeCatSedPipe(cmd);
|
|
7836
|
+
if (catSed !== null) cmd = catSed;
|
|
7802
7837
|
const sed = extractSedRange(cmd);
|
|
7803
7838
|
if (sed !== null) return { ...sed, tool: "sed" };
|
|
7804
7839
|
const awk = extractAwkRange(cmd);
|
|
7805
7840
|
if (awk !== null) return { ...awk, tool: "awk" };
|
|
7806
7841
|
return null;
|
|
7807
7842
|
}
|
|
7843
|
+
var FORMATTING_STAGE_RE = /^(?:fold|cut|cat|nl|head|tail)(?:\s+(?:-\S+|[\d,+-]+))*\s*$/;
|
|
7844
|
+
function extractLineRangeReadsCompound(cmd) {
|
|
7845
|
+
const cleaned = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
|
|
7846
|
+
const segments = splitShellSegments(cleaned);
|
|
7847
|
+
if (segments.length < 2) return null;
|
|
7848
|
+
const reads = [];
|
|
7849
|
+
for (const seg of segments) {
|
|
7850
|
+
const r = extractLineRangeRead(seg);
|
|
7851
|
+
if (r !== null) {
|
|
7852
|
+
reads.push(r);
|
|
7853
|
+
continue;
|
|
7854
|
+
}
|
|
7855
|
+
if (/^echo\b[^<>]*$/.test(seg)) continue;
|
|
7856
|
+
if (FORMATTING_STAGE_RE.test(seg)) continue;
|
|
7857
|
+
return null;
|
|
7858
|
+
}
|
|
7859
|
+
if (reads.length === 0) return null;
|
|
7860
|
+
const merged = /* @__PURE__ */ new Map();
|
|
7861
|
+
for (const r of reads) {
|
|
7862
|
+
const prev = merged.get(r.filePath);
|
|
7863
|
+
if (prev !== void 0) prev.ranges.push(...r.ranges);
|
|
7864
|
+
else merged.set(r.filePath, { filePath: r.filePath, ranges: [...r.ranges], tool: r.tool });
|
|
7865
|
+
}
|
|
7866
|
+
return [...merged.values()];
|
|
7867
|
+
}
|
|
7808
7868
|
var SYMBOL_BEARING_LANGUAGES = /* @__PURE__ */ new Set([
|
|
7809
7869
|
"python",
|
|
7810
7870
|
"typescript",
|
|
@@ -7904,11 +7964,12 @@ function extractTailFile(cmd) {
|
|
|
7904
7964
|
if (/-f\b/.test(cmd)) return null;
|
|
7905
7965
|
if (/-c\b/.test(cmd)) return null;
|
|
7906
7966
|
if (/-n\s*\+/.test(cmd)) return null;
|
|
7907
|
-
const
|
|
7908
|
-
|
|
7909
|
-
|
|
7967
|
+
const direct = /^tail(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
|
|
7968
|
+
const piped = direct === null ? /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+2>(?:&1|\/dev\/null))?\s*\|\s*tail(?:\s+-n\s+(\d+)|\s+-(\d+))?\s*$/.exec(cmd) : null;
|
|
7969
|
+
if (direct === null && piped === null) return null;
|
|
7970
|
+
const n = parseInt((direct !== null ? direct[1] ?? direct[2] : piped[4] ?? piped[5]) ?? "0", 10);
|
|
7910
7971
|
if (n <= 10) return null;
|
|
7911
|
-
const filePath =
|
|
7972
|
+
const filePath = direct !== null ? direct[3] ?? direct[4] ?? direct[5] : piped[1] ?? piped[2] ?? piped[3];
|
|
7912
7973
|
if (!filePath) return null;
|
|
7913
7974
|
if (isTempPath(filePath)) return null;
|
|
7914
7975
|
if (!/\.(?:ts|tsx|js|jsx|py|go|java|rs|rb|cs|md|mdx|rst|txt|json|yaml|yml|toml|sql|sh)$/i.test(filePath)) return null;
|
|
@@ -8362,10 +8423,19 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
|
|
|
8362
8423
|
});
|
|
8363
8424
|
const minNet = resolveMinNetSavingsBytes();
|
|
8364
8425
|
if (!compressed.worthApplying(minNet)) return null;
|
|
8365
|
-
const id = await
|
|
8366
|
-
recordStat("bash_compress:generic", compressed.bytesSaved, compressed.tokensSaved);
|
|
8426
|
+
const id = await commandHash(cmd, cwd);
|
|
8367
8427
|
const body = compressed.withMarker(minNet) + "\n[token-goat] full output: bash-output " + id + " --full";
|
|
8368
|
-
|
|
8428
|
+
const emittedBytes = Buffer.byteLength(body, "utf-8");
|
|
8429
|
+
if (!isRewriteWorthwhile({
|
|
8430
|
+
originalBytes: compressed.originalBytes,
|
|
8431
|
+
rewrittenBytes: emittedBytes,
|
|
8432
|
+
noticeBytes: 0,
|
|
8433
|
+
minNetSavingsBytes: minNet
|
|
8434
|
+
})) {
|
|
8435
|
+
return null;
|
|
8436
|
+
}
|
|
8437
|
+
await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
|
|
8438
|
+
return emitRewrite(body, "bash", { kind: "bash_compress:generic", originalBytes: compressed.originalBytes });
|
|
8369
8439
|
}
|
|
8370
8440
|
function unwrapCompressCommand(executed) {
|
|
8371
8441
|
const t = executed.trim();
|
|
@@ -8591,25 +8661,28 @@ function preBashHandlerInner(event) {
|
|
|
8591
8661
|
"Use `token-goat outline <file>` to see symbol names and line counts without loading files."
|
|
8592
8662
|
);
|
|
8593
8663
|
}
|
|
8594
|
-
const
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
|
|
8664
|
+
const singleLineRangeRead = extractLineRangeRead(cmd);
|
|
8665
|
+
const sedReads = singleLineRangeRead !== null ? [singleLineRangeRead] : extractLineRangeReadsCompound(cmd);
|
|
8666
|
+
if (sedReads !== null) {
|
|
8598
8667
|
recordStat("session_hint", 0, 0);
|
|
8599
|
-
const
|
|
8600
|
-
const
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
const
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8668
|
+
const hints = [];
|
|
8669
|
+
for (const { filePath, ranges, tool } of sedReads) {
|
|
8670
|
+
const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
|
|
8671
|
+
const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
|
|
8672
|
+
const overlapHints = [];
|
|
8673
|
+
const freshRanges = [];
|
|
8674
|
+
for (const [start, end] of ranges) {
|
|
8675
|
+
const priorOverlap = findRangeOverlap(getFileLineRanges(sedDedupKey), start, end);
|
|
8676
|
+
recordFileLineRange(sedDedupKey, start, end);
|
|
8677
|
+
if (priorOverlap !== null) {
|
|
8678
|
+
overlapHints.push(sedOverlapHint(hintPath, priorOverlap, start, end));
|
|
8679
|
+
} else {
|
|
8680
|
+
freshRanges.push([start, end]);
|
|
8681
|
+
}
|
|
8609
8682
|
}
|
|
8683
|
+
hints.push(...overlapHints);
|
|
8684
|
+
if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
|
|
8610
8685
|
}
|
|
8611
|
-
const hints = [...overlapHints];
|
|
8612
|
-
if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
|
|
8613
8686
|
return contextOutput(hints.join(" "));
|
|
8614
8687
|
}
|
|
8615
8688
|
const catJsonPipe = extractCatJsonPipe(cmd);
|
|
@@ -8623,7 +8696,7 @@ function preBashHandlerInner(event) {
|
|
|
8623
8696
|
}
|
|
8624
8697
|
const catResult = extractCatFile(cmd);
|
|
8625
8698
|
if (catResult !== null) {
|
|
8626
|
-
const { filePath, isDoc, isEnv, isConfig, isSql, cmd0 } = catResult;
|
|
8699
|
+
const { filePath, isDoc, isEnv, isConfig, isSql, cmd0, advisoryOnly } = catResult;
|
|
8627
8700
|
const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
|
|
8628
8701
|
recordStat("session_hint", 0, 0);
|
|
8629
8702
|
if (isSql) {
|
|
@@ -8632,7 +8705,7 @@ function preBashHandlerInner(event) {
|
|
|
8632
8705
|
);
|
|
8633
8706
|
}
|
|
8634
8707
|
const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc);
|
|
8635
|
-
return cdStripped ? contextOutput("`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
|
|
8708
|
+
return cdStripped || advisoryOnly ? contextOutput("`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
|
|
8636
8709
|
}
|
|
8637
8710
|
const catMulti = extractCatFilesMulti(cmd);
|
|
8638
8711
|
if (catMulti !== null) {
|
|
@@ -8771,13 +8844,13 @@ function preBashHandlerInner(event) {
|
|
|
8771
8844
|
const monBytes = monEntry.sizeBytes;
|
|
8772
8845
|
const catFile = extractCatSourceFile(cmd);
|
|
8773
8846
|
if (catFile !== null) {
|
|
8774
|
-
recordStat("bash_compress:recall", monBytes,
|
|
8847
|
+
recordStat("bash_compress:recall", monBytes, savedTokensFromBytes(monBytes));
|
|
8775
8848
|
return contextOutput(
|
|
8776
8849
|
"Prior output from `" + cmd + "`" + pipelineDivergenceNote(cmd, monEntry.command) + " is cached. Use `token-goat bash-output " + monOutputId + "` to recall the full file, or `token-goat read '" + catFile + "::SymbolName'` to extract only the symbol you need."
|
|
8777
8850
|
);
|
|
8778
8851
|
}
|
|
8779
8852
|
const cmdSummary = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
8780
|
-
recordStat("bash_compress:recall", monBytes,
|
|
8853
|
+
recordStat("bash_compress:recall", monBytes, savedTokensFromBytes(monBytes));
|
|
8781
8854
|
return contextOutput(
|
|
8782
8855
|
"Prior output from `" + cmdSummary + "`" + pipelineDivergenceNote(cmd, monEntry.command) + " is cached.\nUse `token-goat bash-output " + monOutputId + " " + monitoringHint + "` to re-inspect without re-running."
|
|
8783
8856
|
);
|
|
@@ -8804,7 +8877,7 @@ function preBashHandlerInner(event) {
|
|
|
8804
8877
|
const curlEntry = curlEntryRaw !== null && !isBashEntryStale(curlEntryRaw, cmd, preHookCwd) ? curlEntryRaw : null;
|
|
8805
8878
|
if (curlOutputId !== null && curlEntry !== null && curlEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(curlEntry.sizeBytes)) {
|
|
8806
8879
|
const curlBytes = curlEntry.sizeBytes;
|
|
8807
|
-
recordStat("bash_compress:recall", curlBytes,
|
|
8880
|
+
recordStat("bash_compress:recall", curlBytes, savedTokensFromBytes(curlBytes));
|
|
8808
8881
|
const curlPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
8809
8882
|
return contextOutput(
|
|
8810
8883
|
"curl response cached (`" + curlPreview + "`)." + pipelineDivergenceNote(cmd, curlEntry.command) + " Use `token-goat bash-output " + curlOutputId + "` to recall it. Append `--grep PATTERN` to filter or `--section HeadingName` for a markdown section."
|
|
@@ -8818,7 +8891,7 @@ function preBashHandlerInner(event) {
|
|
|
8818
8891
|
const ghEntry = ghEntryRaw !== null && !isBashEntryStale(ghEntryRaw, cmd, preHookCwd) ? ghEntryRaw : null;
|
|
8819
8892
|
if (ghOutputId !== null && ghEntry !== null && ghEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(ghEntry.sizeBytes)) {
|
|
8820
8893
|
const ghBytes = ghEntry.sizeBytes;
|
|
8821
|
-
recordStat("bash_compress:recall", ghBytes,
|
|
8894
|
+
recordStat("bash_compress:recall", ghBytes, savedTokensFromBytes(ghBytes));
|
|
8822
8895
|
const ghPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
8823
8896
|
return contextOutput(
|
|
8824
8897
|
"gh api response cached (`" + ghPreview + "`)." + pipelineDivergenceNote(cmd, ghEntry.command) + " Use `token-goat bash-output " + ghOutputId + "` to recall it. Append `--jq '.field'` on the original call, or `--grep PATTERN` / `--max-matches N` here, to narrow it."
|
|
@@ -8832,7 +8905,7 @@ function preBashHandlerInner(event) {
|
|
|
8832
8905
|
const gitScopedEntry = gitScopedEntryRaw !== null && !isBashEntryStale(gitScopedEntryRaw, cmd, preHookCwd) ? gitScopedEntryRaw : null;
|
|
8833
8906
|
if (gitScopedOutputId !== null && gitScopedEntry !== null && gitScopedEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(gitScopedEntry.sizeBytes)) {
|
|
8834
8907
|
const gitScopedBytes = gitScopedEntry.sizeBytes;
|
|
8835
|
-
recordStat("bash_compress:recall", gitScopedBytes,
|
|
8908
|
+
recordStat("bash_compress:recall", gitScopedBytes, savedTokensFromBytes(gitScopedBytes));
|
|
8836
8909
|
const gitScopedPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
8837
8910
|
return contextOutput(
|
|
8838
8911
|
"Output from `" + gitScopedPreview + "`" + pipelineDivergenceNote(cmd, gitScopedEntry.command) + " is cached and unchanged (no edits to that path or HEAD since). Use `token-goat bash-output " + gitScopedOutputId + "` to recall it instead of re-running."
|
|
@@ -8874,7 +8947,7 @@ function preBashHandlerInner(event) {
|
|
|
8874
8947
|
const entry = entryRaw !== null && !isBashEntryStale(entryRaw, cmd, preHookCwd) ? entryRaw : null;
|
|
8875
8948
|
if (outputId !== null && entry !== null && entry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(entry.sizeBytes)) {
|
|
8876
8949
|
const bytes = entry.sizeBytes;
|
|
8877
|
-
recordStat("bash_compress:recall", bytes,
|
|
8950
|
+
recordStat("bash_compress:recall", bytes, savedTokensFromBytes(bytes));
|
|
8878
8951
|
return contextOutput(buildRecallHint(cmd, outputId));
|
|
8879
8952
|
}
|
|
8880
8953
|
return maybeCompressRewrite(event, rawCmd, cmd) ?? passOutput();
|
|
@@ -9121,10 +9194,7 @@ function postBashOutputHandler(event) {
|
|
|
9121
9194
|
})) {
|
|
9122
9195
|
return passOutput();
|
|
9123
9196
|
}
|
|
9124
|
-
return {
|
|
9125
|
-
hookType: "rewriteOutput",
|
|
9126
|
-
updatedOutput: unchangedNotice
|
|
9127
|
-
};
|
|
9197
|
+
return emitRewrite(unchangedNotice, "bashoutput", { kind: "bashoutput:unchanged", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
|
|
9128
9198
|
}
|
|
9129
9199
|
if (!output.startsWith(prior.output)) {
|
|
9130
9200
|
storePollSnapshot(event.sessionId, bashId, output);
|
|
@@ -9143,10 +9213,7 @@ function postBashOutputHandler(event) {
|
|
|
9143
9213
|
})) {
|
|
9144
9214
|
return passOutput();
|
|
9145
9215
|
}
|
|
9146
|
-
return {
|
|
9147
|
-
hookType: "rewriteOutput",
|
|
9148
|
-
updatedOutput: `${deltaNotice}${delta}`
|
|
9149
|
-
};
|
|
9216
|
+
return emitRewrite(`${deltaNotice}${delta}`, "bashoutput", { kind: "bashoutput:delta", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
|
|
9150
9217
|
} catch {
|
|
9151
9218
|
return passOutput();
|
|
9152
9219
|
}
|
|
@@ -9176,12 +9243,23 @@ function getTaskId(toolInput) {
|
|
|
9176
9243
|
const value = toolInput["task_id"];
|
|
9177
9244
|
return typeof value === "string" && value !== "" ? value : void 0;
|
|
9178
9245
|
}
|
|
9246
|
+
function extractTaskOutputText(raw) {
|
|
9247
|
+
const tr = raw["tool_response"];
|
|
9248
|
+
if (tr !== null && typeof tr === "object" && !Array.isArray(tr)) {
|
|
9249
|
+
const task = tr["task"];
|
|
9250
|
+
if (task !== null && typeof task === "object" && !Array.isArray(task)) {
|
|
9251
|
+
const output = task["output"];
|
|
9252
|
+
if (typeof output === "string") return output;
|
|
9253
|
+
}
|
|
9254
|
+
}
|
|
9255
|
+
return extractToolResultText(raw);
|
|
9256
|
+
}
|
|
9179
9257
|
function postTaskOutputHandler(event) {
|
|
9180
9258
|
try {
|
|
9181
9259
|
if (getToolName(event) !== "TaskOutput" || !event.sessionId) return passOutput();
|
|
9182
9260
|
const taskId = getTaskId(getToolInput(event));
|
|
9183
9261
|
if (taskId === void 0) return passOutput();
|
|
9184
|
-
const rawOutput =
|
|
9262
|
+
const rawOutput = extractTaskOutputText(event.raw);
|
|
9185
9263
|
if (!rawOutput) return passOutput();
|
|
9186
9264
|
const output = redactSecrets(rawOutput).text;
|
|
9187
9265
|
if (!output) return passOutput();
|
|
@@ -9201,10 +9279,7 @@ function postTaskOutputHandler(event) {
|
|
|
9201
9279
|
})) {
|
|
9202
9280
|
return passOutput();
|
|
9203
9281
|
}
|
|
9204
|
-
return {
|
|
9205
|
-
hookType: "rewriteOutput",
|
|
9206
|
-
updatedOutput: collapsed
|
|
9207
|
-
};
|
|
9282
|
+
return emitRewrite(collapsed, "taskoutput", { kind: "taskoutput:collapse", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
|
|
9208
9283
|
}
|
|
9209
9284
|
if (output === prior.output) {
|
|
9210
9285
|
storePollSnapshot2(event.sessionId, taskId, output);
|
|
@@ -9218,10 +9293,7 @@ function postTaskOutputHandler(event) {
|
|
|
9218
9293
|
})) {
|
|
9219
9294
|
return passOutput();
|
|
9220
9295
|
}
|
|
9221
|
-
return {
|
|
9222
|
-
hookType: "rewriteOutput",
|
|
9223
|
-
updatedOutput: unchangedNotice
|
|
9224
|
-
};
|
|
9296
|
+
return emitRewrite(unchangedNotice, "taskoutput", { kind: "taskoutput:unchanged", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
|
|
9225
9297
|
}
|
|
9226
9298
|
if (!output.startsWith(prior.output)) {
|
|
9227
9299
|
storePollSnapshot2(event.sessionId, taskId, output);
|
|
@@ -9241,10 +9313,7 @@ function postTaskOutputHandler(event) {
|
|
|
9241
9313
|
})) {
|
|
9242
9314
|
return passOutput();
|
|
9243
9315
|
}
|
|
9244
|
-
return {
|
|
9245
|
-
hookType: "rewriteOutput",
|
|
9246
|
-
updatedOutput: `${deltaNotice}${collapsedDelta}`
|
|
9247
|
-
};
|
|
9316
|
+
return emitRewrite(`${deltaNotice}${collapsedDelta}`, "taskoutput", { kind: "taskoutput:delta", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
|
|
9248
9317
|
} catch {
|
|
9249
9318
|
return passOutput();
|
|
9250
9319
|
}
|
|
@@ -9340,8 +9409,10 @@ var APPROVED_PLAN_MARKER = "## Approved Plan:";
|
|
|
9340
9409
|
function postExitPlanModeHandler(event) {
|
|
9341
9410
|
try {
|
|
9342
9411
|
if (getToolName(event) !== "ExitPlanMode") return passOutput();
|
|
9343
|
-
const
|
|
9344
|
-
if (!
|
|
9412
|
+
const rawOutput = extractToolResultText(event.raw);
|
|
9413
|
+
if (!rawOutput) return passOutput();
|
|
9414
|
+
const redacted = redactSecrets(rawOutput);
|
|
9415
|
+
const output = redacted.text;
|
|
9345
9416
|
const markerIndex = output.indexOf(APPROVED_PLAN_MARKER);
|
|
9346
9417
|
if (markerIndex === -1) return passOutput();
|
|
9347
9418
|
const planValue = getToolInput(event)["plan"];
|
|
@@ -9360,10 +9431,10 @@ ${PLAN_OMIT_POINTER}`;
|
|
|
9360
9431
|
})) {
|
|
9361
9432
|
return passOutput();
|
|
9362
9433
|
}
|
|
9363
|
-
return {
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
};
|
|
9434
|
+
return emitRewrite(`${prefix}${notice}`, "exitplanmode", {
|
|
9435
|
+
kind: "plan_echo_collapse",
|
|
9436
|
+
originalBytes: Buffer.byteLength(output, "utf-8")
|
|
9437
|
+
});
|
|
9367
9438
|
} catch {
|
|
9368
9439
|
return passOutput();
|
|
9369
9440
|
}
|
|
@@ -9431,6 +9502,9 @@ function storeMcpOutput(sessionId, toolName, toolInput, resultText) {
|
|
|
9431
9502
|
${redactedOutput}`, entry.storedAt);
|
|
9432
9503
|
return id;
|
|
9433
9504
|
}
|
|
9505
|
+
function mcpOutputBytes(id) {
|
|
9506
|
+
return getBashOutput(id)?.sizeBytes ?? 0;
|
|
9507
|
+
}
|
|
9434
9508
|
function getMcpOutput(sessionId, toolName, toolInput, ttlMs = Number.POSITIVE_INFINITY) {
|
|
9435
9509
|
if (!sessionId) return null;
|
|
9436
9510
|
const id = mcpOutputId(sessionId, mcpHash(toolName, toolInput));
|
|
@@ -9753,6 +9827,8 @@ function preMcpHandler(event) {
|
|
|
9753
9827
|
const ttlMs = loadConfig().hints.mcp_dedup_ttl_secs * 1e3;
|
|
9754
9828
|
const id = getMcpOutput(event.sessionId, toolName, toolInput, ttlMs);
|
|
9755
9829
|
if (!id) return passOutput();
|
|
9830
|
+
const denyCredit = Math.min(mcpOutputBytes(id), PER_FILE_COUNTERFACTUAL_CEILING);
|
|
9831
|
+
recordStat("mcp:recall", denyCredit, savedTokensFromBytes(denyCredit));
|
|
9756
9832
|
return denyOutput(
|
|
9757
9833
|
"Identical read-only MCP call already cached this session. Use `token-goat bash-output " + id + "` to recall the result (add `--grep PATTERN`, `--tail N`, or `--head N` to slice) instead of repeating the call."
|
|
9758
9834
|
);
|
|
@@ -9770,11 +9846,16 @@ function postMcpHandler(event) {
|
|
|
9770
9846
|
injectionMatches = [];
|
|
9771
9847
|
}
|
|
9772
9848
|
if (injectionMatches.length > 0) recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
|
|
9773
|
-
const
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
9849
|
+
const redactedResult = redactSecrets(resultText);
|
|
9850
|
+
const fenced = () => emitRewrite(
|
|
9851
|
+
fenceUntrustedContent(redactedResult.text, injectionMatches, UNTRUSTED_TOOL_TAG),
|
|
9852
|
+
"mcp"
|
|
9853
|
+
);
|
|
9854
|
+
const passOrFence = () => {
|
|
9855
|
+
if (injectionMatches.length > 0) return fenced();
|
|
9856
|
+
if (redactedResult.count > 0) return emitRewrite(redactedResult.text, "mcp");
|
|
9857
|
+
return passOutput();
|
|
9858
|
+
};
|
|
9778
9859
|
if (!event.sessionId) return passOrFence();
|
|
9779
9860
|
if (isMcpErrorResponse(event.raw)) return passOrFence();
|
|
9780
9861
|
const readOnly = isMcpReadOnly(toolName, toolInput);
|
|
@@ -9792,17 +9873,19 @@ function postMcpHandler(event) {
|
|
|
9792
9873
|
const redactedBody = redactSecrets(compressed).text;
|
|
9793
9874
|
const notice = `[token-goat: compressed, full via mcp-output ${id}]
|
|
9794
9875
|
`;
|
|
9876
|
+
const originalBytes = Buffer.byteLength(resultText, "utf-8");
|
|
9795
9877
|
const worthwhile = isRewriteWorthwhile({
|
|
9796
|
-
originalBytes
|
|
9878
|
+
originalBytes,
|
|
9797
9879
|
rewrittenBytes: Buffer.byteLength(redactedBody, "utf-8"),
|
|
9798
9880
|
noticeBytes: Buffer.byteLength(notice, "utf-8"),
|
|
9799
9881
|
minNetSavingsBytes: resolveMinNetSavingsBytes()
|
|
9800
9882
|
});
|
|
9801
9883
|
if (worthwhile) {
|
|
9802
|
-
return
|
|
9803
|
-
|
|
9804
|
-
|
|
9805
|
-
|
|
9884
|
+
return emitRewrite(
|
|
9885
|
+
injectionMatches.length > 0 ? `${notice}${fenceUntrustedContent(redactedBody, injectionMatches, UNTRUSTED_TOOL_TAG)}` : `${notice}${redactedBody}`,
|
|
9886
|
+
"mcp",
|
|
9887
|
+
{ kind: "mcp:compress", originalBytes }
|
|
9888
|
+
);
|
|
9806
9889
|
}
|
|
9807
9890
|
}
|
|
9808
9891
|
}
|
|
@@ -9848,14 +9931,32 @@ function preWebSearchDedupHandler(event) {
|
|
|
9848
9931
|
}
|
|
9849
9932
|
function postWebSearchHandler(event) {
|
|
9850
9933
|
try {
|
|
9851
|
-
if (getToolName(event) !== "WebSearch"
|
|
9852
|
-
const signature = webSearchSignatureInput(getToolInput(event));
|
|
9853
|
-
if (signature === null) return passOutput();
|
|
9854
|
-
if (isMcpErrorResponse(event.raw)) return passOutput();
|
|
9934
|
+
if (getToolName(event) !== "WebSearch") return passOutput();
|
|
9855
9935
|
const resultText = extractToolResultText(event.raw);
|
|
9856
9936
|
if (!resultText) return passOutput();
|
|
9937
|
+
let injectionMatches = [];
|
|
9938
|
+
try {
|
|
9939
|
+
if (loadConfig().injection.enabled) injectionMatches = scanForInjectionPatterns(resultText);
|
|
9940
|
+
} catch {
|
|
9941
|
+
injectionMatches = [];
|
|
9942
|
+
}
|
|
9943
|
+
if (injectionMatches.length > 0) recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
|
|
9944
|
+
const redacted = redactSecrets(resultText);
|
|
9945
|
+
const passOrFence = () => {
|
|
9946
|
+
if (injectionMatches.length > 0) {
|
|
9947
|
+
return emitRewrite(fenceUntrustedContent(redacted.text, injectionMatches), "websearch");
|
|
9948
|
+
}
|
|
9949
|
+
if (redacted.count > 0) {
|
|
9950
|
+
return emitRewrite(redacted.text, "websearch");
|
|
9951
|
+
}
|
|
9952
|
+
return passOutput();
|
|
9953
|
+
};
|
|
9954
|
+
if (!event.sessionId) return passOrFence();
|
|
9955
|
+
const signature = webSearchSignatureInput(getToolInput(event));
|
|
9956
|
+
if (signature === null) return passOrFence();
|
|
9957
|
+
if (isMcpErrorResponse(event.raw)) return passOrFence();
|
|
9857
9958
|
storeMcpOutput(event.sessionId, "WebSearch", signature, resultText);
|
|
9858
|
-
return
|
|
9959
|
+
return passOrFence();
|
|
9859
9960
|
} catch {
|
|
9860
9961
|
return passOutput();
|
|
9861
9962
|
}
|
|
@@ -9905,13 +10006,15 @@ async function shrinkImageBlock(block) {
|
|
|
9905
10006
|
const mediaType = typeof source?.media_type === "string" ? source.media_type : "image/png";
|
|
9906
10007
|
const data = typeof source?.data === "string" ? source.data : "";
|
|
9907
10008
|
const originalDataUrl = `data:${mediaType};base64,${data}`;
|
|
9908
|
-
if (source?.type !== "base64" || data === "") return { text: originalDataUrl, changed: false, savedBytes: 0 };
|
|
10009
|
+
if (source?.type !== "base64" || data === "") return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
|
|
9909
10010
|
let buffer;
|
|
9910
10011
|
try {
|
|
9911
10012
|
buffer = Buffer.from(data, "base64");
|
|
9912
10013
|
} catch {
|
|
9913
|
-
return { text: originalDataUrl, changed: false, savedBytes: 0 };
|
|
10014
|
+
return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
|
|
9914
10015
|
}
|
|
10016
|
+
const meta = await probeImageMeta(buffer);
|
|
10017
|
+
const tier = loadConfig().image_shrink.vision_tier;
|
|
9915
10018
|
const fingerprint = screenshotFingerprint(data);
|
|
9916
10019
|
const alreadyShown = hasSeenImage(fingerprint);
|
|
9917
10020
|
recordSeenImage(fingerprint);
|
|
@@ -9926,16 +10029,23 @@ async function shrinkImageBlock(block) {
|
|
|
9926
10029
|
return {
|
|
9927
10030
|
text: SCREENSHOT_REPEAT_NOTICE,
|
|
9928
10031
|
changed: true,
|
|
9929
|
-
|
|
10032
|
+
// Decoded image bytes, not base64 characters. Both branches of this function file under the same `image_shrink` kind, and the shrink branch below credits `result.originalBytes - result.shrunkBytes` -- decoded bytes, the convention documented at image_shrink.ts's own recordStat call. Base64 inflates by 4/3, so crediting `originalDataUrl.length` here booked roughly 33% more for a repeat screenshot than an identical shrink of the same image, and the ledger summed the two units into one row. The gate above deliberately still measures the data URL: it decides whether the rewrite pays off on the wire, where base64 characters are what is actually sent.
|
|
10033
|
+
savedBytes: buffer.length - Buffer.byteLength(SCREENSHOT_REPEAT_NOTICE, "utf-8"),
|
|
10034
|
+
// The whole image is withheld here rather than resized, so the visual tokens saved are its
|
|
10035
|
+
// entire billed cost less the notice standing in for it. Billed cost, not raw patch count:
|
|
10036
|
+
// an oversized screenshot would have been capped by the API's own downscale, so pricing the
|
|
10037
|
+
// untouched dimensions would credit a bill that was never going to be sent.
|
|
10038
|
+
savedTokens: Math.max(0, visionTokens(meta?.width ?? 0, meta?.height ?? 0, tier) - savedTokensFromBytes(Buffer.byteLength(SCREENSHOT_REPEAT_NOTICE, "utf-8")))
|
|
9930
10039
|
};
|
|
9931
10040
|
}
|
|
9932
10041
|
}
|
|
9933
10042
|
const result = await imageQualifiesForShrink(buffer) ? await shrinkImage(buffer, { sizeThresholdBytes: 0 }) : null;
|
|
9934
|
-
if (result === null) return { text: originalDataUrl, changed: false, savedBytes: 0 };
|
|
10043
|
+
if (result === null) return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
|
|
9935
10044
|
const saved = result.originalBytes - result.shrunkBytes;
|
|
9936
10045
|
const { summary, dataUrl } = formatShrinkSummary(result, "an inline browser screenshot");
|
|
10046
|
+
const savedTokens = visionTokensSaved(result.originalWidth, result.originalHeight, result.width, result.height, tier);
|
|
9937
10047
|
return { text: `${summary}
|
|
9938
|
-
${dataUrl}`, changed: true, savedBytes: saved };
|
|
10048
|
+
${dataUrl}`, changed: true, savedBytes: saved, savedTokens };
|
|
9939
10049
|
}
|
|
9940
10050
|
var TAB_CONTEXT_UNCHANGED_NOTICE = "(tabs unchanged since last check)";
|
|
9941
10051
|
function dedupTabContext(text) {
|
|
@@ -9963,15 +10073,19 @@ async function postBrowserImageHandler(event) {
|
|
|
9963
10073
|
const parts = [];
|
|
9964
10074
|
for (const block of blocks) {
|
|
9965
10075
|
if (block.type === "image") {
|
|
9966
|
-
const { text, changed, savedBytes } = await shrinkImageBlock(block);
|
|
10076
|
+
const { text, changed, savedBytes, savedTokens } = await shrinkImageBlock(block);
|
|
9967
10077
|
if (changed) {
|
|
9968
10078
|
anyChanged = true;
|
|
9969
|
-
recordStat("image_shrink", savedBytes,
|
|
10079
|
+
recordStat("image_shrink", savedBytes, savedTokens, void 0, toolName);
|
|
9970
10080
|
}
|
|
9971
10081
|
parts.push(text);
|
|
9972
10082
|
} else if (block.type === "text" && typeof block.text === "string") {
|
|
9973
10083
|
const { text, changed } = dedupTabContext(block.text);
|
|
9974
|
-
if (changed)
|
|
10084
|
+
if (changed) {
|
|
10085
|
+
anyChanged = true;
|
|
10086
|
+
const savedBytes = Buffer.byteLength(block.text, "utf-8") - Buffer.byteLength(text, "utf-8");
|
|
10087
|
+
if (savedBytes > 0) recordStat("browser_tab_dedup", savedBytes, savedTokensFromBytes(savedBytes), void 0, toolName);
|
|
10088
|
+
}
|
|
9975
10089
|
parts.push(text);
|
|
9976
10090
|
} else {
|
|
9977
10091
|
try {
|
|
@@ -9982,7 +10096,7 @@ async function postBrowserImageHandler(event) {
|
|
|
9982
10096
|
}
|
|
9983
10097
|
}
|
|
9984
10098
|
if (!anyChanged) return passOutput();
|
|
9985
|
-
return
|
|
10099
|
+
return emitRewrite(parts.join("\n"), toolName);
|
|
9986
10100
|
} catch {
|
|
9987
10101
|
return passOutput();
|
|
9988
10102
|
}
|
|
@@ -9991,6 +10105,9 @@ registerHook("post_tool_use", postBrowserImageHandler, { toolPattern: "^mcp__" }
|
|
|
9991
10105
|
|
|
9992
10106
|
// src/hooks_agent_spawn.ts
|
|
9993
10107
|
import { createHash as createHash2 } from "node:crypto";
|
|
10108
|
+
import * as fs from "node:fs";
|
|
10109
|
+
import * as os from "node:os";
|
|
10110
|
+
import * as path2 from "node:path";
|
|
9994
10111
|
var BRIEFING_TARGET_TOKENS = 550;
|
|
9995
10112
|
function buildSubagentBriefing() {
|
|
9996
10113
|
try {
|
|
@@ -10062,8 +10179,11 @@ function findDuplicateOutstandingPrompt(prompt) {
|
|
|
10062
10179
|
function truncateForWarning(text, max) {
|
|
10063
10180
|
return text.length > max ? text.slice(0, max) + "..." : text;
|
|
10064
10181
|
}
|
|
10182
|
+
function isAgentTool(toolName) {
|
|
10183
|
+
return toolName === "Agent" || toolName === "task" || toolName === "Task";
|
|
10184
|
+
}
|
|
10065
10185
|
function preAgentHandler(event) {
|
|
10066
|
-
if (event.toolName
|
|
10186
|
+
if (!isAgentTool(event.toolName)) return passOutput();
|
|
10067
10187
|
const toolInput = event.toolInput;
|
|
10068
10188
|
const prompt = toolInput["prompt"];
|
|
10069
10189
|
if (typeof prompt !== "string" || prompt.trim() === "") return passOutput();
|
|
@@ -10085,7 +10205,7 @@ function preAgentHandler(event) {
|
|
|
10085
10205
|
return passOutput();
|
|
10086
10206
|
}
|
|
10087
10207
|
}
|
|
10088
|
-
var FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})(
|
|
10208
|
+
var FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})([^\n]*)$/;
|
|
10089
10209
|
function findFencedBlockLines(lines) {
|
|
10090
10210
|
const blocks = [];
|
|
10091
10211
|
let fenceStart = -1;
|
|
@@ -10185,21 +10305,118 @@ function collapseBlankRunsInFences(text) {
|
|
|
10185
10305
|
out.push(...lines.slice(cursor));
|
|
10186
10306
|
return changedAny ? out.join("\n") : text;
|
|
10187
10307
|
}
|
|
10308
|
+
var SPAWN_RESTRICT_HINT_KEY = "agent-spawn-restrict-hint";
|
|
10309
|
+
var SPAWN_RESTRICT_MAX_NAMES = 3;
|
|
10310
|
+
var ROSTER_WALK_MAX_DEPTH = 4;
|
|
10311
|
+
var ROSTER_WALK_MAX_FILES = 400;
|
|
10312
|
+
function parseAgentDefinition(text, fallbackName) {
|
|
10313
|
+
const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
|
|
10314
|
+
if (!fmMatch) return null;
|
|
10315
|
+
const fm = fmMatch[1];
|
|
10316
|
+
const nameMatch = /^name:(.*)$/m.exec(fm);
|
|
10317
|
+
const rawName = nameMatch ? nameMatch[1].trim().replace(/^["']|["']$/g, "") : "";
|
|
10318
|
+
const name2 = rawName !== "" ? rawName : fallbackName;
|
|
10319
|
+
const toolsMatch = /^tools:(.*)$/m.exec(fm);
|
|
10320
|
+
if (!toolsMatch) return { name: name2, restricted: false };
|
|
10321
|
+
const inline = toolsMatch[1].trim();
|
|
10322
|
+
if (inline === "*" || inline === '"*"' || inline === "'*'") return { name: name2, restricted: false };
|
|
10323
|
+
if (inline !== "") return { name: name2, restricted: true };
|
|
10324
|
+
const after = fm.slice(toolsMatch.index + toolsMatch[0].length);
|
|
10325
|
+
for (const line of after.split(/\r?\n/)) {
|
|
10326
|
+
if (/^\s+-\s*\S/.test(line)) return { name: name2, restricted: true };
|
|
10327
|
+
if (/^\S/.test(line)) break;
|
|
10328
|
+
}
|
|
10329
|
+
return { name: name2, restricted: false };
|
|
10330
|
+
}
|
|
10331
|
+
function findRestrictedAgentNames(roots) {
|
|
10332
|
+
const scanRoots = roots ?? [path2.join(os.homedir(), ".claude", "agents")];
|
|
10333
|
+
const names = /* @__PURE__ */ new Set();
|
|
10334
|
+
const visited = /* @__PURE__ */ new Set();
|
|
10335
|
+
let filesSeen = 0;
|
|
10336
|
+
const walk = (dir, depth) => {
|
|
10337
|
+
if (depth > ROSTER_WALK_MAX_DEPTH || filesSeen >= ROSTER_WALK_MAX_FILES) return;
|
|
10338
|
+
let real;
|
|
10339
|
+
try {
|
|
10340
|
+
real = fs.realpathSync(dir);
|
|
10341
|
+
} catch {
|
|
10342
|
+
return;
|
|
10343
|
+
}
|
|
10344
|
+
const key = process.platform === "win32" ? real.toLocaleLowerCase() : real;
|
|
10345
|
+
if (visited.has(key)) return;
|
|
10346
|
+
visited.add(key);
|
|
10347
|
+
let entries;
|
|
10348
|
+
try {
|
|
10349
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
10350
|
+
} catch {
|
|
10351
|
+
return;
|
|
10352
|
+
}
|
|
10353
|
+
for (const entry of entries) {
|
|
10354
|
+
if (filesSeen >= ROSTER_WALK_MAX_FILES) return;
|
|
10355
|
+
const full = path2.join(dir, entry.name);
|
|
10356
|
+
let stat;
|
|
10357
|
+
try {
|
|
10358
|
+
stat = fs.statSync(full);
|
|
10359
|
+
} catch {
|
|
10360
|
+
continue;
|
|
10361
|
+
}
|
|
10362
|
+
if (stat.isDirectory()) {
|
|
10363
|
+
walk(full, depth + 1);
|
|
10364
|
+
continue;
|
|
10365
|
+
}
|
|
10366
|
+
if (!stat.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
|
|
10367
|
+
filesSeen++;
|
|
10368
|
+
let text;
|
|
10369
|
+
try {
|
|
10370
|
+
text = fs.readFileSync(full, "utf-8");
|
|
10371
|
+
} catch {
|
|
10372
|
+
continue;
|
|
10373
|
+
}
|
|
10374
|
+
const parsed = parseAgentDefinition(text, path2.basename(entry.name, path2.extname(entry.name)));
|
|
10375
|
+
if (parsed !== null && parsed.restricted) names.add(parsed.name);
|
|
10376
|
+
}
|
|
10377
|
+
};
|
|
10378
|
+
for (const root of scanRoots) walk(root, 0);
|
|
10379
|
+
return Array.from(names).sort();
|
|
10380
|
+
}
|
|
10381
|
+
function buildUnrestrictedSpawnAdvisory(toolInput) {
|
|
10382
|
+
try {
|
|
10383
|
+
if (getHarnessName() === "copilot_cli") return "";
|
|
10384
|
+
const rawType = toolInput["subagent_type"];
|
|
10385
|
+
const spawnType = typeof rawType === "string" ? rawType.trim() : "";
|
|
10386
|
+
if (spawnType !== "" && spawnType !== "general-purpose") return "";
|
|
10387
|
+
if (wasHintShown(SPAWN_RESTRICT_HINT_KEY)) return "";
|
|
10388
|
+
const names = findRestrictedAgentNames();
|
|
10389
|
+
if (names.length === 0) return "";
|
|
10390
|
+
markHintShown(SPAWN_RESTRICT_HINT_KEY);
|
|
10391
|
+
recordStat("session_hint", 0, 0, void 0, "agent-spawn-restrict");
|
|
10392
|
+
const shown = names.slice(0, SPAWN_RESTRICT_MAX_NAMES).join(", ");
|
|
10393
|
+
const more = names.length > SPAWN_RESTRICT_MAX_NAMES ? ` and ${names.length - SPAWN_RESTRICT_MAX_NAMES} more` : "";
|
|
10394
|
+
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.`;
|
|
10395
|
+
} catch {
|
|
10396
|
+
return "";
|
|
10397
|
+
}
|
|
10398
|
+
}
|
|
10188
10399
|
function postAgentHandler(event) {
|
|
10189
10400
|
try {
|
|
10190
|
-
if (event.toolName
|
|
10401
|
+
if (!isAgentTool(event.toolName) || !event.sessionId) return passOutput();
|
|
10191
10402
|
const finishedPrompt = event.toolInput["prompt"];
|
|
10192
10403
|
if (typeof finishedPrompt === "string" && finishedPrompt !== "") {
|
|
10193
10404
|
removeOutstandingAgentSpawn(finishedPrompt);
|
|
10194
10405
|
}
|
|
10195
|
-
const
|
|
10406
|
+
const redactedReport = redactSecrets(extractToolResultText(event.raw));
|
|
10407
|
+
const resultText = redactedReport.text;
|
|
10196
10408
|
const agentReportCfg = loadConfig().agent_report;
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10409
|
+
const spawnAdvisory = buildUnrestrictedSpawnAdvisory(event.toolInput);
|
|
10410
|
+
if (!resultText || resultText.length < agentReportCfg.min_bytes) {
|
|
10411
|
+
return spawnAdvisory === "" ? passOutput() : contextOutput(spawnAdvisory);
|
|
10412
|
+
}
|
|
10413
|
+
const id = storeMcpOutput(event.sessionId, event.toolName ?? "Agent", event.toolInput, resultText);
|
|
10414
|
+
if (id === null) return spawnAdvisory === "" ? passOutput() : contextOutput(spawnAdvisory);
|
|
10415
|
+
if (redactedReport.count > 0) recordStat("secret_redacted", 0, redactedReport.count, void 0, "agent");
|
|
10200
10416
|
recordStat("session_hint", 0, 0);
|
|
10201
10417
|
const recallHint = `token-goat mcp-output ${id} --full`;
|
|
10202
|
-
const notice = `[token-goat] This subagent report (${toKB(resultText.length)}KB) is cached for later recall: ${recallHint}
|
|
10418
|
+
const notice = `[token-goat] This subagent report (${toKB(resultText.length)}KB) is cached for later recall: ${recallHint}${spawnAdvisory === "" ? "" : `
|
|
10419
|
+
${spawnAdvisory}`}`;
|
|
10203
10420
|
const collapsed = collapseFencedBlocks(resultText, recallHint, agentReportCfg.fence_collapse_min_lines, agentReportCfg.fence_collapse_keep_lines);
|
|
10204
10421
|
const deduped = dedupeFencedBlocks(collapsed, resultText, recallHint);
|
|
10205
10422
|
const final = collapseBlankRunsInFences(deduped);
|
|
@@ -10216,8 +10433,8 @@ function postAgentHandler(event) {
|
|
|
10216
10433
|
|
|
10217
10434
|
${notice}`;
|
|
10218
10435
|
const savedBytes = originalBytes - Buffer.byteLength(updatedOutput, "utf-8");
|
|
10219
|
-
if (savedBytes > 0) recordStat("agent_report_compact", savedBytes,
|
|
10220
|
-
return
|
|
10436
|
+
if (savedBytes > 0) recordStat("agent_report_compact", savedBytes, savedTokensFromBytes(savedBytes));
|
|
10437
|
+
return emitRewrite(updatedOutput, "agent", void 0, "counted-elsewhere");
|
|
10221
10438
|
}
|
|
10222
10439
|
recordStat("agent_report_compact_declined", 0, 0);
|
|
10223
10440
|
}
|
|
@@ -10227,7 +10444,11 @@ ${notice}`;
|
|
|
10227
10444
|
}
|
|
10228
10445
|
}
|
|
10229
10446
|
registerHook("pre_tool_use", preAgentHandler, { toolName: "Agent" });
|
|
10447
|
+
registerHook("pre_tool_use", preAgentHandler, { toolName: "task" });
|
|
10448
|
+
registerHook("pre_tool_use", preAgentHandler, { toolName: "Task" });
|
|
10230
10449
|
registerHook("post_tool_use", postAgentHandler, { toolName: "Agent" });
|
|
10450
|
+
registerHook("post_tool_use", postAgentHandler, { toolName: "task" });
|
|
10451
|
+
registerHook("post_tool_use", postAgentHandler, { toolName: "Task" });
|
|
10231
10452
|
|
|
10232
10453
|
// src/relay.ts
|
|
10233
10454
|
function isHookEventName(name2) {
|
|
@@ -10243,7 +10464,11 @@ function buildEvent(eventName, payload) {
|
|
|
10243
10464
|
const sessionId = typeof rawSession === "string" ? rawSession : "";
|
|
10244
10465
|
const rawAgentId = obj["agent_id"] ?? obj["agentId"];
|
|
10245
10466
|
const agentId = typeof rawAgentId === "string" && rawAgentId !== "" ? rawAgentId : void 0;
|
|
10246
|
-
|
|
10467
|
+
const rawTraceparent = obj["traceparent"] ?? obj["traceParent"] ?? process.env["TRACEPARENT"] ?? process.env["traceparent"];
|
|
10468
|
+
const traceparent = typeof rawTraceparent === "string" && rawTraceparent.trim() !== "" ? rawTraceparent.trim() : void 0;
|
|
10469
|
+
const rawTracestate = obj["tracestate"] ?? obj["traceState"] ?? process.env["TRACESTATE"] ?? process.env["tracestate"];
|
|
10470
|
+
const tracestate = typeof rawTracestate === "string" && rawTracestate.trim() !== "" ? rawTracestate.trim() : void 0;
|
|
10471
|
+
return { eventName, toolName, toolInput, sessionId, agentId, traceparent, tracestate, raw: obj };
|
|
10247
10472
|
}
|
|
10248
10473
|
function sessionStateKey(event) {
|
|
10249
10474
|
return event.agentId !== void 0 ? `${event.sessionId}:agent:${event.agentId}` : event.sessionId;
|
|
@@ -10254,6 +10479,7 @@ function harnessForNormalization() {
|
|
|
10254
10479
|
if (detected === "gemini") return "gemini";
|
|
10255
10480
|
if (detected === "grok") return "grok";
|
|
10256
10481
|
if (detected === "kimi") return "kimi";
|
|
10482
|
+
if (detected === "qwen") return "qwen";
|
|
10257
10483
|
if (detected === "copilot_cli") return "copilot_cli";
|
|
10258
10484
|
return "claude";
|
|
10259
10485
|
}
|
|
@@ -10268,6 +10494,12 @@ async function relayInProcess(eventName, rawPayload) {
|
|
|
10268
10494
|
if (!process.env["CLAUDE_CODE_SESSION_ID"] && event.sessionId) {
|
|
10269
10495
|
process.env["CLAUDE_CODE_SESSION_ID"] = event.sessionId;
|
|
10270
10496
|
}
|
|
10497
|
+
if (!process.env["TRACEPARENT"] && event.traceparent) {
|
|
10498
|
+
process.env["TRACEPARENT"] = event.traceparent;
|
|
10499
|
+
}
|
|
10500
|
+
if (!process.env["TRACESTATE"] && event.tracestate) {
|
|
10501
|
+
process.env["TRACESTATE"] = event.tracestate;
|
|
10502
|
+
}
|
|
10271
10503
|
const stateKey = sessionStateKey(event);
|
|
10272
10504
|
try {
|
|
10273
10505
|
loadSessionState(stateKey);
|