token-goat 2.8.3 → 2.8.6
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 +23 -17
- package/dist/{token-goat-chunk-6ODZ6PZK.mjs → token-goat-chunk-2JZ66BBE.mjs} +216 -72
- package/dist/{token-goat-chunk-PWVXXPCC.mjs → token-goat-chunk-DK4VLLYB.mjs} +563 -155
- package/dist/{token-goat-chunk-EFF2XCLB.mjs → token-goat-chunk-GUNYAGOZ.mjs} +175 -56
- package/dist/{token-goat-chunk-VAGPOZHO.mjs → token-goat-chunk-L2XHDICZ.mjs} +2 -2
- package/dist/{token-goat-chunk-DP3NNGGV.mjs → token-goat-chunk-LJEETTER.mjs} +5 -5
- package/dist/{token-goat-chunk-NKNCHJ4H.mjs → token-goat-chunk-MA5237JN.mjs} +327 -169
- package/dist/{token-goat-chunk-4OM2Q2SX.mjs → token-goat-chunk-SHN4UTL4.mjs} +5 -5
- package/dist/{token-goat-chunk-4HIMCBYK.mjs → token-goat-chunk-TELKICYU.mjs} +55 -6
- package/dist/{token-goat-chunk-222VPFP2.mjs → token-goat-chunk-U7M2LTGP.mjs} +1100 -269
- package/dist/{token-goat-chunk-TX4JFJTD.mjs → token-goat-chunk-XAWIELXH.mjs} +216 -105
- package/dist/{token-goat-chunk-C5IL6MJH.mjs → token-goat-chunk-ZIPBLIUZ.mjs} +6 -5
- 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-TELKICYU.mjs";
|
|
27
27
|
import {
|
|
28
28
|
BODY_FIRST_TOOL_RESPONSE_KEYS,
|
|
29
29
|
OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
|
|
@@ -42,14 +42,14 @@ import {
|
|
|
42
42
|
countSymbols,
|
|
43
43
|
denyOutput,
|
|
44
44
|
emitRewrite,
|
|
45
|
+
emitRewriteIfChanged,
|
|
45
46
|
enqueueDirtyPathSafe,
|
|
46
47
|
ensureWorkerAlive,
|
|
47
48
|
estimateTokens,
|
|
48
|
-
estimateTokensFromLength,
|
|
49
49
|
extractCompactFromMarker,
|
|
50
50
|
extractToolResponseField,
|
|
51
51
|
extractToolResultText,
|
|
52
|
-
|
|
52
|
+
fenceWithMatches,
|
|
53
53
|
formatProjectMap,
|
|
54
54
|
formatShrinkSummary,
|
|
55
55
|
getBashOutputId,
|
|
@@ -89,6 +89,7 @@ import {
|
|
|
89
89
|
matchesDenyPattern,
|
|
90
90
|
meetsSavingsFloor,
|
|
91
91
|
passOutput,
|
|
92
|
+
probeImageMeta,
|
|
92
93
|
recordBashOutput,
|
|
93
94
|
recordBashRerun,
|
|
94
95
|
recordCliRead,
|
|
@@ -108,7 +109,7 @@ import {
|
|
|
108
109
|
removeOutstandingAgentSpawn,
|
|
109
110
|
runHook,
|
|
110
111
|
saveSessionState,
|
|
111
|
-
|
|
112
|
+
scanAndRecord,
|
|
112
113
|
serializeOutput,
|
|
113
114
|
sessionOutputBodyBytes,
|
|
114
115
|
sessionSidecarPath,
|
|
@@ -117,10 +118,12 @@ import {
|
|
|
117
118
|
storeBlob,
|
|
118
119
|
storeOutput,
|
|
119
120
|
takePendingLargeFileHint,
|
|
121
|
+
visionTokens,
|
|
122
|
+
visionTokensSaved,
|
|
120
123
|
wasCliReadThisSession,
|
|
121
124
|
wasFileReadThisSession,
|
|
122
125
|
wasHintShown
|
|
123
|
-
} from "./token-goat-chunk-
|
|
126
|
+
} from "./token-goat-chunk-DK4VLLYB.mjs";
|
|
124
127
|
import {
|
|
125
128
|
canRunWrappedShell,
|
|
126
129
|
compressOutput,
|
|
@@ -131,7 +134,7 @@ import {
|
|
|
131
134
|
isRewriteWorthwhile,
|
|
132
135
|
resolveMinNetSavingsBytes,
|
|
133
136
|
shlexSplit
|
|
134
|
-
} from "./token-goat-chunk-
|
|
137
|
+
} from "./token-goat-chunk-GUNYAGOZ.mjs";
|
|
135
138
|
import {
|
|
136
139
|
PER_FILE_COUNTERFACTUAL_CEILING,
|
|
137
140
|
VERSION,
|
|
@@ -150,9 +153,10 @@ import {
|
|
|
150
153
|
redactSecrets,
|
|
151
154
|
resolveIndexPath,
|
|
152
155
|
runGit,
|
|
156
|
+
savedTokensFromBytes,
|
|
153
157
|
shortFingerprint,
|
|
154
158
|
toKB
|
|
155
|
-
} from "./token-goat-chunk-
|
|
159
|
+
} from "./token-goat-chunk-2JZ66BBE.mjs";
|
|
156
160
|
|
|
157
161
|
// src/hooks_grep.ts
|
|
158
162
|
function grepIntInput(toolInput, key) {
|
|
@@ -167,7 +171,7 @@ function grepIntInput(toolInput, key) {
|
|
|
167
171
|
function grepSignature(toolInput) {
|
|
168
172
|
const pattern = toolInput["pattern"];
|
|
169
173
|
if (typeof pattern !== "string" || pattern === "") return null;
|
|
170
|
-
const
|
|
174
|
+
const path3 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
|
|
171
175
|
const outputMode = typeof toolInput["output_mode"] === "string" ? toolInput["output_mode"] : "files_with_matches";
|
|
172
176
|
const glob = typeof toolInput["glob"] === "string" ? toolInput["glob"] : "";
|
|
173
177
|
const type = typeof toolInput["type"] === "string" ? toolInput["type"] : "";
|
|
@@ -182,7 +186,7 @@ function grepSignature(toolInput) {
|
|
|
182
186
|
const offset = grepIntInput(toolInput, "offset");
|
|
183
187
|
return JSON.stringify([
|
|
184
188
|
pattern,
|
|
185
|
-
|
|
189
|
+
path3,
|
|
186
190
|
outputMode,
|
|
187
191
|
glob,
|
|
188
192
|
type,
|
|
@@ -218,8 +222,7 @@ function foldGrepContentHandler(event) {
|
|
|
218
222
|
if (toolInput["-n"] === false) return passOutput();
|
|
219
223
|
const rawText = extractToolResponseField(event.raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
|
|
220
224
|
if (!rawText) return passOutput();
|
|
221
|
-
const
|
|
222
|
-
const text = redacted.text;
|
|
225
|
+
const text = redactSecrets(rawText).text;
|
|
223
226
|
const rawLines = text.split(/\r\n|\r|\n/);
|
|
224
227
|
const parsed = [];
|
|
225
228
|
for (const line of rawLines) {
|
|
@@ -260,10 +263,7 @@ function foldGrepContentHandler(event) {
|
|
|
260
263
|
})) {
|
|
261
264
|
return passOutput();
|
|
262
265
|
}
|
|
263
|
-
|
|
264
|
-
recordStat("grep:fold", bytesDelta, Math.round(bytesDelta / 4));
|
|
265
|
-
if (redacted.count > 0) recordStat("secret_redacted", 0, redacted.count, void 0, "grep");
|
|
266
|
-
return { hookType: "rewriteOutput", updatedOutput: rewritten };
|
|
266
|
+
return emitRewrite(rewritten, "grep", { kind: "grep:fold", originalBytes });
|
|
267
267
|
} catch {
|
|
268
268
|
return passOutput();
|
|
269
269
|
}
|
|
@@ -281,8 +281,8 @@ registerHook("post_tool_use", postGrepHandler, { toolName: "Grep" });
|
|
|
281
281
|
function globSignature(toolInput) {
|
|
282
282
|
const pattern = toolInput["pattern"];
|
|
283
283
|
if (typeof pattern !== "string" || pattern === "") return null;
|
|
284
|
-
const
|
|
285
|
-
return JSON.stringify([pattern,
|
|
284
|
+
const path3 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
|
|
285
|
+
return JSON.stringify([pattern, path3]);
|
|
286
286
|
}
|
|
287
287
|
var { post: postGlobHandler, pre: preGlobDedupHandler } = makeDedupHintHandlers({
|
|
288
288
|
toolName: "Glob",
|
|
@@ -430,6 +430,9 @@ function renderReadRow(entry) {
|
|
|
430
430
|
const edited = entry.wasEdited ? ", edited" : "";
|
|
431
431
|
return `- ${entry.path} (${kb}kb, ${entry.readCount} ${plural}${edited})`;
|
|
432
432
|
}
|
|
433
|
+
function renderSymbolReadRow(entry) {
|
|
434
|
+
return `- ${entry.path} (symbols: ${(entry.symbols_read ?? []).join(", ")})`;
|
|
435
|
+
}
|
|
433
436
|
function mergeManifestFiles(parent, siblingFiles) {
|
|
434
437
|
const byPath = /* @__PURE__ */ new Map();
|
|
435
438
|
for (const f of parent) byPath.set(foldPath(f.path), f);
|
|
@@ -457,6 +460,7 @@ function buildManifest(sessionId, cwd) {
|
|
|
457
460
|
const files = siblingFiles.length > 0 ? mergeManifestFiles(ownFiles, siblingFiles) : ownFiles;
|
|
458
461
|
const editedFiles = files.filter((f) => f.wasEdited);
|
|
459
462
|
const readFiles = files.filter((f) => f.readCount > 0 && !f.wasEdited);
|
|
463
|
+
const symbolOnlyFiles = files.filter((f) => f.readCount === 0 && !f.wasEdited && (f.symbols_read?.length ?? 0) > 0);
|
|
460
464
|
const webFetches = [...getSessionWebFetches().entries()];
|
|
461
465
|
const lines = [];
|
|
462
466
|
lines.push("## Session context");
|
|
@@ -469,6 +473,7 @@ function buildManifest(sessionId, cwd) {
|
|
|
469
473
|
editedFiles.map((entry) => `- ${entry.path}`),
|
|
470
474
|
MAX_ROWS
|
|
471
475
|
);
|
|
476
|
+
appendCappedSection(lines, "### Surgically read files (symbol/section reads, never read whole)", symbolOnlyFiles.map(renderSymbolReadRow), MAX_ROWS);
|
|
472
477
|
appendCappedSection(
|
|
473
478
|
lines,
|
|
474
479
|
"### Web URLs fetched",
|
|
@@ -613,7 +618,7 @@ function postCompactHandler(event) {
|
|
|
613
618
|
0,
|
|
614
619
|
0,
|
|
615
620
|
void 0,
|
|
616
|
-
`trigger=${trigger} bytes=${bytes} est_tokens=${
|
|
621
|
+
`trigger=${trigger} bytes=${bytes} est_tokens=${savedTokensFromBytes(bytes)} manifest_paths=${survived}/${sample.length}`
|
|
617
622
|
);
|
|
618
623
|
return passOutput();
|
|
619
624
|
}
|
|
@@ -626,7 +631,7 @@ import crypto from "node:crypto";
|
|
|
626
631
|
var TRACKED_SKILL = "token-goat";
|
|
627
632
|
var MAX_COMMANDS_SHOWN = 8;
|
|
628
633
|
async function currentCommandNames() {
|
|
629
|
-
const { buildProgram } = await import("./token-goat-chunk-
|
|
634
|
+
const { buildProgram } = await import("./token-goat-chunk-SHN4UTL4.mjs");
|
|
630
635
|
return flattenCommandNames(buildCommandManifest(buildProgram()));
|
|
631
636
|
}
|
|
632
637
|
async function recordSkillVersionSnapshot(sessionId, skillName) {
|
|
@@ -1470,11 +1475,11 @@ function mapInner(r, f) {
|
|
|
1470
1475
|
function mapOuter(r, f) {
|
|
1471
1476
|
return r.matched ? f(r) : r;
|
|
1472
1477
|
}
|
|
1473
|
-
function ab(pa, pb,
|
|
1474
|
-
return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) =>
|
|
1478
|
+
function ab(pa, pb, join2) {
|
|
1479
|
+
return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join2(ma.value, vb, data, i, j)));
|
|
1475
1480
|
}
|
|
1476
|
-
function abc(pa, pb, pc,
|
|
1477
|
-
return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) =>
|
|
1481
|
+
function abc(pa, pb, pc, join2) {
|
|
1482
|
+
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))));
|
|
1478
1483
|
}
|
|
1479
1484
|
function ahead(p) {
|
|
1480
1485
|
return (data, i) => mapOuter(p(data, i), (m1) => ({
|
|
@@ -5391,8 +5396,8 @@ function trimCharacterEnd(str, char) {
|
|
|
5391
5396
|
function unicodeEscape(str) {
|
|
5392
5397
|
return str.replace(/[\s\S]/g, (c) => "\\u" + c.charCodeAt().toString(16).padStart(4, "0"));
|
|
5393
5398
|
}
|
|
5394
|
-
function get(obj,
|
|
5395
|
-
for (const key of
|
|
5399
|
+
function get(obj, path3) {
|
|
5400
|
+
for (const key of path3) {
|
|
5396
5401
|
if (!obj) {
|
|
5397
5402
|
return void 0;
|
|
5398
5403
|
}
|
|
@@ -6593,8 +6598,8 @@ function withBrackets(str, brackets) {
|
|
|
6593
6598
|
const rbr = typeof brackets[1] === "string" ? brackets[1] : "]";
|
|
6594
6599
|
return lbr + str + rbr;
|
|
6595
6600
|
}
|
|
6596
|
-
function pathRewrite(
|
|
6597
|
-
const modifiedPath = typeof rewriter === "function" ? rewriter(
|
|
6601
|
+
function pathRewrite(path3, rewriter, baseUrl, metadata, elem) {
|
|
6602
|
+
const modifiedPath = typeof rewriter === "function" ? rewriter(path3, metadata, elem) : path3;
|
|
6598
6603
|
return modifiedPath[0] === "/" && baseUrl ? trimCharacterEnd(baseUrl, "/") + modifiedPath : modifiedPath;
|
|
6599
6604
|
}
|
|
6600
6605
|
function formatImage(elem, walk, builder, formatOptions) {
|
|
@@ -6918,9 +6923,9 @@ function handleDeprecatedOptions(options) {
|
|
|
6918
6923
|
options.selectors.push(...tagDefinitions);
|
|
6919
6924
|
options.selectors = mergeDuplicatesPreferLast(options.selectors, ((s) => s.selector));
|
|
6920
6925
|
}
|
|
6921
|
-
function set(obj,
|
|
6922
|
-
const valueKey =
|
|
6923
|
-
for (const key of
|
|
6926
|
+
function set(obj, path3, value) {
|
|
6927
|
+
const valueKey = path3.pop();
|
|
6928
|
+
for (const key of path3) {
|
|
6924
6929
|
let nested = obj[key];
|
|
6925
6930
|
if (!nested) {
|
|
6926
6931
|
nested = {};
|
|
@@ -7011,7 +7016,7 @@ function preFetchHandler(event) {
|
|
|
7011
7016
|
if (cached !== null) {
|
|
7012
7017
|
const cachedBytes = Buffer.byteLength(cached, "utf-8");
|
|
7013
7018
|
if (cachedBytes >= loadConfig().hints.web_dedup_min_bytes) {
|
|
7014
|
-
recordStat("webfetch:recall", cachedBytes,
|
|
7019
|
+
recordStat("webfetch:recall", cachedBytes, savedTokensFromBytes(cachedBytes));
|
|
7015
7020
|
return denyOutput(
|
|
7016
7021
|
"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."
|
|
7017
7022
|
);
|
|
@@ -7031,29 +7036,13 @@ function postFetchHandler(event) {
|
|
|
7031
7036
|
const { toolInput, url } = urlCtx;
|
|
7032
7037
|
recordStat("web_fetch");
|
|
7033
7038
|
const body = extractToolResponse(event.raw);
|
|
7034
|
-
const
|
|
7035
|
-
const injectionMatches = injCfg.enabled && body ? scanForInjectionPatterns(body) : [];
|
|
7036
|
-
if (injectionMatches.length > 0) {
|
|
7037
|
-
recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
|
|
7038
|
-
}
|
|
7039
|
+
const injectionMatches = body ? scanAndRecord(body) : [];
|
|
7039
7040
|
const bodyRedacted = body ? redactSecrets(body) : { text: body, count: 0 };
|
|
7040
7041
|
if (!event.sessionId) {
|
|
7041
|
-
|
|
7042
|
-
return emitRewrite(fenceUntrustedContent(bodyRedacted.text, injectionMatches), "fetch");
|
|
7043
|
-
}
|
|
7044
|
-
if (bodyRedacted.count > 0) {
|
|
7045
|
-
return emitRewrite(bodyRedacted.text, "fetch");
|
|
7046
|
-
}
|
|
7047
|
-
return passOutput();
|
|
7042
|
+
return emitRewriteIfChanged(body, fenceWithMatches(bodyRedacted.text, injectionMatches), "fetch");
|
|
7048
7043
|
}
|
|
7049
7044
|
if (!body || body.length < 1024) {
|
|
7050
|
-
|
|
7051
|
-
return emitRewrite(fenceUntrustedContent(bodyRedacted.text, injectionMatches), "fetch");
|
|
7052
|
-
}
|
|
7053
|
-
if (bodyRedacted.count > 0) {
|
|
7054
|
-
return emitRewrite(bodyRedacted.text, "fetch");
|
|
7055
|
-
}
|
|
7056
|
-
return passOutput();
|
|
7045
|
+
return emitRewriteIfChanged(body, fenceWithMatches(bodyRedacted.text, injectionMatches), "fetch");
|
|
7057
7046
|
}
|
|
7058
7047
|
const prompt = typeof toolInput["prompt"] === "string" ? toolInput["prompt"] : "";
|
|
7059
7048
|
let storedBody = body;
|
|
@@ -7068,31 +7057,24 @@ function postFetchHandler(event) {
|
|
|
7068
7057
|
const cacheId = storeWebOutput(url, storedBody, `${url}\0${prompt}`, storedBody !== body ? body : void 0);
|
|
7069
7058
|
recordWebFetch(url, prompt, cacheId);
|
|
7070
7059
|
const storedRedacted = storedBody === body ? bodyRedacted : redactSecrets(storedBody);
|
|
7071
|
-
|
|
7072
|
-
return emitRewrite(fenceUntrustedContent(storedRedacted.text, injectionMatches), "fetch");
|
|
7073
|
-
}
|
|
7060
|
+
const fencedStored = fenceWithMatches(storedRedacted.text, injectionMatches);
|
|
7074
7061
|
if (storedBody !== body) {
|
|
7075
7062
|
const noticeFor = (id) => `
|
|
7076
7063
|
[token-goat: WebFetch body compressed via extractCleanText; use \`token-goat web-output ${id} --raw\` to recall it]`;
|
|
7077
7064
|
const notice = noticeFor(cacheId);
|
|
7078
7065
|
const noticeBytes = Buffer.byteLength(notice, "utf-8");
|
|
7079
7066
|
const originalBytes = Buffer.byteLength(body, "utf-8");
|
|
7080
|
-
const rewrittenBytes = Buffer.byteLength(
|
|
7067
|
+
const rewrittenBytes = Buffer.byteLength(fencedStored, "utf-8");
|
|
7081
7068
|
if (isRewriteWorthwhile({
|
|
7082
7069
|
originalBytes,
|
|
7083
7070
|
rewrittenBytes,
|
|
7084
7071
|
noticeBytes,
|
|
7085
7072
|
minNetSavingsBytes: resolveMinNetSavingsBytes()
|
|
7086
7073
|
})) {
|
|
7087
|
-
|
|
7088
|
-
recordStat("webfetch:compress", bytesDelta, Math.round(bytesDelta / 4));
|
|
7089
|
-
return emitRewrite(storedRedacted.text + notice, "fetch");
|
|
7074
|
+
return emitRewrite(fencedStored + notice, "fetch", { kind: "webfetch:compress", originalBytes });
|
|
7090
7075
|
}
|
|
7091
7076
|
}
|
|
7092
|
-
|
|
7093
|
-
return emitRewrite(storedRedacted.text, "fetch");
|
|
7094
|
-
}
|
|
7095
|
-
return passOutput();
|
|
7077
|
+
return emitRewriteIfChanged(body, fencedStored, "fetch");
|
|
7096
7078
|
} catch {
|
|
7097
7079
|
return passOutput();
|
|
7098
7080
|
}
|
|
@@ -7146,7 +7128,7 @@ async function preSkillHandler(event) {
|
|
|
7146
7128
|
if (await hasSessionOutput(event.sessionId, skillName)) {
|
|
7147
7129
|
const cachedBytes = await sessionOutputBodyBytes(event.sessionId, skillName);
|
|
7148
7130
|
const denyCredit = cachedBytes !== null ? Math.min(cachedBytes, PER_FILE_COUNTERFACTUAL_CEILING) : 0;
|
|
7149
|
-
recordStat("session_hint", denyCredit,
|
|
7131
|
+
recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit));
|
|
7150
7132
|
return denyOutput(
|
|
7151
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."
|
|
7152
7134
|
);
|
|
@@ -7161,7 +7143,7 @@ async function preSkillHandler(event) {
|
|
|
7161
7143
|
const compactBytes = Buffer.byteLength(compact, "utf-8");
|
|
7162
7144
|
if (compactBytes * 2 <= bodyBytes && compactBytes <= COMPACT_INLINE_MAX_BYTES) {
|
|
7163
7145
|
const savedBytes = bodyBytes - compactBytes;
|
|
7164
|
-
recordStat("skill_compact_inlined", savedBytes,
|
|
7146
|
+
recordStat("skill_compact_inlined", savedBytes, savedTokensFromBytes(savedBytes));
|
|
7165
7147
|
return denyOutput(
|
|
7166
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
|
|
7167
7149
|
);
|
|
@@ -7485,16 +7467,16 @@ function isTempPath(fp) {
|
|
|
7485
7467
|
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);
|
|
7486
7468
|
}
|
|
7487
7469
|
function isOrchestratorStateFile(filePath) {
|
|
7488
|
-
const
|
|
7489
|
-
return /^\.improve-state-/.test(
|
|
7470
|
+
const basename3 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
|
|
7471
|
+
return /^\.improve-state-/.test(basename3);
|
|
7490
7472
|
}
|
|
7491
7473
|
function extractCatSourceFile(cmd) {
|
|
7492
7474
|
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);
|
|
7493
7475
|
return m?.[1] ?? null;
|
|
7494
7476
|
}
|
|
7495
7477
|
function classifyFileExtensions(filePath) {
|
|
7496
|
-
const
|
|
7497
|
-
const isEnvFile = /^\.env(\.\w+)?$/i.test(
|
|
7478
|
+
const basename3 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
|
|
7479
|
+
const isEnvFile = /^\.env(\.\w+)?$/i.test(basename3);
|
|
7498
7480
|
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);
|
|
7499
7481
|
if (!hasKnownExt && !isEnvFile) return null;
|
|
7500
7482
|
const isSql = /\.sql$/i.test(filePath);
|
|
@@ -7516,12 +7498,14 @@ function classifyCatPath(filePath, cmd0) {
|
|
|
7516
7498
|
return { filePath, ...flags, cmd0 };
|
|
7517
7499
|
}
|
|
7518
7500
|
function extractCatFile(cmd) {
|
|
7519
|
-
const m = /^(cat|bat|type|Get-Content|gc)(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+-[a-zA-Z]
|
|
7501
|
+
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);
|
|
7520
7502
|
if (!m) return null;
|
|
7521
7503
|
const cmd0 = m[1];
|
|
7522
7504
|
const filePath = m[2] ?? m[3] ?? m[4];
|
|
7523
7505
|
if (filePath === void 0) return null;
|
|
7524
|
-
|
|
7506
|
+
const r = classifyCatPath(filePath, cmd0);
|
|
7507
|
+
if (r === null) return null;
|
|
7508
|
+
return { ...r, advisoryOnly: m[5] === "/dev/null" };
|
|
7525
7509
|
}
|
|
7526
7510
|
function extractCatFilesMulti(cmd) {
|
|
7527
7511
|
if (/[|<>;&`]/.test(cmd) || cmd.includes("$(")) return null;
|
|
@@ -7769,11 +7753,12 @@ function extractPythonFileRead(cmd) {
|
|
|
7769
7753
|
return null;
|
|
7770
7754
|
}
|
|
7771
7755
|
function extractHeadFile(cmd) {
|
|
7772
|
-
const
|
|
7773
|
-
|
|
7774
|
-
|
|
7756
|
+
const direct = /^head(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
|
|
7757
|
+
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;
|
|
7758
|
+
if (direct === null && piped === null) return null;
|
|
7759
|
+
const n = parseInt((direct !== null ? direct[1] ?? direct[2] : piped[4] ?? piped[5]) ?? "0", 10);
|
|
7775
7760
|
if (n <= 10) return null;
|
|
7776
|
-
const filePath =
|
|
7761
|
+
const filePath = direct !== null ? direct[3] ?? direct[4] ?? direct[5] : piped[1] ?? piped[2] ?? piped[3];
|
|
7777
7762
|
if (filePath === void 0) return null;
|
|
7778
7763
|
if (isTempPath(filePath)) return null;
|
|
7779
7764
|
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;
|
|
@@ -7781,12 +7766,12 @@ function extractHeadFile(cmd) {
|
|
|
7781
7766
|
return { filePath, isDoc, isConfig, isSql, n };
|
|
7782
7767
|
}
|
|
7783
7768
|
function extractSedRange(cmd) {
|
|
7784
|
-
const m = /^sed\s+-n\s+['"](?:\d+,\d+p)(?:;\d+,\d+p)*['"]\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2
|
|
7769
|
+
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);
|
|
7785
7770
|
if (!m) return null;
|
|
7786
|
-
const
|
|
7787
|
-
if (
|
|
7771
|
+
const addressList = m[1] ?? m[2];
|
|
7772
|
+
if (addressList === void 0) return null;
|
|
7788
7773
|
const ranges = [];
|
|
7789
|
-
for (const clause of
|
|
7774
|
+
for (const clause of addressList.split(";")) {
|
|
7790
7775
|
const cm = /^(\d+),(\d+)p$/.exec(clause ?? "");
|
|
7791
7776
|
if (!cm) return null;
|
|
7792
7777
|
const start = parseInt(cm[1], 10);
|
|
@@ -7795,19 +7780,21 @@ function extractSedRange(cmd) {
|
|
|
7795
7780
|
ranges.push([start, end]);
|
|
7796
7781
|
}
|
|
7797
7782
|
if (ranges.length === 0) return null;
|
|
7798
|
-
const filePath = m[
|
|
7783
|
+
const filePath = m[3] ?? m[4] ?? m[5];
|
|
7799
7784
|
if (filePath === void 0) return null;
|
|
7800
7785
|
if (isTempPath(filePath)) return null;
|
|
7801
7786
|
return { filePath, ranges };
|
|
7802
7787
|
}
|
|
7803
7788
|
function extractAwkRange(cmd) {
|
|
7804
|
-
const m = /^awk\s+(?:'([^']+)'|"([^"]+)")\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2
|
|
7789
|
+
const m = /^awk\s+(?:'([^']+)'|"([^"]+)")\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>(?:\/dev\/null|&1))?\s*$/.exec(cmd);
|
|
7805
7790
|
if (!m) return null;
|
|
7806
7791
|
const program = m[1] ?? m[2];
|
|
7807
7792
|
if (program === void 0) return null;
|
|
7808
|
-
const cmp = /^\s*NR\s*>=\s*(\d+)\s*&&\s*NR\s*<=\s*(\d+)\s*$/.exec(program);
|
|
7809
|
-
const rng = /^\s*NR\s*==\s*(\d+)\s*,\s*NR\s*==\s*(\d+)\s*$/.exec(program);
|
|
7793
|
+
const cmp = /^\s*NR\s*>=\s*(\d+)\s*&&\s*NR\s*<=\s*(\d+)\s*(\{.*\})?\s*$/.exec(program);
|
|
7794
|
+
const rng = /^\s*NR\s*==\s*(\d+)\s*,\s*NR\s*==\s*(\d+)\s*(\{.*\})?\s*$/.exec(program);
|
|
7810
7795
|
const hit = cmp ?? rng;
|
|
7796
|
+
const action = hit?.[3];
|
|
7797
|
+
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;
|
|
7811
7798
|
if (!hit) return null;
|
|
7812
7799
|
const start = parseInt(hit[1], 10);
|
|
7813
7800
|
const end = parseInt(hit[2], 10);
|
|
@@ -7817,13 +7804,47 @@ function extractAwkRange(cmd) {
|
|
|
7817
7804
|
if (isTempPath(filePath)) return null;
|
|
7818
7805
|
return { filePath, ranges: [[start, end]] };
|
|
7819
7806
|
}
|
|
7807
|
+
function normalizeCatSedPipe(cmd) {
|
|
7808
|
+
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);
|
|
7809
|
+
if (!m) return null;
|
|
7810
|
+
const filePath = m[1] ?? m[2] ?? m[3];
|
|
7811
|
+
if (filePath === void 0 || filePath.includes('"')) return null;
|
|
7812
|
+
return "sed -n " + m[4] + ' "' + filePath + '"';
|
|
7813
|
+
}
|
|
7820
7814
|
function extractLineRangeRead(cmd) {
|
|
7815
|
+
const catSed = normalizeCatSedPipe(cmd);
|
|
7816
|
+
if (catSed !== null) cmd = catSed;
|
|
7821
7817
|
const sed = extractSedRange(cmd);
|
|
7822
7818
|
if (sed !== null) return { ...sed, tool: "sed" };
|
|
7823
7819
|
const awk = extractAwkRange(cmd);
|
|
7824
7820
|
if (awk !== null) return { ...awk, tool: "awk" };
|
|
7825
7821
|
return null;
|
|
7826
7822
|
}
|
|
7823
|
+
var FORMATTING_STAGE_RE = /^(?:fold|cut|cat|nl|head|tail)(?:\s+(?:-\S+|[\d,+-]+))*\s*$/;
|
|
7824
|
+
function extractLineRangeReadsCompound(cmd) {
|
|
7825
|
+
const cleaned = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
|
|
7826
|
+
const segments = splitShellSegments(cleaned);
|
|
7827
|
+
if (segments.length < 2) return null;
|
|
7828
|
+
const reads = [];
|
|
7829
|
+
for (const seg of segments) {
|
|
7830
|
+
const r = extractLineRangeRead(seg);
|
|
7831
|
+
if (r !== null) {
|
|
7832
|
+
reads.push(r);
|
|
7833
|
+
continue;
|
|
7834
|
+
}
|
|
7835
|
+
if (/^echo\b[^<>]*$/.test(seg)) continue;
|
|
7836
|
+
if (FORMATTING_STAGE_RE.test(seg)) continue;
|
|
7837
|
+
return null;
|
|
7838
|
+
}
|
|
7839
|
+
if (reads.length === 0) return null;
|
|
7840
|
+
const merged = /* @__PURE__ */ new Map();
|
|
7841
|
+
for (const r of reads) {
|
|
7842
|
+
const prev = merged.get(r.filePath);
|
|
7843
|
+
if (prev !== void 0) prev.ranges.push(...r.ranges);
|
|
7844
|
+
else merged.set(r.filePath, { filePath: r.filePath, ranges: [...r.ranges], tool: r.tool });
|
|
7845
|
+
}
|
|
7846
|
+
return [...merged.values()];
|
|
7847
|
+
}
|
|
7827
7848
|
var SYMBOL_BEARING_LANGUAGES = /* @__PURE__ */ new Set([
|
|
7828
7849
|
"python",
|
|
7829
7850
|
"typescript",
|
|
@@ -7923,11 +7944,12 @@ function extractTailFile(cmd) {
|
|
|
7923
7944
|
if (/-f\b/.test(cmd)) return null;
|
|
7924
7945
|
if (/-c\b/.test(cmd)) return null;
|
|
7925
7946
|
if (/-n\s*\+/.test(cmd)) return null;
|
|
7926
|
-
const
|
|
7927
|
-
|
|
7928
|
-
|
|
7947
|
+
const direct = /^tail(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
|
|
7948
|
+
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;
|
|
7949
|
+
if (direct === null && piped === null) return null;
|
|
7950
|
+
const n = parseInt((direct !== null ? direct[1] ?? direct[2] : piped[4] ?? piped[5]) ?? "0", 10);
|
|
7929
7951
|
if (n <= 10) return null;
|
|
7930
|
-
const filePath =
|
|
7952
|
+
const filePath = direct !== null ? direct[3] ?? direct[4] ?? direct[5] : piped[1] ?? piped[2] ?? piped[3];
|
|
7931
7953
|
if (!filePath) return null;
|
|
7932
7954
|
if (isTempPath(filePath)) return null;
|
|
7933
7955
|
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;
|
|
@@ -8381,10 +8403,19 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
|
|
|
8381
8403
|
});
|
|
8382
8404
|
const minNet = resolveMinNetSavingsBytes();
|
|
8383
8405
|
if (!compressed.worthApplying(minNet)) return null;
|
|
8384
|
-
const id = await
|
|
8385
|
-
recordStat("bash_compress:generic", compressed.bytesSaved, compressed.tokensSaved);
|
|
8406
|
+
const id = await commandHash(cmd, cwd);
|
|
8386
8407
|
const body = compressed.withMarker(minNet) + "\n[token-goat] full output: bash-output " + id + " --full";
|
|
8387
|
-
|
|
8408
|
+
const emittedBytes = Buffer.byteLength(body, "utf-8");
|
|
8409
|
+
if (!isRewriteWorthwhile({
|
|
8410
|
+
originalBytes: compressed.originalBytes,
|
|
8411
|
+
rewrittenBytes: emittedBytes,
|
|
8412
|
+
noticeBytes: 0,
|
|
8413
|
+
minNetSavingsBytes: minNet
|
|
8414
|
+
})) {
|
|
8415
|
+
return null;
|
|
8416
|
+
}
|
|
8417
|
+
await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
|
|
8418
|
+
return emitRewrite(body, "bash", { kind: "bash_compress:generic", originalBytes: compressed.originalBytes });
|
|
8388
8419
|
}
|
|
8389
8420
|
function unwrapCompressCommand(executed) {
|
|
8390
8421
|
const t = executed.trim();
|
|
@@ -8610,25 +8641,28 @@ function preBashHandlerInner(event) {
|
|
|
8610
8641
|
"Use `token-goat outline <file>` to see symbol names and line counts without loading files."
|
|
8611
8642
|
);
|
|
8612
8643
|
}
|
|
8613
|
-
const
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
|
|
8644
|
+
const singleLineRangeRead = extractLineRangeRead(cmd);
|
|
8645
|
+
const sedReads = singleLineRangeRead !== null ? [singleLineRangeRead] : extractLineRangeReadsCompound(cmd);
|
|
8646
|
+
if (sedReads !== null) {
|
|
8617
8647
|
recordStat("session_hint", 0, 0);
|
|
8618
|
-
const
|
|
8619
|
-
const
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
const
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8648
|
+
const hints = [];
|
|
8649
|
+
for (const { filePath, ranges, tool } of sedReads) {
|
|
8650
|
+
const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
|
|
8651
|
+
const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
|
|
8652
|
+
const overlapHints = [];
|
|
8653
|
+
const freshRanges = [];
|
|
8654
|
+
for (const [start, end] of ranges) {
|
|
8655
|
+
const priorOverlap = findRangeOverlap(getFileLineRanges(sedDedupKey), start, end);
|
|
8656
|
+
recordFileLineRange(sedDedupKey, start, end);
|
|
8657
|
+
if (priorOverlap !== null) {
|
|
8658
|
+
overlapHints.push(sedOverlapHint(hintPath, priorOverlap, start, end));
|
|
8659
|
+
} else {
|
|
8660
|
+
freshRanges.push([start, end]);
|
|
8661
|
+
}
|
|
8628
8662
|
}
|
|
8663
|
+
hints.push(...overlapHints);
|
|
8664
|
+
if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
|
|
8629
8665
|
}
|
|
8630
|
-
const hints = [...overlapHints];
|
|
8631
|
-
if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
|
|
8632
8666
|
return contextOutput(hints.join(" "));
|
|
8633
8667
|
}
|
|
8634
8668
|
const catJsonPipe = extractCatJsonPipe(cmd);
|
|
@@ -8642,7 +8676,7 @@ function preBashHandlerInner(event) {
|
|
|
8642
8676
|
}
|
|
8643
8677
|
const catResult = extractCatFile(cmd);
|
|
8644
8678
|
if (catResult !== null) {
|
|
8645
|
-
const { filePath, isDoc, isEnv, isConfig, isSql, cmd0 } = catResult;
|
|
8679
|
+
const { filePath, isDoc, isEnv, isConfig, isSql, cmd0, advisoryOnly } = catResult;
|
|
8646
8680
|
const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
|
|
8647
8681
|
recordStat("session_hint", 0, 0);
|
|
8648
8682
|
if (isSql) {
|
|
@@ -8651,7 +8685,7 @@ function preBashHandlerInner(event) {
|
|
|
8651
8685
|
);
|
|
8652
8686
|
}
|
|
8653
8687
|
const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc);
|
|
8654
|
-
return cdStripped ? contextOutput("`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
|
|
8688
|
+
return cdStripped || advisoryOnly ? contextOutput("`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
|
|
8655
8689
|
}
|
|
8656
8690
|
const catMulti = extractCatFilesMulti(cmd);
|
|
8657
8691
|
if (catMulti !== null) {
|
|
@@ -8790,13 +8824,13 @@ function preBashHandlerInner(event) {
|
|
|
8790
8824
|
const monBytes = monEntry.sizeBytes;
|
|
8791
8825
|
const catFile = extractCatSourceFile(cmd);
|
|
8792
8826
|
if (catFile !== null) {
|
|
8793
|
-
recordStat("bash_compress:recall", monBytes,
|
|
8827
|
+
recordStat("bash_compress:recall", monBytes, savedTokensFromBytes(monBytes));
|
|
8794
8828
|
return contextOutput(
|
|
8795
8829
|
"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."
|
|
8796
8830
|
);
|
|
8797
8831
|
}
|
|
8798
8832
|
const cmdSummary = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
8799
|
-
recordStat("bash_compress:recall", monBytes,
|
|
8833
|
+
recordStat("bash_compress:recall", monBytes, savedTokensFromBytes(monBytes));
|
|
8800
8834
|
return contextOutput(
|
|
8801
8835
|
"Prior output from `" + cmdSummary + "`" + pipelineDivergenceNote(cmd, monEntry.command) + " is cached.\nUse `token-goat bash-output " + monOutputId + " " + monitoringHint + "` to re-inspect without re-running."
|
|
8802
8836
|
);
|
|
@@ -8823,7 +8857,7 @@ function preBashHandlerInner(event) {
|
|
|
8823
8857
|
const curlEntry = curlEntryRaw !== null && !isBashEntryStale(curlEntryRaw, cmd, preHookCwd) ? curlEntryRaw : null;
|
|
8824
8858
|
if (curlOutputId !== null && curlEntry !== null && curlEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(curlEntry.sizeBytes)) {
|
|
8825
8859
|
const curlBytes = curlEntry.sizeBytes;
|
|
8826
|
-
recordStat("bash_compress:recall", curlBytes,
|
|
8860
|
+
recordStat("bash_compress:recall", curlBytes, savedTokensFromBytes(curlBytes));
|
|
8827
8861
|
const curlPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
8828
8862
|
return contextOutput(
|
|
8829
8863
|
"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."
|
|
@@ -8837,7 +8871,7 @@ function preBashHandlerInner(event) {
|
|
|
8837
8871
|
const ghEntry = ghEntryRaw !== null && !isBashEntryStale(ghEntryRaw, cmd, preHookCwd) ? ghEntryRaw : null;
|
|
8838
8872
|
if (ghOutputId !== null && ghEntry !== null && ghEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(ghEntry.sizeBytes)) {
|
|
8839
8873
|
const ghBytes = ghEntry.sizeBytes;
|
|
8840
|
-
recordStat("bash_compress:recall", ghBytes,
|
|
8874
|
+
recordStat("bash_compress:recall", ghBytes, savedTokensFromBytes(ghBytes));
|
|
8841
8875
|
const ghPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
8842
8876
|
return contextOutput(
|
|
8843
8877
|
"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."
|
|
@@ -8851,7 +8885,7 @@ function preBashHandlerInner(event) {
|
|
|
8851
8885
|
const gitScopedEntry = gitScopedEntryRaw !== null && !isBashEntryStale(gitScopedEntryRaw, cmd, preHookCwd) ? gitScopedEntryRaw : null;
|
|
8852
8886
|
if (gitScopedOutputId !== null && gitScopedEntry !== null && gitScopedEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(gitScopedEntry.sizeBytes)) {
|
|
8853
8887
|
const gitScopedBytes = gitScopedEntry.sizeBytes;
|
|
8854
|
-
recordStat("bash_compress:recall", gitScopedBytes,
|
|
8888
|
+
recordStat("bash_compress:recall", gitScopedBytes, savedTokensFromBytes(gitScopedBytes));
|
|
8855
8889
|
const gitScopedPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
8856
8890
|
return contextOutput(
|
|
8857
8891
|
"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."
|
|
@@ -8893,7 +8927,7 @@ function preBashHandlerInner(event) {
|
|
|
8893
8927
|
const entry = entryRaw !== null && !isBashEntryStale(entryRaw, cmd, preHookCwd) ? entryRaw : null;
|
|
8894
8928
|
if (outputId !== null && entry !== null && entry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(entry.sizeBytes)) {
|
|
8895
8929
|
const bytes = entry.sizeBytes;
|
|
8896
|
-
recordStat("bash_compress:recall", bytes,
|
|
8930
|
+
recordStat("bash_compress:recall", bytes, savedTokensFromBytes(bytes));
|
|
8897
8931
|
return contextOutput(buildRecallHint(cmd, outputId));
|
|
8898
8932
|
}
|
|
8899
8933
|
return maybeCompressRewrite(event, rawCmd, cmd) ?? passOutput();
|
|
@@ -9189,12 +9223,23 @@ function getTaskId(toolInput) {
|
|
|
9189
9223
|
const value = toolInput["task_id"];
|
|
9190
9224
|
return typeof value === "string" && value !== "" ? value : void 0;
|
|
9191
9225
|
}
|
|
9226
|
+
function extractTaskOutputText(raw) {
|
|
9227
|
+
const tr = raw["tool_response"];
|
|
9228
|
+
if (tr !== null && typeof tr === "object" && !Array.isArray(tr)) {
|
|
9229
|
+
const task = tr["task"];
|
|
9230
|
+
if (task !== null && typeof task === "object" && !Array.isArray(task)) {
|
|
9231
|
+
const output = task["output"];
|
|
9232
|
+
if (typeof output === "string") return output;
|
|
9233
|
+
}
|
|
9234
|
+
}
|
|
9235
|
+
return extractToolResultText(raw);
|
|
9236
|
+
}
|
|
9192
9237
|
function postTaskOutputHandler(event) {
|
|
9193
9238
|
try {
|
|
9194
9239
|
if (getToolName(event) !== "TaskOutput" || !event.sessionId) return passOutput();
|
|
9195
9240
|
const taskId = getTaskId(getToolInput(event));
|
|
9196
9241
|
if (taskId === void 0) return passOutput();
|
|
9197
|
-
const rawOutput =
|
|
9242
|
+
const rawOutput = extractTaskOutputText(event.raw);
|
|
9198
9243
|
if (!rawOutput) return passOutput();
|
|
9199
9244
|
const output = redactSecrets(rawOutput).text;
|
|
9200
9245
|
if (!output) return passOutput();
|
|
@@ -9366,10 +9411,10 @@ ${PLAN_OMIT_POINTER}`;
|
|
|
9366
9411
|
})) {
|
|
9367
9412
|
return passOutput();
|
|
9368
9413
|
}
|
|
9369
|
-
return {
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
};
|
|
9414
|
+
return emitRewrite(`${prefix}${notice}`, "exitplanmode", {
|
|
9415
|
+
kind: "plan_echo_collapse",
|
|
9416
|
+
originalBytes: Buffer.byteLength(output, "utf-8")
|
|
9417
|
+
});
|
|
9373
9418
|
} catch {
|
|
9374
9419
|
return passOutput();
|
|
9375
9420
|
}
|
|
@@ -9437,6 +9482,9 @@ function storeMcpOutput(sessionId, toolName, toolInput, resultText) {
|
|
|
9437
9482
|
${redactedOutput}`, entry.storedAt);
|
|
9438
9483
|
return id;
|
|
9439
9484
|
}
|
|
9485
|
+
function mcpOutputBytes(id) {
|
|
9486
|
+
return getBashOutput(id)?.sizeBytes ?? 0;
|
|
9487
|
+
}
|
|
9440
9488
|
function getMcpOutput(sessionId, toolName, toolInput, ttlMs = Number.POSITIVE_INFINITY) {
|
|
9441
9489
|
if (!sessionId) return null;
|
|
9442
9490
|
const id = mcpOutputId(sessionId, mcpHash(toolName, toolInput));
|
|
@@ -9759,6 +9807,8 @@ function preMcpHandler(event) {
|
|
|
9759
9807
|
const ttlMs = loadConfig().hints.mcp_dedup_ttl_secs * 1e3;
|
|
9760
9808
|
const id = getMcpOutput(event.sessionId, toolName, toolInput, ttlMs);
|
|
9761
9809
|
if (!id) return passOutput();
|
|
9810
|
+
const denyCredit = Math.min(mcpOutputBytes(id), PER_FILE_COUNTERFACTUAL_CEILING);
|
|
9811
|
+
recordStat("mcp:recall", denyCredit, savedTokensFromBytes(denyCredit));
|
|
9762
9812
|
return denyOutput(
|
|
9763
9813
|
"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."
|
|
9764
9814
|
);
|
|
@@ -9769,23 +9819,14 @@ function postMcpHandler(event) {
|
|
|
9769
9819
|
const toolInput = getToolInput(event);
|
|
9770
9820
|
const resultText = extractToolResultText(event.raw);
|
|
9771
9821
|
if (!resultText) return passOutput();
|
|
9772
|
-
|
|
9773
|
-
try {
|
|
9774
|
-
if (loadConfig().injection.enabled) injectionMatches = scanForInjectionPatterns(resultText);
|
|
9775
|
-
} catch {
|
|
9776
|
-
injectionMatches = [];
|
|
9777
|
-
}
|
|
9778
|
-
if (injectionMatches.length > 0) recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
|
|
9822
|
+
const injectionMatches = scanAndRecord(resultText);
|
|
9779
9823
|
const redactedResult = redactSecrets(resultText);
|
|
9780
|
-
const fenced = () =>
|
|
9781
|
-
|
|
9824
|
+
const fenced = () => emitRewriteIfChanged(
|
|
9825
|
+
resultText,
|
|
9826
|
+
fenceWithMatches(redactedResult.text, injectionMatches, UNTRUSTED_TOOL_TAG),
|
|
9782
9827
|
"mcp"
|
|
9783
9828
|
);
|
|
9784
|
-
const passOrFence = () =>
|
|
9785
|
-
if (injectionMatches.length > 0) return fenced();
|
|
9786
|
-
if (redactedResult.count > 0) return emitRewrite(redactedResult.text, "mcp");
|
|
9787
|
-
return passOutput();
|
|
9788
|
-
};
|
|
9829
|
+
const passOrFence = () => fenced();
|
|
9789
9830
|
if (!event.sessionId) return passOrFence();
|
|
9790
9831
|
if (isMcpErrorResponse(event.raw)) return passOrFence();
|
|
9791
9832
|
const readOnly = isMcpReadOnly(toolName, toolInput);
|
|
@@ -9803,16 +9844,18 @@ function postMcpHandler(event) {
|
|
|
9803
9844
|
const redactedBody = redactSecrets(compressed).text;
|
|
9804
9845
|
const notice = `[token-goat: compressed, full via mcp-output ${id}]
|
|
9805
9846
|
`;
|
|
9847
|
+
const originalBytes = Buffer.byteLength(resultText, "utf-8");
|
|
9806
9848
|
const worthwhile = isRewriteWorthwhile({
|
|
9807
|
-
originalBytes
|
|
9849
|
+
originalBytes,
|
|
9808
9850
|
rewrittenBytes: Buffer.byteLength(redactedBody, "utf-8"),
|
|
9809
9851
|
noticeBytes: Buffer.byteLength(notice, "utf-8"),
|
|
9810
9852
|
minNetSavingsBytes: resolveMinNetSavingsBytes()
|
|
9811
9853
|
});
|
|
9812
9854
|
if (worthwhile) {
|
|
9813
9855
|
return emitRewrite(
|
|
9814
|
-
|
|
9815
|
-
"mcp"
|
|
9856
|
+
`${notice}${fenceWithMatches(redactedBody, injectionMatches, UNTRUSTED_TOOL_TAG)}`,
|
|
9857
|
+
"mcp",
|
|
9858
|
+
{ kind: "mcp:compress", originalBytes }
|
|
9816
9859
|
);
|
|
9817
9860
|
}
|
|
9818
9861
|
}
|
|
@@ -9862,23 +9905,9 @@ function postWebSearchHandler(event) {
|
|
|
9862
9905
|
if (getToolName(event) !== "WebSearch") return passOutput();
|
|
9863
9906
|
const resultText = extractToolResultText(event.raw);
|
|
9864
9907
|
if (!resultText) return passOutput();
|
|
9865
|
-
|
|
9866
|
-
try {
|
|
9867
|
-
if (loadConfig().injection.enabled) injectionMatches = scanForInjectionPatterns(resultText);
|
|
9868
|
-
} catch {
|
|
9869
|
-
injectionMatches = [];
|
|
9870
|
-
}
|
|
9871
|
-
if (injectionMatches.length > 0) recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
|
|
9908
|
+
const injectionMatches = scanAndRecord(resultText);
|
|
9872
9909
|
const redacted = redactSecrets(resultText);
|
|
9873
|
-
const passOrFence = () =>
|
|
9874
|
-
if (injectionMatches.length > 0) {
|
|
9875
|
-
return emitRewrite(fenceUntrustedContent(redacted.text, injectionMatches), "websearch");
|
|
9876
|
-
}
|
|
9877
|
-
if (redacted.count > 0) {
|
|
9878
|
-
return emitRewrite(redacted.text, "websearch");
|
|
9879
|
-
}
|
|
9880
|
-
return passOutput();
|
|
9881
|
-
};
|
|
9910
|
+
const passOrFence = () => emitRewriteIfChanged(resultText, fenceWithMatches(redacted.text, injectionMatches), "websearch");
|
|
9882
9911
|
if (!event.sessionId) return passOrFence();
|
|
9883
9912
|
const signature = webSearchSignatureInput(getToolInput(event));
|
|
9884
9913
|
if (signature === null) return passOrFence();
|
|
@@ -9934,13 +9963,15 @@ async function shrinkImageBlock(block) {
|
|
|
9934
9963
|
const mediaType = typeof source?.media_type === "string" ? source.media_type : "image/png";
|
|
9935
9964
|
const data = typeof source?.data === "string" ? source.data : "";
|
|
9936
9965
|
const originalDataUrl = `data:${mediaType};base64,${data}`;
|
|
9937
|
-
if (source?.type !== "base64" || data === "") return { text: originalDataUrl, changed: false, savedBytes: 0 };
|
|
9966
|
+
if (source?.type !== "base64" || data === "") return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
|
|
9938
9967
|
let buffer;
|
|
9939
9968
|
try {
|
|
9940
9969
|
buffer = Buffer.from(data, "base64");
|
|
9941
9970
|
} catch {
|
|
9942
|
-
return { text: originalDataUrl, changed: false, savedBytes: 0 };
|
|
9971
|
+
return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
|
|
9943
9972
|
}
|
|
9973
|
+
const meta = await probeImageMeta(buffer);
|
|
9974
|
+
const tier = loadConfig().image_shrink.vision_tier;
|
|
9944
9975
|
const fingerprint = screenshotFingerprint(data);
|
|
9945
9976
|
const alreadyShown = hasSeenImage(fingerprint);
|
|
9946
9977
|
recordSeenImage(fingerprint);
|
|
@@ -9955,16 +9986,23 @@ async function shrinkImageBlock(block) {
|
|
|
9955
9986
|
return {
|
|
9956
9987
|
text: SCREENSHOT_REPEAT_NOTICE,
|
|
9957
9988
|
changed: true,
|
|
9958
|
-
|
|
9989
|
+
// 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.
|
|
9990
|
+
savedBytes: buffer.length - Buffer.byteLength(SCREENSHOT_REPEAT_NOTICE, "utf-8"),
|
|
9991
|
+
// The whole image is withheld here rather than resized, so the visual tokens saved are its
|
|
9992
|
+
// entire billed cost less the notice standing in for it. Billed cost, not raw patch count:
|
|
9993
|
+
// an oversized screenshot would have been capped by the API's own downscale, so pricing the
|
|
9994
|
+
// untouched dimensions would credit a bill that was never going to be sent.
|
|
9995
|
+
savedTokens: Math.max(0, visionTokens(meta?.width ?? 0, meta?.height ?? 0, tier) - savedTokensFromBytes(Buffer.byteLength(SCREENSHOT_REPEAT_NOTICE, "utf-8")))
|
|
9959
9996
|
};
|
|
9960
9997
|
}
|
|
9961
9998
|
}
|
|
9962
9999
|
const result = await imageQualifiesForShrink(buffer) ? await shrinkImage(buffer, { sizeThresholdBytes: 0 }) : null;
|
|
9963
|
-
if (result === null) return { text: originalDataUrl, changed: false, savedBytes: 0 };
|
|
10000
|
+
if (result === null) return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
|
|
9964
10001
|
const saved = result.originalBytes - result.shrunkBytes;
|
|
9965
10002
|
const { summary, dataUrl } = formatShrinkSummary(result, "an inline browser screenshot");
|
|
10003
|
+
const savedTokens = visionTokensSaved(result.originalWidth, result.originalHeight, result.width, result.height, tier);
|
|
9966
10004
|
return { text: `${summary}
|
|
9967
|
-
${dataUrl}`, changed: true, savedBytes: saved };
|
|
10005
|
+
${dataUrl}`, changed: true, savedBytes: saved, savedTokens };
|
|
9968
10006
|
}
|
|
9969
10007
|
var TAB_CONTEXT_UNCHANGED_NOTICE = "(tabs unchanged since last check)";
|
|
9970
10008
|
function dedupTabContext(text) {
|
|
@@ -9992,15 +10030,19 @@ async function postBrowserImageHandler(event) {
|
|
|
9992
10030
|
const parts = [];
|
|
9993
10031
|
for (const block of blocks) {
|
|
9994
10032
|
if (block.type === "image") {
|
|
9995
|
-
const { text, changed, savedBytes } = await shrinkImageBlock(block);
|
|
10033
|
+
const { text, changed, savedBytes, savedTokens } = await shrinkImageBlock(block);
|
|
9996
10034
|
if (changed) {
|
|
9997
10035
|
anyChanged = true;
|
|
9998
|
-
recordStat("image_shrink", savedBytes,
|
|
10036
|
+
recordStat("image_shrink", savedBytes, savedTokens, void 0, toolName);
|
|
9999
10037
|
}
|
|
10000
10038
|
parts.push(text);
|
|
10001
10039
|
} else if (block.type === "text" && typeof block.text === "string") {
|
|
10002
10040
|
const { text, changed } = dedupTabContext(block.text);
|
|
10003
|
-
if (changed)
|
|
10041
|
+
if (changed) {
|
|
10042
|
+
anyChanged = true;
|
|
10043
|
+
const savedBytes = Buffer.byteLength(block.text, "utf-8") - Buffer.byteLength(text, "utf-8");
|
|
10044
|
+
if (savedBytes > 0) recordStat("browser_tab_dedup", savedBytes, savedTokensFromBytes(savedBytes), void 0, toolName);
|
|
10045
|
+
}
|
|
10004
10046
|
parts.push(text);
|
|
10005
10047
|
} else {
|
|
10006
10048
|
try {
|
|
@@ -10011,7 +10053,7 @@ async function postBrowserImageHandler(event) {
|
|
|
10011
10053
|
}
|
|
10012
10054
|
}
|
|
10013
10055
|
if (!anyChanged) return passOutput();
|
|
10014
|
-
return
|
|
10056
|
+
return emitRewrite(parts.join("\n"), toolName);
|
|
10015
10057
|
} catch {
|
|
10016
10058
|
return passOutput();
|
|
10017
10059
|
}
|
|
@@ -10020,6 +10062,9 @@ registerHook("post_tool_use", postBrowserImageHandler, { toolPattern: "^mcp__" }
|
|
|
10020
10062
|
|
|
10021
10063
|
// src/hooks_agent_spawn.ts
|
|
10022
10064
|
import { createHash as createHash2 } from "node:crypto";
|
|
10065
|
+
import * as fs from "node:fs";
|
|
10066
|
+
import * as os from "node:os";
|
|
10067
|
+
import * as path2 from "node:path";
|
|
10023
10068
|
var BRIEFING_TARGET_TOKENS = 550;
|
|
10024
10069
|
function buildSubagentBriefing() {
|
|
10025
10070
|
try {
|
|
@@ -10091,8 +10136,11 @@ function findDuplicateOutstandingPrompt(prompt) {
|
|
|
10091
10136
|
function truncateForWarning(text, max) {
|
|
10092
10137
|
return text.length > max ? text.slice(0, max) + "..." : text;
|
|
10093
10138
|
}
|
|
10139
|
+
function isAgentTool(toolName) {
|
|
10140
|
+
return toolName === "Agent" || toolName === "task" || toolName === "Task";
|
|
10141
|
+
}
|
|
10094
10142
|
function preAgentHandler(event) {
|
|
10095
|
-
if (event.toolName
|
|
10143
|
+
if (!isAgentTool(event.toolName)) return passOutput();
|
|
10096
10144
|
const toolInput = event.toolInput;
|
|
10097
10145
|
const prompt = toolInput["prompt"];
|
|
10098
10146
|
if (typeof prompt !== "string" || prompt.trim() === "") return passOutput();
|
|
@@ -10114,7 +10162,7 @@ function preAgentHandler(event) {
|
|
|
10114
10162
|
return passOutput();
|
|
10115
10163
|
}
|
|
10116
10164
|
}
|
|
10117
|
-
var FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})(
|
|
10165
|
+
var FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})([^\n]*)$/;
|
|
10118
10166
|
function findFencedBlockLines(lines) {
|
|
10119
10167
|
const blocks = [];
|
|
10120
10168
|
let fenceStart = -1;
|
|
@@ -10214,9 +10262,100 @@ function collapseBlankRunsInFences(text) {
|
|
|
10214
10262
|
out.push(...lines.slice(cursor));
|
|
10215
10263
|
return changedAny ? out.join("\n") : text;
|
|
10216
10264
|
}
|
|
10265
|
+
var SPAWN_RESTRICT_HINT_KEY = "agent-spawn-restrict-hint";
|
|
10266
|
+
var SPAWN_RESTRICT_MAX_NAMES = 3;
|
|
10267
|
+
var ROSTER_WALK_MAX_DEPTH = 4;
|
|
10268
|
+
var ROSTER_WALK_MAX_FILES = 400;
|
|
10269
|
+
function parseAgentDefinition(text, fallbackName) {
|
|
10270
|
+
const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
|
|
10271
|
+
if (!fmMatch) return null;
|
|
10272
|
+
const fm = fmMatch[1];
|
|
10273
|
+
const nameMatch = /^name:(.*)$/m.exec(fm);
|
|
10274
|
+
const rawName = nameMatch ? nameMatch[1].trim().replace(/^["']|["']$/g, "") : "";
|
|
10275
|
+
const name2 = rawName !== "" ? rawName : fallbackName;
|
|
10276
|
+
const toolsMatch = /^tools:(.*)$/m.exec(fm);
|
|
10277
|
+
if (!toolsMatch) return { name: name2, restricted: false };
|
|
10278
|
+
const inline = toolsMatch[1].trim();
|
|
10279
|
+
if (inline === "*" || inline === '"*"' || inline === "'*'") return { name: name2, restricted: false };
|
|
10280
|
+
if (inline !== "") return { name: name2, restricted: true };
|
|
10281
|
+
const after = fm.slice(toolsMatch.index + toolsMatch[0].length);
|
|
10282
|
+
for (const line of after.split(/\r?\n/)) {
|
|
10283
|
+
if (/^\s+-\s*\S/.test(line)) return { name: name2, restricted: true };
|
|
10284
|
+
if (/^\S/.test(line)) break;
|
|
10285
|
+
}
|
|
10286
|
+
return { name: name2, restricted: false };
|
|
10287
|
+
}
|
|
10288
|
+
function findRestrictedAgentNames(roots) {
|
|
10289
|
+
const scanRoots = roots ?? [path2.join(os.homedir(), ".claude", "agents")];
|
|
10290
|
+
const names = /* @__PURE__ */ new Set();
|
|
10291
|
+
const visited = /* @__PURE__ */ new Set();
|
|
10292
|
+
let filesSeen = 0;
|
|
10293
|
+
const walk = (dir, depth) => {
|
|
10294
|
+
if (depth > ROSTER_WALK_MAX_DEPTH || filesSeen >= ROSTER_WALK_MAX_FILES) return;
|
|
10295
|
+
let real;
|
|
10296
|
+
try {
|
|
10297
|
+
real = fs.realpathSync(dir);
|
|
10298
|
+
} catch {
|
|
10299
|
+
return;
|
|
10300
|
+
}
|
|
10301
|
+
const key = process.platform === "win32" ? real.toLocaleLowerCase() : real;
|
|
10302
|
+
if (visited.has(key)) return;
|
|
10303
|
+
visited.add(key);
|
|
10304
|
+
let entries;
|
|
10305
|
+
try {
|
|
10306
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
10307
|
+
} catch {
|
|
10308
|
+
return;
|
|
10309
|
+
}
|
|
10310
|
+
for (const entry of entries) {
|
|
10311
|
+
if (filesSeen >= ROSTER_WALK_MAX_FILES) return;
|
|
10312
|
+
const full = path2.join(dir, entry.name);
|
|
10313
|
+
let stat;
|
|
10314
|
+
try {
|
|
10315
|
+
stat = fs.statSync(full);
|
|
10316
|
+
} catch {
|
|
10317
|
+
continue;
|
|
10318
|
+
}
|
|
10319
|
+
if (stat.isDirectory()) {
|
|
10320
|
+
walk(full, depth + 1);
|
|
10321
|
+
continue;
|
|
10322
|
+
}
|
|
10323
|
+
if (!stat.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
|
|
10324
|
+
filesSeen++;
|
|
10325
|
+
let text;
|
|
10326
|
+
try {
|
|
10327
|
+
text = fs.readFileSync(full, "utf-8");
|
|
10328
|
+
} catch {
|
|
10329
|
+
continue;
|
|
10330
|
+
}
|
|
10331
|
+
const parsed = parseAgentDefinition(text, path2.basename(entry.name, path2.extname(entry.name)));
|
|
10332
|
+
if (parsed !== null && parsed.restricted) names.add(parsed.name);
|
|
10333
|
+
}
|
|
10334
|
+
};
|
|
10335
|
+
for (const root of scanRoots) walk(root, 0);
|
|
10336
|
+
return Array.from(names).sort();
|
|
10337
|
+
}
|
|
10338
|
+
function buildUnrestrictedSpawnAdvisory(toolInput) {
|
|
10339
|
+
try {
|
|
10340
|
+
if (getHarnessName() === "copilot_cli") return "";
|
|
10341
|
+
const rawType = toolInput["subagent_type"];
|
|
10342
|
+
const spawnType = typeof rawType === "string" ? rawType.trim() : "";
|
|
10343
|
+
if (spawnType !== "" && spawnType !== "general-purpose") return "";
|
|
10344
|
+
if (wasHintShown(SPAWN_RESTRICT_HINT_KEY)) return "";
|
|
10345
|
+
const names = findRestrictedAgentNames();
|
|
10346
|
+
if (names.length === 0) return "";
|
|
10347
|
+
markHintShown(SPAWN_RESTRICT_HINT_KEY);
|
|
10348
|
+
recordStat("session_hint", 0, 0, void 0, "agent-spawn-restrict");
|
|
10349
|
+
const shown = names.slice(0, SPAWN_RESTRICT_MAX_NAMES).join(", ");
|
|
10350
|
+
const more = names.length > SPAWN_RESTRICT_MAX_NAMES ? ` and ${names.length - SPAWN_RESTRICT_MAX_NAMES} more` : "";
|
|
10351
|
+
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.`;
|
|
10352
|
+
} catch {
|
|
10353
|
+
return "";
|
|
10354
|
+
}
|
|
10355
|
+
}
|
|
10217
10356
|
function postAgentHandler(event) {
|
|
10218
10357
|
try {
|
|
10219
|
-
if (event.toolName
|
|
10358
|
+
if (!isAgentTool(event.toolName) || !event.sessionId) return passOutput();
|
|
10220
10359
|
const finishedPrompt = event.toolInput["prompt"];
|
|
10221
10360
|
if (typeof finishedPrompt === "string" && finishedPrompt !== "") {
|
|
10222
10361
|
removeOutstandingAgentSpawn(finishedPrompt);
|
|
@@ -10224,13 +10363,17 @@ function postAgentHandler(event) {
|
|
|
10224
10363
|
const redactedReport = redactSecrets(extractToolResultText(event.raw));
|
|
10225
10364
|
const resultText = redactedReport.text;
|
|
10226
10365
|
const agentReportCfg = loadConfig().agent_report;
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10366
|
+
const spawnAdvisory = buildUnrestrictedSpawnAdvisory(event.toolInput);
|
|
10367
|
+
if (!resultText || resultText.length < agentReportCfg.min_bytes) {
|
|
10368
|
+
return spawnAdvisory === "" ? passOutput() : contextOutput(spawnAdvisory);
|
|
10369
|
+
}
|
|
10370
|
+
const id = storeMcpOutput(event.sessionId, event.toolName ?? "Agent", event.toolInput, resultText);
|
|
10371
|
+
if (id === null) return spawnAdvisory === "" ? passOutput() : contextOutput(spawnAdvisory);
|
|
10230
10372
|
if (redactedReport.count > 0) recordStat("secret_redacted", 0, redactedReport.count, void 0, "agent");
|
|
10231
10373
|
recordStat("session_hint", 0, 0);
|
|
10232
10374
|
const recallHint = `token-goat mcp-output ${id} --full`;
|
|
10233
|
-
const notice = `[token-goat] This subagent report (${toKB(resultText.length)}KB) is cached for later recall: ${recallHint}
|
|
10375
|
+
const notice = `[token-goat] This subagent report (${toKB(resultText.length)}KB) is cached for later recall: ${recallHint}${spawnAdvisory === "" ? "" : `
|
|
10376
|
+
${spawnAdvisory}`}`;
|
|
10234
10377
|
const collapsed = collapseFencedBlocks(resultText, recallHint, agentReportCfg.fence_collapse_min_lines, agentReportCfg.fence_collapse_keep_lines);
|
|
10235
10378
|
const deduped = dedupeFencedBlocks(collapsed, resultText, recallHint);
|
|
10236
10379
|
const final = collapseBlankRunsInFences(deduped);
|
|
@@ -10247,8 +10390,8 @@ function postAgentHandler(event) {
|
|
|
10247
10390
|
|
|
10248
10391
|
${notice}`;
|
|
10249
10392
|
const savedBytes = originalBytes - Buffer.byteLength(updatedOutput, "utf-8");
|
|
10250
|
-
if (savedBytes > 0) recordStat("agent_report_compact", savedBytes,
|
|
10251
|
-
return
|
|
10393
|
+
if (savedBytes > 0) recordStat("agent_report_compact", savedBytes, savedTokensFromBytes(savedBytes));
|
|
10394
|
+
return emitRewrite(updatedOutput, "agent", void 0, "counted-elsewhere");
|
|
10252
10395
|
}
|
|
10253
10396
|
recordStat("agent_report_compact_declined", 0, 0);
|
|
10254
10397
|
}
|
|
@@ -10258,7 +10401,11 @@ ${notice}`;
|
|
|
10258
10401
|
}
|
|
10259
10402
|
}
|
|
10260
10403
|
registerHook("pre_tool_use", preAgentHandler, { toolName: "Agent" });
|
|
10404
|
+
registerHook("pre_tool_use", preAgentHandler, { toolName: "task" });
|
|
10405
|
+
registerHook("pre_tool_use", preAgentHandler, { toolName: "Task" });
|
|
10261
10406
|
registerHook("post_tool_use", postAgentHandler, { toolName: "Agent" });
|
|
10407
|
+
registerHook("post_tool_use", postAgentHandler, { toolName: "task" });
|
|
10408
|
+
registerHook("post_tool_use", postAgentHandler, { toolName: "Task" });
|
|
10262
10409
|
|
|
10263
10410
|
// src/relay.ts
|
|
10264
10411
|
function isHookEventName(name2) {
|
|
@@ -10274,7 +10421,11 @@ function buildEvent(eventName, payload) {
|
|
|
10274
10421
|
const sessionId = typeof rawSession === "string" ? rawSession : "";
|
|
10275
10422
|
const rawAgentId = obj["agent_id"] ?? obj["agentId"];
|
|
10276
10423
|
const agentId = typeof rawAgentId === "string" && rawAgentId !== "" ? rawAgentId : void 0;
|
|
10277
|
-
|
|
10424
|
+
const rawTraceparent = obj["traceparent"] ?? obj["traceParent"] ?? process.env["TRACEPARENT"] ?? process.env["traceparent"];
|
|
10425
|
+
const traceparent = typeof rawTraceparent === "string" && rawTraceparent.trim() !== "" ? rawTraceparent.trim() : void 0;
|
|
10426
|
+
const rawTracestate = obj["tracestate"] ?? obj["traceState"] ?? process.env["TRACESTATE"] ?? process.env["tracestate"];
|
|
10427
|
+
const tracestate = typeof rawTracestate === "string" && rawTracestate.trim() !== "" ? rawTracestate.trim() : void 0;
|
|
10428
|
+
return { eventName, toolName, toolInput, sessionId, agentId, traceparent, tracestate, raw: obj };
|
|
10278
10429
|
}
|
|
10279
10430
|
function sessionStateKey(event) {
|
|
10280
10431
|
return event.agentId !== void 0 ? `${event.sessionId}:agent:${event.agentId}` : event.sessionId;
|
|
@@ -10285,6 +10436,7 @@ function harnessForNormalization() {
|
|
|
10285
10436
|
if (detected === "gemini") return "gemini";
|
|
10286
10437
|
if (detected === "grok") return "grok";
|
|
10287
10438
|
if (detected === "kimi") return "kimi";
|
|
10439
|
+
if (detected === "qwen") return "qwen";
|
|
10288
10440
|
if (detected === "copilot_cli") return "copilot_cli";
|
|
10289
10441
|
return "claude";
|
|
10290
10442
|
}
|
|
@@ -10299,6 +10451,12 @@ async function relayInProcess(eventName, rawPayload) {
|
|
|
10299
10451
|
if (!process.env["CLAUDE_CODE_SESSION_ID"] && event.sessionId) {
|
|
10300
10452
|
process.env["CLAUDE_CODE_SESSION_ID"] = event.sessionId;
|
|
10301
10453
|
}
|
|
10454
|
+
if (!process.env["TRACEPARENT"] && event.traceparent) {
|
|
10455
|
+
process.env["TRACEPARENT"] = event.traceparent;
|
|
10456
|
+
}
|
|
10457
|
+
if (!process.env["TRACESTATE"] && event.tracestate) {
|
|
10458
|
+
process.env["TRACESTATE"] = event.tracestate;
|
|
10459
|
+
}
|
|
10302
10460
|
const stateKey = sessionStateKey(event);
|
|
10303
10461
|
try {
|
|
10304
10462
|
loadSessionState(stateKey);
|