token-goat 2.9.15 → 2.9.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{token-goat-chunk-ASYEPR3S.mjs → token-goat-chunk-2IEIBVVD.mjs} +7 -3
  2. package/dist/{token-goat-chunk-LCZBPOIN.mjs → token-goat-chunk-2PCFAQAS.mjs} +66 -51
  3. package/dist/{token-goat-chunk-PM76YS22.mjs → token-goat-chunk-3H6FQT62.mjs} +1 -1
  4. package/dist/{token-goat-chunk-C5JIO6HK.mjs → token-goat-chunk-4DC6DDIB.mjs} +5 -5
  5. package/dist/{token-goat-chunk-YQ7WI2CO.mjs → token-goat-chunk-5USHO7B6.mjs} +393 -3957
  6. package/dist/{token-goat-chunk-2X2EBBC6.mjs → token-goat-chunk-63QWJ6UM.mjs} +2 -2
  7. package/dist/{token-goat-chunk-NDRP4KJQ.mjs → token-goat-chunk-BFNTLGVA.mjs} +226 -50
  8. package/dist/token-goat-chunk-BGHSSJUA.mjs +102 -0
  9. package/dist/{token-goat-chunk-IVUQLQWN.mjs → token-goat-chunk-DMQ32ZSR.mjs} +5 -3
  10. package/dist/{token-goat-chunk-SFAS46RE.mjs → token-goat-chunk-EJCY6KHO.mjs} +2 -2
  11. package/dist/{token-goat-chunk-Z6UXPYJA.mjs → token-goat-chunk-F2ARFHLJ.mjs} +1 -1
  12. package/dist/{token-goat-chunk-YKG35VHC.mjs → token-goat-chunk-GIMGT34T.mjs} +1 -1
  13. package/dist/{token-goat-chunk-SZWYESBS.mjs → token-goat-chunk-GMXBQS6O.mjs} +155 -54
  14. package/dist/{token-goat-chunk-S4XRY446.mjs → token-goat-chunk-H3DXI6VD.mjs} +4 -3
  15. package/dist/{token-goat-chunk-6DLVZDB6.mjs → token-goat-chunk-LJL6FGDS.mjs} +13 -12
  16. package/dist/{token-goat-chunk-NEI4NC54.mjs → token-goat-chunk-LKSXAMJB.mjs} +7 -2
  17. package/dist/{token-goat-chunk-A4JYKD5H.mjs → token-goat-chunk-M2R4UQ3B.mjs} +2 -2
  18. package/dist/{token-goat-chunk-MZDIJJ3R.mjs → token-goat-chunk-MA5BZ2KI.mjs} +40 -3
  19. package/dist/{token-goat-chunk-GIIHUSZX.mjs → token-goat-chunk-MIL62GMN.mjs} +41 -11
  20. package/dist/{token-goat-chunk-NU7TLMQK.mjs → token-goat-chunk-NAMH252H.mjs} +10 -7
  21. package/dist/{token-goat-chunk-DQ4J5AFF.mjs → token-goat-chunk-NOBZV7PM.mjs} +69 -18
  22. package/dist/{token-goat-chunk-5V7DAC7V.mjs → token-goat-chunk-NQZCSVN2.mjs} +10 -3
  23. package/dist/{token-goat-chunk-7ZYK25AO.mjs → token-goat-chunk-OCI25AXY.mjs} +4 -4
  24. package/dist/{token-goat-chunk-NDPO7GAH.mjs → token-goat-chunk-QMZMHQRR.mjs} +21 -20
  25. package/dist/{token-goat-chunk-K7F2BFIK.mjs → token-goat-chunk-R3X5TCPB.mjs} +1 -1
  26. package/dist/token-goat-chunk-RRNZMM3A.mjs +3364 -0
  27. package/dist/{token-goat-chunk-ATIFTMRC.mjs → token-goat-chunk-ULA4WCED.mjs} +7 -7
  28. package/dist/{token-goat-chunk-OUGNPMDA.mjs → token-goat-chunk-UMXD2M7O.mjs} +30 -14
  29. package/dist/{token-goat-chunk-YZX7EFG4.mjs → token-goat-chunk-UXHDBVUR.mjs} +1 -1
  30. package/dist/{token-goat-chunk-LT7JRU6K.mjs → token-goat-chunk-VVIYEFBI.mjs} +4 -4
  31. package/dist/{token-goat-chunk-BL5LNGBG.mjs → token-goat-chunk-XOHTFRDH.mjs} +2966 -3869
  32. package/dist/{token-goat-chunk-XEPXYDPI.mjs → token-goat-chunk-YPS64ACE.mjs} +11 -2
  33. package/dist/{token-goat-chunk-GMQQA7E4.mjs → token-goat-chunk-Z5VPAXTC.mjs} +2442 -722
  34. package/dist/{token-goat-chunk-3NSDDTGL.mjs → token-goat-chunk-ZEHFYJ7Z.mjs} +5 -5
  35. package/dist/token-goat-hook.mjs +13 -12
  36. package/dist/token-goat.core.mjs +21 -20
  37. package/package.json +1 -1
@@ -1,65 +1,33 @@
1
1
  import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
- import {
4
- sanitizeFtsQuery
5
- } from "./token-goat-chunk-2X2EBBC6.mjs";
6
3
  import {
7
4
  DEFAULT_MAX_AGE_MS,
8
5
  blobPath,
9
- countRedactionPlaceholders,
10
6
  isBlobStale,
11
7
  loadBlob,
12
- recordStat,
13
- recordUnmappedTool,
14
8
  redactSecrets,
15
9
  savedTokensFromBytes,
16
10
  storeBlob,
17
11
  stripAnsiEscapes
18
- } from "./token-goat-chunk-IVUQLQWN.mjs";
12
+ } from "./token-goat-chunk-DMQ32ZSR.mjs";
13
+ import {
14
+ sanitizeFtsQuery
15
+ } from "./token-goat-chunk-63QWJ6UM.mjs";
19
16
  import {
20
17
  getDb
21
- } from "./token-goat-chunk-K7F2BFIK.mjs";
18
+ } from "./token-goat-chunk-R3X5TCPB.mjs";
22
19
  import {
23
- detectHarness,
24
20
  loadConfig,
25
- parse,
26
- shortFingerprint,
27
- stringify
28
- } from "./token-goat-chunk-S4XRY446.mjs";
21
+ shortFingerprint
22
+ } from "./token-goat-chunk-H3DXI6VD.mjs";
29
23
  import {
30
24
  registerReset
31
25
  } from "./token-goat-chunk-EEIDFMEM.mjs";
32
26
  import {
33
- assertProjectScopeTarget,
34
- assertWriteInScope,
35
- atomicWriteText,
36
- backupFile,
37
- ensureDirSync,
38
- escapeRegExp,
39
- extractErrorMessage,
40
27
  globalDbPath,
41
- hookCommandFor,
42
- hookPowershellCommand,
43
- projectPathIsConsultable,
44
- projectScopeRoot,
45
- recordCreatedConfig,
46
- removeCreatedBackups,
47
- removeFileInScope,
48
- runGit,
49
- safeSlice,
50
- stripDelimitedBlock,
51
- stripOwnHooksFromMap,
52
- stripStaleGroupHooks,
53
- takeCreatedConfig,
54
- upsertDelimitedBlock,
55
- withInstallScope,
56
- writeIfDifferent,
57
- writeJsonSettings
58
- } from "./token-goat-chunk-PM76YS22.mjs";
28
+ runGit
29
+ } from "./token-goat-chunk-3H6FQT62.mjs";
59
30
  import {
60
- displaySafeText,
61
- neutralizeOutsideFences,
62
- normalizeDarwinSystemAlias,
63
31
  normalizePath,
64
32
  resolveIndexPath
65
33
  } from "./token-goat-chunk-ERTXEKB6.mjs";
@@ -67,1222 +35,10 @@ import {
67
35
  init_define_import_meta_env
68
36
  } from "./token-goat-chunk-A37V4PBF.mjs";
69
37
 
70
- // src/bridges/visualstudio_install.ts
71
- init_define_import_meta_env();
72
- import * as fs2 from "node:fs";
73
- import * as os from "node:os";
74
- import * as path2 from "node:path";
75
-
76
- // src/bridges/guidance_block.ts
77
- init_define_import_meta_env();
78
- function buildGuidanceBody(fallbackToolClause, opts = {}) {
79
- const gdrive = opts.gdrive !== false;
80
- return [
81
- "## token-goat",
82
- "",
83
- "**Gate \u2014 before every file read, answer one question first: is there a token-goat command that returns just what I need?** If yes, run it. A read tool invoked without answering the gate is a violation, not an oversight. The gate is per file: batched or parallel reads do not exempt it.",
84
- "",
85
- `This gate decides *whether* to reach for a read tool at all. ${fallbackToolClause} only pick the *fallback* once token-goat has been ruled out for this read \u2014 they never authorize skipping the gate.`,
86
- "",
87
- "Fallback clauses may name your harness's own native read, search, and edit tools, or its shell helpers. Shell binaries and editor programs are commands invoked through the shell tool, never tool identifiers, and must never appear in an agent's tools frontmatter or an allowed-tools list. This paragraph deliberately names no specific tool or binary: instruction-file loaders harvest such names into a tool allowlist and then warn that every one of them is unknown.",
88
- "",
89
- "Exemptions (gate passes, read directly): the file is under ~200 lines and you need all of it; it was never indexed (new, untracked, or generated this turn); it is a genuinely opaque binary (not an image); the target has no symbol handle (e.g. a literal mid-function).",
90
- "",
91
- "Failure shapes to catch yourself in, and the command that replaces each:",
92
- '- a shell text search with context flags to find a function body \u2192 `read "file::symbol"`',
93
- '- paging one function with view/view_range \u2192 `read "file::symbol"`',
94
- '- reading a symbol plus chasing its callers and containing doc section as separate reads \u2192 `brief "file::symbol"`',
95
- '- reading one heading of a large doc \u2192 `section "file::Heading"`',
96
- "- searching for a symbol's callers \u2192 `refs file::symbol --callers`",
97
- '- searching for a *concept* rather than a literal string \u2192 `semantic "description"`',
98
- "- re-reading output you already captured \u2192 `bash-output`/`web-output`/`mcp-output` by ID",
99
- "- a directory listing or recursive wildcard walk to orient in an unfamiliar repo \u2192 `map --compact`",
100
- "- pulling one value or subtree out of a JSON/YAML/XML file (manifest, lockfile, spec, config) \u2192 `json-query file 'a.b.c'` / `yaml-query file 'a.b.c'` / `xml-query file 'a.b.c'`",
101
- "- opening an image to check its dimensions, format, or size \u2192 `image-meta file`",
102
- "- opening a screenshot, diagram, or scan to read the text in it \u2192 `image-text file`",
103
- "- opening a PDF or Office document \u2192 inspect its format first, then read a narrow slice: PDF `pdf-meta`/`pdf-outline` then `pdf-locate` to find the pages and `pdf-extract` only those; Word `docx-outline` then `docx-tables`/`docx-text`; PowerPoint `pptx-outline` then `pptx-slide`/`pptx-notes`; Excel `xlsx-sheets` then `xlsx-columns`/`xlsx-head`/`xlsx-range`/`xlsx-query`",
104
- "",
105
- 'Commands: `symbol NAME`, `read "file::symbol"`, `brief "file::symbol"`, `section "file::Heading"`, `semantic "description"`, `outline file`/`skeleton file`, `map --compact`, `refs file::symbol --callers`, `changed --symbol`, `config-get file KEY`, `json-query file \'a.b.c\'`/`yaml-query`/`xml-query`, `json-outline file`/`yaml-outline`/`xml-outline`, `bash-output`/`web-output`/`mcp-output`, ' + (gdrive ? "`gdrive-sections <file-id>`, " : "") + "`image-meta file`/`image-text file`, `pdf-meta`/`pdf-outline`/`pdf-locate`/`pdf-extract`, `docx-outline`/`docx-tables`/`docx-text`, `pptx-outline`/`pptx-slide`/`pptx-notes`/`pptx-text`, `xlsx-sheets`/`xlsx-columns`/`xlsx-head`/`xlsx-range`/`xlsx-query`.",
106
- "",
107
- "Sub-agent briefs must carry this gate verbatim: a sub-agent inherits none of this context and its reads spend the same token budget.",
108
- "",
109
- "`token-goat stats` \u2014 self-check. Flat counts during code work mean the gate is being skipped."
110
- ].join("\n");
111
- }
112
- function skillDescriptionLine(gdrive) {
113
- return `description: Use before reading whole files or grepping wide. token-goat commands (symbol, read, section, semantic, outline, skeleton, map, refs, changed, config-get, bash-output, web-output${gdrive ? ", gdrive-sections" : ""}) return narrow slices of code and docs at a fraction of the token cost.`;
114
- }
115
- function buildGuidanceBlock(h) {
116
- return [h.beginMarker, buildGuidanceBody(h.fallbackToolClause, h.gdrive === void 0 ? {} : { gdrive: h.gdrive }), h.endMarker].join("\n");
117
- }
118
-
119
- // src/bridges/mcp_servers_json.ts
38
+ // src/tool_filters/helpers.ts
120
39
  init_define_import_meta_env();
121
40
  import * as fs from "node:fs";
122
41
  import * as path from "node:path";
123
- import { createRequire } from "node:module";
124
- import { fileURLToPath } from "node:url";
125
- var jsoncParser;
126
- function jsonc() {
127
- jsoncParser ??= createRequire(import.meta.url)("jsonc-parser");
128
- return jsoncParser;
129
- }
130
- function bundledCliPath() {
131
- const here = path.dirname(fileURLToPath(import.meta.url));
132
- const bundled = path.join(here, "token-goat.mjs");
133
- if (fs.existsSync(bundled)) return bundled;
134
- return path.resolve(here, "..", "..", "dist", "token-goat.mjs");
135
- }
136
- function managedServer() {
137
- return {
138
- type: "stdio",
139
- command: process.execPath,
140
- args: [bundledCliPath(), "mcp-serve"]
141
- };
142
- }
143
- function isManagedServer(value) {
144
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
145
- const entry = value;
146
- const args = entry["args"];
147
- return entry["type"] === "stdio" && Array.isArray(args) && args.length === 2 && typeof args[0] === "string" && path.basename(args[0]).toLowerCase() === "token-goat.mjs" && args[1] === "mcp-serve";
148
- }
149
- function readServersJson(filePath, label) {
150
- const text = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "{}\n";
151
- const errors = [];
152
- const parsed = jsonc().parse(text, errors, { allowTrailingComma: true, disallowComments: false });
153
- if (errors.length > 0) {
154
- throw new Error(`malformed ${label} MCP JSON at ${filePath}`);
155
- }
156
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
157
- throw new Error(`malformed ${label} MCP JSON at ${filePath}: expected a JSON object`);
158
- }
159
- return { text, value: parsed };
160
- }
161
- function serversOf(config, filePath, label, rootKey = "servers") {
162
- const servers = config.value[rootKey];
163
- if (servers === void 0) return {};
164
- if (servers === null || typeof servers !== "object" || Array.isArray(servers)) {
165
- throw new Error(`malformed ${label} MCP JSON at ${filePath}: ${rootKey} must be an object`);
166
- }
167
- return servers;
168
- }
169
- var FORMAT = { insertSpaces: true, tabSize: 2, eol: "\n" };
170
- function editAt(text, jsonPath, value) {
171
- let out = text;
172
- for (const e of [...jsonc().modify(text, jsonPath, value, {})].sort((a, b) => b.offset - a.offset)) {
173
- out = out.slice(0, e.offset) + e.content + out.slice(e.offset + e.length);
174
- if (e.content.length > 0) out = jsonc().applyEdits(out, jsonc().format(out, { offset: e.offset, length: e.content.length }, FORMAT));
175
- }
176
- return out;
177
- }
178
- function setTokenGoatServer(text, value, rootKey = "servers") {
179
- if (value !== void 0 && /^\s*\{\s*\}\s*$/.test(text)) return `${JSON.stringify({ [rootKey]: { "token-goat": value } }, null, 2)}
180
- `;
181
- return editAt(text, [rootKey, "token-goat"], value);
182
- }
183
- function holdsOnlyManagedServer(value) {
184
- const keys = Object.keys(value);
185
- if (keys.length !== 1) return false;
186
- const root = keys[0];
187
- if (root !== "servers" && root !== "mcpServers") return false;
188
- const servers = value[root];
189
- if (servers === null || typeof servers !== "object" || Array.isArray(servers)) return false;
190
- const names = Object.keys(servers);
191
- return names.length === 1 && names[0] === "token-goat" && isManagedServer(servers["token-goat"]);
192
- }
193
- function dropEmptyServers(text, rootKey = "servers") {
194
- const parsed = parseObject(text);
195
- if (parsed === null) return text;
196
- const servers = parsed[rootKey];
197
- if (servers === null || typeof servers !== "object" || Array.isArray(servers) || Object.keys(servers).length > 0) return text;
198
- return editAt(text, [rootKey], void 0);
199
- }
200
- function parseObject(text) {
201
- const parsed = jsonc().parse(text, [], { allowTrailingComma: true, disallowComments: false });
202
- return parsed === null || typeof parsed !== "object" || Array.isArray(parsed) ? null : parsed;
203
- }
204
- function ensureMcpServersKey(text) {
205
- const parsed = parseObject(text);
206
- if (parsed === null || "mcpServers" in parsed) return text;
207
- return editAt(text, ["mcpServers"], {});
208
- }
209
- function dropLoneEmptyMcpServers(text) {
210
- const parsed = parseObject(text);
211
- if (parsed === null || Object.keys(parsed).length !== 1) return text;
212
- const mcpServers = parsed["mcpServers"];
213
- if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers) || Object.keys(mcpServers).length > 0) return text;
214
- return editAt(text, ["mcpServers"], void 0);
215
- }
216
- function hasManagedServer(filePath, label, rootKey = "servers", isManaged = isManagedServer) {
217
- if (!fs.existsSync(filePath)) return false;
218
- try {
219
- const config = readServersJson(filePath, label);
220
- return isManaged(serversOf(config, filePath, label, rootKey)["token-goat"]);
221
- } catch {
222
- return false;
223
- }
224
- }
225
-
226
- // src/bridges/visualstudio_install.ts
227
- var LABEL = "Visual Studio";
228
- var VISUALSTUDIO_GUIDANCE_BEGIN = "<!-- token-goat-visualstudio-begin -->";
229
- var VISUALSTUDIO_GUIDANCE_END = "<!-- token-goat-visualstudio-end -->";
230
- var SIBLING_GATE_MARKERS = [
231
- ["<!-- token-goat-vscode-begin -->", "<!-- token-goat-vscode-end -->"],
232
- ["<!-- token-goat-begin -->", "<!-- token-goat-end -->"]
233
- ];
234
- function visualStudioUserMcpPath() {
235
- return path2.join(os.homedir(), ".mcp.json");
236
- }
237
- function visualStudioProjectMcpPath(projectRoot = process.cwd()) {
238
- return path2.join(path2.resolve(projectRoot), ".mcp.json");
239
- }
240
- function visualStudioMcpPath(opts = {}) {
241
- return opts.project === true ? visualStudioProjectMcpPath(opts.projectRoot) : visualStudioUserMcpPath();
242
- }
243
- function otherScopeMcpPath(opts) {
244
- return opts.project === true ? visualStudioUserMcpPath() : visualStudioProjectMcpPath(opts.projectRoot);
245
- }
246
- function visualStudioInstructionsPath(opts = {}) {
247
- return opts.project === true ? path2.join(path2.resolve(opts.projectRoot ?? process.cwd()), ".github", "copilot-instructions.md") : path2.join(os.homedir(), "copilot-instructions.md");
248
- }
249
- function visualStudioOtherScopeHasManagedServer(opts = {}) {
250
- return hasManagedServer(otherScopeMcpPath(opts), LABEL);
251
- }
252
- function isVisualStudioInstalled(opts = {}) {
253
- return hasManagedServer(visualStudioMcpPath(opts), LABEL);
254
- }
255
- function visualStudioSolutionVscodeMcpPath(projectRoot = process.cwd()) {
256
- return path2.join(path2.resolve(projectRoot), ".vscode", "mcp.json");
257
- }
258
- function visualStudioManagedRegistrations(projectRoot = process.cwd()) {
259
- return [visualStudioUserMcpPath(), visualStudioProjectMcpPath(projectRoot), visualStudioSolutionVscodeMcpPath(projectRoot)].filter((p) => hasManagedServer(p, LABEL));
260
- }
261
- function visualStudioDuplicateNote(projectRoot = process.cwd()) {
262
- const found = visualStudioManagedRegistrations(projectRoot);
263
- if (found.length < 2) return null;
264
- const lead = `Note: Visual Studio reads ${found.join(" and ")}, and each registers token-goat, so it will list the token-goat server more than once for this solution.`;
265
- if (!found.includes(visualStudioSolutionVscodeMcpPath(projectRoot))) return `${lead} Keep one: run "token-goat uninstall --visualstudio" or "token-goat uninstall --visualstudio -p".`;
266
- return `${lead} Keep one: if you do not use VS Code in this folder, run "token-goat uninstall --vscode -p"; otherwise run "token-goat uninstall --visualstudio${found.includes(visualStudioUserMcpPath()) ? "" : " -p"}".`;
267
- }
268
- function visualStudioMcpStatus(opts = {}) {
269
- const checkedPaths = [visualStudioUserMcpPath()];
270
- if (opts.projectRoot !== void 0) checkedPaths.push(visualStudioProjectMcpPath(opts.projectRoot));
271
- return { configured: checkedPaths.some((p) => hasManagedServer(p, LABEL)), checkedPaths };
272
- }
273
- function visualStudioManagedEntry(filePath) {
274
- if (!fs2.existsSync(filePath)) return null;
275
- try {
276
- const entry = serversOf(readServersJson(filePath, LABEL), filePath, LABEL)["token-goat"];
277
- if (!isManagedServer(entry)) return null;
278
- const { command, args } = entry;
279
- return { command: typeof command === "string" ? command : "", bundlePath: args[0] ?? "" };
280
- } catch {
281
- return null;
282
- }
283
- }
284
- var FALLBACK_TOOL_CLAUSE = "Visual Studio\u2019s built-in readfile, code_search, and find_references tools";
285
- var RETRIEVE_NOTE = "**Compressed payloads:** a message containing a token-goat payload block (recognizable by a `recovery: token-goat retrieve <id>` line) is compressed text, not an answer. Call the MCP tool `retrieve_text` with that id to recover the original text, then answer the question the message asks using the recovered text. Never present the raw payload to the user as the response; if the `retrieve_text` tool is unavailable (the MCP server is not running, or its tools are not ticked in the chat Tools picker), say so plainly and ask the user to turn on the token-goat tools.";
286
- var SUPPORT_NOTE = "Visual Studio support: token-goat install --visualstudio registers a stdio MCP server under the servers root key of %USERPROFILE%\\.mcp.json (with --project, the .mcp.json in the solution folder). Visual Studio runs no token-goat hooks, so nothing denies a repeated read, adds a hint, shrinks an image, or folds what readfile returns there: the narrow reads come only from calling the token-goat MCP tools, or the token-goat CLI through runcommandinterminal.";
287
- function buildVisualStudioGuidanceBlock(sharesFileWithGate) {
288
- const lead = sharesFileWithGate ? ["## token-goat in Visual Studio", "", `The token-goat gate elsewhere in this file applies in Visual Studio too; there, ${FALLBACK_TOOL_CLAUSE} are the fallback it names.`] : [buildGuidanceBody(FALLBACK_TOOL_CLAUSE, { gdrive: loadConfig().gdrive.enabled })];
289
- return [VISUALSTUDIO_GUIDANCE_BEGIN, ...lead, "", RETRIEVE_NOTE, "", SUPPORT_NOTE, VISUALSTUDIO_GUIDANCE_END].join("\n");
290
- }
291
- function hasBlock(text, begin, end) {
292
- const b = text.indexOf(begin);
293
- const e = text.indexOf(end);
294
- return b !== -1 && e > b;
295
- }
296
- function readText(filePath) {
297
- try {
298
- return fs2.readFileSync(filePath, "utf8");
299
- } catch {
300
- return "";
301
- }
302
- }
303
- function writeGuidance(filePath) {
304
- const text = readText(filePath);
305
- const shares = SIBLING_GATE_MARKERS.some(([begin, end]) => hasBlock(text, begin, end));
306
- return upsertDelimitedBlock(filePath, VISUALSTUDIO_GUIDANCE_BEGIN, VISUALSTUDIO_GUIDANCE_END, buildVisualStudioGuidanceBlock(shares));
307
- }
308
- function syncVisualStudioProjectGuidance(filePath) {
309
- if (!hasBlock(readText(filePath), VISUALSTUDIO_GUIDANCE_BEGIN, VISUALSTUDIO_GUIDANCE_END)) return false;
310
- return writeGuidance(filePath);
311
- }
312
- function installVisualStudio(opts = {}) {
313
- return withInstallScope(projectScopeRoot(opts), () => installVisualStudioScoped(opts));
314
- }
315
- function installVisualStudioScoped(opts) {
316
- const scope = opts.project === true ? "project" : "user";
317
- const mcpPath = visualStudioMcpPath(opts);
318
- const instructionsPath = visualStudioInstructionsPath(opts);
319
- if (visualStudioOtherScopeHasManagedServer(opts)) {
320
- const otherScope = scope === "project" ? "user" : "project";
321
- throw new Error(
322
- `token-goat is already registered in Visual Studio ${otherScope} scope (${otherScopeMcpPath(opts)}). Visual Studio reads both files, so installing into ${scope} scope too would register it twice. Run "token-goat uninstall --visualstudio${otherScope === "project" ? " --project" : ""}" first if you want to move it, or drop --visualstudio from this run.`
323
- );
324
- }
325
- const mcpExisted = fs2.existsSync(mcpPath);
326
- const config = readServersJson(mcpPath, LABEL);
327
- const ownedAlready = mcpExisted && holdsOnlyManagedServer(config.value);
328
- const current = serversOf(config, mcpPath, LABEL)["token-goat"];
329
- if (current !== void 0 && !isManagedServer(current)) {
330
- throw new Error(`Visual Studio MCP JSON at ${mcpPath} already has a non-token-goat-managed server named "token-goat"`);
331
- }
332
- const next = ensureMcpServersKey(setTokenGoatServer(config.text, managedServer()));
333
- if (config.text !== next) {
334
- ensureDirSync(path2.dirname(mcpPath));
335
- backupFile(mcpPath);
336
- atomicWriteText(mcpPath, next);
337
- if (!mcpExisted || ownedAlready) recordCreatedConfig(mcpPath);
338
- }
339
- const guidanceChanged = writeGuidance(instructionsPath);
340
- return { mcpPath, instructionsPath, alreadyInstalled: config.text === next && !guidanceChanged, scope };
341
- }
342
- function uninstallVisualStudio(opts = {}) {
343
- return withInstallScope(projectScopeRoot(opts), () => uninstallVisualStudioScoped(opts));
344
- }
345
- function uninstallVisualStudioScoped(opts) {
346
- const mcpPath = visualStudioMcpPath(opts);
347
- let removed = false;
348
- if (fs2.existsSync(mcpPath)) {
349
- const config = readServersJson(mcpPath, LABEL);
350
- if (isManagedServer(serversOf(config, mcpPath, LABEL)["token-goat"])) {
351
- const next = dropEmptyServers(setTokenGoatServer(config.text, void 0));
352
- if (/^\s*\{\s*\}\s*$/.test(dropLoneEmptyMcpServers(next)) && takeCreatedConfig(mcpPath)) removeFileInScope(mcpPath);
353
- else {
354
- backupFile(mcpPath);
355
- atomicWriteText(mcpPath, ensureMcpServersKey(next));
356
- }
357
- removeCreatedBackups(mcpPath);
358
- removed = true;
359
- }
360
- }
361
- const instructionsPath = visualStudioInstructionsPath(opts);
362
- if (stripDelimitedBlock(instructionsPath, VISUALSTUDIO_GUIDANCE_BEGIN, VISUALSTUDIO_GUIDANCE_END)) {
363
- removed = true;
364
- if (readText(instructionsPath).trim() === "") removeFileInScope(instructionsPath);
365
- }
366
- return removed;
367
- }
368
-
369
- // src/bridges/copilot_cli_install.ts
370
- init_define_import_meta_env();
371
- import * as fs3 from "node:fs";
372
- import * as os2 from "node:os";
373
- import * as path3 from "node:path";
374
-
375
- // src/bridges/copilot_cli.ts
376
- init_define_import_meta_env();
377
-
378
- // src/bridges/shrink_block.ts
379
- init_define_import_meta_env();
380
- var MATERIALIZE_SHRUNK_IMAGE_JS = `// Best-effort sweep of previously materialized shrunk copies in the OS temp dir. The temp file only needs to outlive the single tool call whose path argument was rewritten to it, so anything older than an hour is finished with; the "token-goat-shrink-" prefix check confines the sweep to this mechanism's own files (same defense-in-depth rule as pruneShrinkCache in src/image_shrink.ts). Throttled per process; runs only when a shrink payload actually arrives, so the common no-image path never pays for it.
381
- const MATERIALIZED_SHRINK_MAX_AGE_MS = 60 * 60 * 1000
382
- let lastMaterializedShrinkSweepAtMs = 0
383
- function pruneMaterializedShrinks() {
384
- const now = Date.now()
385
- if (now - lastMaterializedShrinkSweepAtMs < MATERIALIZED_SHRINK_MAX_AGE_MS) return
386
- lastMaterializedShrinkSweepAtMs = now
387
- try {
388
- const dir = os.tmpdir()
389
- for (const file of fs.readdirSync(dir)) {
390
- if (!file.startsWith("token-goat-shrink-")) continue
391
- const full = path.join(dir, file)
392
- try {
393
- const st = fs.statSync(full)
394
- if (st.isFile() && now - st.mtimeMs > MATERIALIZED_SHRINK_MAX_AGE_MS) fs.unlinkSync(full)
395
- } catch {
396
- // Best-effort per-file cleanup; one bad stat/unlink must not abort the sweep.
397
- }
398
- }
399
- } catch {
400
- // Best-effort; a readdir failure must never break the materialization below.
401
- }
402
- }
403
-
404
- // Decode a token-goat image-shrink additionalContext payload ("<summary>\\ndata:image/<fmt>;base64,<data>") into a real file on disk, since the host's pre-tool hook has no context-injection channel to hand the shrunk image to the model directly -- only argument rewriting. The temp filename is derived from pid/time/random alone, never from the source image's own name, so an attacker-chosen filename cannot steer the write; the format suffix comes from the data-URL's media subtype, whose character class ([a-zA-Z0-9.+-]) admits no path separators. Returns undefined (leaving the original path argument untouched, so a failed shrink falls back to the original image) if the context isn't a shrink payload or anything goes wrong writing it.
405
- function materializeShrunkImage(context) {
406
- if (typeof context !== "string") return undefined
407
- const idx = context.indexOf("data:image/")
408
- if (idx === -1) return undefined
409
- const match = /^data:image\\/([a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)$/.exec(context.slice(idx).trim())
410
- if (!match) return undefined
411
- try {
412
- pruneMaterializedShrinks()
413
- const buf = Buffer.from(match[2], "base64")
414
- const name = \`token-goat-shrink-\${process.pid}-\${Date.now()}-\${globalThis.crypto.randomUUID()}.\${match[1]}\`
415
- const file = path.join(os.tmpdir(), name)
416
- // Owner-only: the copy is of a workspace image and sits in the shared temp dir.
417
- fs.writeFileSync(file, buf, { mode: 0o600 })
418
- return file
419
- } catch {
420
- return undefined
421
- }
422
- }`;
423
-
424
- // src/bridges/copilot_cli.ts
425
- var COPILOT_CLI_HOOK_SCRIPT = `#!/usr/bin/env node
426
- // token-goat Copilot CLI hook shim. Translates Copilot's hook event names and
427
- // request/response schema to/from token-goat's internal hook protocol.
428
- 'use strict'
429
- const { spawnSync } = require('node:child_process')
430
- const fs = require('node:fs')
431
- const os = require('node:os')
432
- const path = require('node:path')
433
- const { pathToFileURL } = require('node:url')
434
-
435
- // Copilot event name -> token-goat internal HookEventName (src/types.ts's
436
- // HOOK_EVENTS). Only these eight have a token-goat handler; every other real
437
- // Copilot event (sessionEnd, subagentStart, errorOccurred, notification,
438
- // permissionRequest) is left unimplemented
439
- // rather than guessed at, and falls through to the default no-op below.
440
- // postToolUseFailure is the newest of the eight and the only one whose channel
441
- // costs tokens instead of saving them: it fires instead of postToolUse when a
442
- // tool result is a failure, and accepts only additionalContext back --
443
- // modifiedResult and suppressOutput are not honored there, so a failed result
444
- // still cannot be fenced, compressed or shrunk. It is wired anyway because
445
- // additionalContext demonstrably reaches the model (see the postToolUseFailure
446
- // branch in translate() for the bundle offset), and hooks_tool_failure.ts
447
- // spends that channel only on an exact repeat failure, where staying silent
448
- // costs a whole wasted retry.
449
- // 'sessionStart' was previously a permanent no-op on the stated grounds that
450
- // token-goat has no internal session_start handler. That was simply wrong --
451
- // hooks_session_start.ts has long emitted the command-routing reminder that
452
- // every other harness receives -- and the no-op was the reason Copilot CLI
453
- // sessions alone never got told token-goat exists. It is wired now: verified
454
- // against Copilot CLI 1.0.77 that a hooks.json sessionStart entry returning
455
- // {additionalContext} does reach the model. The github/copilot-cli#2142
456
- // fire-and-forget bug that would have made this dead wiring was fixed in a
457
- // pre-release months before that version, and its companion multi-extension
458
- // hook-overwrite bug never applied here: it hit runtime *extension* hooks,
459
- // while this config-file hooks.json path goes through Copilot's own merge.
460
- const COPILOT_TO_TG_EVENT = {
461
- sessionStart: 'session_start',
462
- preToolUse: 'pre_tool_use',
463
- postToolUse: 'post_tool_use',
464
- preCompact: 'pre_compact',
465
- agentStop: 'stop',
466
- subagentStop: 'subagent_stop',
467
- userPromptSubmitted: 'user_prompt_submit',
468
- postToolUseFailure: 'post_tool_use_failure',
469
- }
470
-
471
- // Copilot built-in tool name -> token-goat internal tool name. Confirmed via
472
- // @github/copilot-sdk type definitions and multiple real GitHub issue payload
473
- // dumps -- supersedes an earlier docs-based guess (shell/read/write/url) that
474
- // didn't hold up in practice; 'write' in particular was never a real
475
- // toolName value, only a Copilot permission-pattern keyword. 'powershell'
476
- // maps to the same 'Bash' handler as 'bash' since both are shell-command
477
- // execution from token-goat's perspective (mirrors how hooks_bash.ts's own
478
- // filters already treat powershell-wrapped commands as part of the Bash
479
- // pipeline, not a separate tool). 'task', 'ask_user', 'memory', and
480
- // MCP-server tool invocations (<server-name>-<tool-name>) have no
481
- // token-goat equivalent and are passed through unmapped (safe no-op for
482
- // handlers that don't recognize the name).
483
- // 'read_bash'/'read_powershell' are the background-shell output pollers, and
484
- // they are the exact shape of Claude Code's BashOutput: Copilot's shell tool
485
- // is async, so a long-running command is started once and then re-read over
486
- // and over within one turn, each read returning the accumulated output again.
487
- // hooks_bashoutput.ts already collapses that into a delta (or a short
488
- // "unchanged" marker) for Claude Code, and now does the same here. Both the
489
- // names and the argument key were read out of the shipping 1.0.80 bundle
490
- // rather than guessed: the builtin tool-name table in
491
- // prebuilds/win32-x64/runtime.node lists read_bash/stop_bash/list_bash and
492
- // read_powershell/stop_powershell/list_powershell, and the poller's own input
493
- // schema in app.js is {shellId, delay} -- so the shell id needs the
494
- // POLL_ID_ARG_KEY remap below to reach postBashOutputHandler, which reads
495
- // 'bash_id'. read_shell/stop_shell/list_shells, which an earlier static sweep
496
- // of the same binary suggested were the real names, are internal Rust
497
- // identifiers (tool_read_shell_prepare_input, tool_list_shells_descriptor,
498
- // PreparedStopShellInput, and the serde field names of the shell config
499
- // struct), never wire tool names, so they are deliberately absent here.
500
- // stop_bash/list_bash and their powershell twins stay unmapped because
501
- // token-goat has no KillShell-equivalent handler for them to reach; a mapping
502
- // would be pure decoration.
503
- // 'bash'/'powershell' stay exactly as they were. The same sweep suggested the
504
- // executor had been renamed to write_bash/write_powershell and that this
505
- // mapping was dead, and that is not what the bundle says: app.js resolves the
506
- // executor as shellConfig?.shellToolName ?? "bash" (with "powershell" as the
507
- // Windows default of the same config), and its command lives under 'command'
508
- // exactly as hooks_bash.ts expects. write_bash/write_powershell are real tool
509
- // names, but they are a different tool -- their schema is {shellId, input,
510
- // delay} and the bundle files them under the subtype "write_shell", i.e. send
511
- // stdin to an already-running shell, not run a command. Mapping them to Bash
512
- // would label a stdin write as a shell execution, so they are left unmapped.
513
- // 'task', 'read_agent' and 'memory'-family tools are likewise left alone:
514
- // task's result is assembled in the native addon and its shape is unknown,
515
- // read_agent is an incremental poll that postAgentHandler is not written for,
516
- // and the real tool names behind memory were never confirmed. Each would put
517
- // a handler that rewrites model-visible output in front of a payload shape
518
- // nobody has seen, which is worse than leaving the compression on the table.
519
- const TOOL_TO_TG = {
520
- bash: 'Bash',
521
- powershell: 'Bash',
522
- read_bash: 'BashOutput',
523
- read_powershell: 'BashOutput',
524
- view: 'Read',
525
- create: 'Write',
526
- edit: 'Edit',
527
- web_fetch: 'WebFetch',
528
- web_search: 'WebSearch',
529
- grep: 'Grep',
530
- glob: 'Glob',
531
- skill: 'Skill',
532
- exit_plan_mode: 'ExitPlanMode',
533
- }
534
-
535
- function foldToolName(name) {
536
- return typeof name === 'string' ? name.toLowerCase().replace(/[_-]/g, '') : ''
537
- }
538
-
539
- const FOLDED_TOOL_TO_TG = {}
540
- for (const [k, v] of Object.entries(TOOL_TO_TG)) {
541
- FOLDED_TOOL_TO_TG[foldToolName(k)] = v
542
- }
543
- // Also register folded entries for canonical TG tool names directly (e.g. WebSearch -> websearch)
544
- for (const v of Object.values(TOOL_TO_TG)) {
545
- FOLDED_TOOL_TO_TG[foldToolName(v)] = v
546
- }
547
-
548
- function resolveCanonicalToolName(name) {
549
- if (!name || typeof name !== 'string') return name
550
- const direct = TOOL_TO_TG[name]
551
- if (direct !== undefined) return direct
552
- const folded = foldToolName(name)
553
- return FOLDED_TOOL_TO_TG[folded] || name
554
- }
555
-
556
- // Confirmed via github/copilot-cli#3349 (open, unresolved as of writing): some
557
- // real Copilot CLI invocations send toolArgs as a JSON-*encoded string*
558
- // rather than a parsed object, contradicting the documented schema. Left
559
- // unhandled, canonical.tool_input would become a raw string and every
560
- // downstream event.toolInput[key] lookup in token-goat's handlers would
561
- // silently return undefined -- the deny/dedup mechanism would no-op with no
562
- // error surfaced. Parse it defensively; on a non-string, absent, or
563
- // unparsable value, fall back to {} rather than throwing (this shim's
564
- // convention throughout is fail-open, never crash on a malformed payload).
565
- function parseMaybeJsonObject(value) {
566
- if (value && typeof value === 'object') return value
567
- if (typeof value === 'string') {
568
- try {
569
- const parsed = JSON.parse(value)
570
- if (parsed && typeof parsed === 'object') return parsed
571
- } catch {
572
- // fall through to {}
573
- }
574
- }
575
- return {}
576
- }
577
-
578
- // view/edit/create send the file path under 'path'; every token-goat handler these
579
- // three tools reach only ever looks for 'file_path' (getFilePath in hooks_common.ts).
580
- // Keyed by the ORIGINAL Copilot tool name (before TOOL_TO_TG renames it) since that's
581
- // the name toolArgs' shape is keyed to, not token-goat's internal tool name.
582
- const FILE_PATH_ARG_KEY = {
583
- view: 'path',
584
- edit: 'path',
585
- create: 'path',
586
- }
587
-
588
- // read_bash/read_powershell send the background shell's id under 'shellId' (confirmed
589
- // against the poller's input schema in the shipping 1.0.80 app.js: {shellId, delay});
590
- // postBashOutputHandler reads 'bash_id'. Without this the tool-name mapping above would
591
- // be inert -- the handler bails on a missing bash_id and every poll would keep costing
592
- // the full accumulated output. Same keying convention as FILE_PATH_ARG_KEY: the ORIGINAL
593
- // Copilot tool name, since that is what the argument shape belongs to.
594
- const POLL_ID_ARG_KEY = {
595
- read_bash: 'shellId',
596
- read_powershell: 'shellId',
597
- }
598
-
599
- // Copilot spawns a brand-new process for every single hook invocation (no long-lived plugin
600
- // process the way OpenClaw's is -- OPENCLAW_HOOK_SCRIPT's own \`copilot-\${process.pid}-\${Date.now()}\`
601
- // fallback is safe there specifically because that process lives for the whole session, so the
602
- // pid stays constant across calls). If Copilot ever omits \`sessionId\` from a payload, falling
603
- // back to \`process.pid\` here would mint a DIFFERENT id on every single call for what's really
604
- // the same session, since process.pid varies per invocation -- breaking token-goat's
605
- // session-based dedup/state ledger, which never accumulates across calls as a result. Derive a
606
- // stable id instead from the one thing that's actually constant across calls for the same
607
- // session: the working directory Copilot reports. That field is \`workingDirectory\`, declared
608
- // required on BaseHookInput in copilot-sdk/types.d.ts since 1.0.76, so it is present on EVERY
609
- // hook event. This previously read \`payload.cwd\`, which Copilot has never sent under any name in
610
- // any version -- the key simply did not exist, so this derived every fallback id from
611
- // process.cwd() instead and \`canonical.cwd\` below was undefined on every single call. It went
612
- // unnoticed because process.cwd() happens to be the project directory Copilot spawns the hook in,
613
- // so the fallback was accidentally right; nothing about that was by design. \`cwd\` is still read
614
- // as a secondary in case a future version adds it under the shorter name.
615
- function stableFallbackSessionId(cwd) {
616
- const key = typeof cwd === 'string' && cwd ? cwd : process.cwd()
617
- const hash = require('node:crypto').createHash('sha256').update(key).digest('hex').slice(0, 16)
618
- return 'copilot-' + hash
619
- }
620
-
621
- function remapToolInput(copilotToolName, input) {
622
- if (!input || typeof input !== 'object') return input
623
- let out = input
624
- // Add the canonical key alongside the original rather than renaming it, so nothing that
625
- // might read the original 'path'/'shellId' key elsewhere (e.g. a future handler) loses it.
626
- const folded = foldToolName(copilotToolName)
627
- const pathKey = FILE_PATH_ARG_KEY[copilotToolName] || (folded === 'view' || folded === 'edit' || folded === 'create' ? 'path' : undefined)
628
- if (pathKey !== undefined && pathKey in out) {
629
- out = Object.assign({}, out, { file_path: out[pathKey] })
630
- }
631
- const idKey = POLL_ID_ARG_KEY[copilotToolName] || (folded === 'readbash' || folded === 'readpowershell' ? 'shellId' : undefined)
632
- if (idKey !== undefined && idKey in out) {
633
- out = Object.assign({}, out, { bash_id: out[idKey] })
634
- }
635
- if (Array.isArray(out.view_range) && out.view_range.length >= 1) {
636
- const rawStart = Number(out.view_range[0])
637
- if (Number.isFinite(rawStart) && rawStart >= 1) {
638
- const mapped = { offset: Math.floor(rawStart) }
639
- if (out.view_range.length >= 2) {
640
- const rawEnd = Number(out.view_range[1])
641
- if (Number.isFinite(rawEnd) && rawEnd >= rawStart) {
642
- mapped.limit = Math.floor(rawEnd - rawStart + 1)
643
- }
644
- }
645
- out = Object.assign({}, out, mapped)
646
- }
647
- }
648
- if ('old_str' in out && !('old_string' in out)) {
649
- out = Object.assign({}, out, { old_string: out.old_str })
650
- }
651
- if ('new_str' in out && !('new_string' in out)) {
652
- out = Object.assign({}, out, { new_string: out.new_str })
653
- }
654
- return out
655
- }
656
-
657
- // Attempts the in-process hook call: import()s dist/token-goat-hook.mjs (a sibling of
658
- // the baked token-goat entry path, built with zero load-time side effects -- unlike
659
- // the CLI entry, which runs the full argv-parsing CLI as a side effect of being
660
- // loaded) and calls its exported relayInProcess() directly, avoiding a second node
661
- // process spawn entirely. Returns undefined (triggering the spawnSync fallback below)
662
- // when entryPath is absent, the sibling file doesn't exist (an older install predating
663
- // this file), or anything else goes wrong -- this must never throw.
664
- async function tryInProcess(entryPath, tgEvent, canonical, harness) {
665
- if (!entryPath) return undefined
666
- try {
667
- const hookLibPath = path.join(path.dirname(entryPath), 'token-goat-hook.mjs')
668
- if (!require('node:fs').existsSync(hookLibPath)) return undefined
669
- const mod = await import(pathToFileURL(hookLibPath).href)
670
- process.env.TOKEN_GOAT_HARNESS_OVERRIDE = harness
671
- // Which hooks directory VS Code loaded this shim from is the only thing separating a
672
- // user-scope copy from a project-scope one -- the two files are byte-identical and so are
673
- // their payloads. vscode_duplicate.ts needs it to stand the redundant copy down.
674
- if (harness === 'vscode') process.env.TOKEN_GOAT_VSCODE_HOOKS_DIR = __dirname
675
- return await mod.relayInProcess(tgEvent, canonical)
676
- } catch {
677
- return undefined
678
- }
679
- }
680
-
681
- // VS Code's built-in agent reads this same hooks file (~/.copilot/hooks and .github/hooks are both
682
- // on its hook-source list) and runs these same entries, but it sends Claude Code's payload shape:
683
- // hook_event_name, session_id, tool_name, tool_input, tool_response. Copilot CLI's payload never
684
- // carries hook_event_name and always names its tool in toolName, so that pair tells them apart.
685
- // The payload goes to token-goat as-is under the vscode harness, which does its own tool-name
686
- // mapping and writes a response VS Code reads directly, so nothing here translates it.
687
- function isVscodePayload(payload) {
688
- return payload !== null && typeof payload === 'object' && typeof payload.hook_event_name === 'string' && payload.toolName === undefined
689
- }
690
-
691
- async function relayVscode(entryPath, tgEvent, payload) {
692
- // VS Code omits session_id when the chat has no session resource yet; key the state on the workspace instead, the same fallback the Copilot path uses.
693
- const input = typeof payload.session_id === 'string' && payload.session_id !== ''
694
- ? payload
695
- : Object.assign({}, payload, { session_id: stableFallbackSessionId(process.cwd()) })
696
- let stdout = await tryInProcess(entryPath, tgEvent, input, 'vscode')
697
- if (stdout === undefined) {
698
- // tgEvent is a value of the closed COPILOT_TO_TG_EVENT map (gated in main) and the harness is a literal, so nothing caller-controlled reaches the shell string below.
699
- const spawnOpts = {
700
- input: JSON.stringify(input),
701
- encoding: 'utf8',
702
- windowsHide: true,
703
- timeout: 3000,
704
- killSignal: 'SIGKILL',
705
- maxBuffer: 32 * 1024 * 1024,
706
- env: Object.assign({}, process.env, { TOKEN_GOAT_HARNESS_OVERRIDE: 'vscode', TOKEN_GOAT_VSCODE_HOOKS_DIR: __dirname }),
707
- }
708
- const res = entryPath
709
- ? spawnSync(process.execPath, [entryPath, 'hook', tgEvent, '--harness', 'vscode'], spawnOpts)
710
- : spawnSync('token-goat hook ' + tgEvent + ' --harness vscode', Object.assign({ shell: true }, spawnOpts))
711
- if (res.status !== 0 || !res.stdout) return '{}'
712
- stdout = res.stdout
713
- }
714
- try {
715
- const parsed = JSON.parse(stdout)
716
- return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? JSON.stringify(parsed) : '{}'
717
- } catch {
718
- return '{}'
719
- }
720
- }
721
-
722
- async function main() {
723
- const copilotEvent = process.argv[2] || ''
724
-
725
- // hasOwnProperty, not a bare lookup: the map is a plain object literal, so eight keys it never
726
- // declares ('constructor', 'toString', '__proto__' and the rest of Object.prototype) come back
727
- // truthy and sail past the check below. tgEvent is then a Function, and 'token-goat hook ' +
728
- // tgEvent is concatenated into a shell command string by the fallback at the bottom of this
729
- // shim. No member of Object.prototype stringifies with a shell metacharacter, so this was a
730
- // failed spawn rather than a second command -- but argv reaching a shell string through a gate
731
- // that reads like it stops it is the shape worth closing, not the payload that happened to be
732
- // harmless. The four shims built on shim_common.ts validate against a closed Set, which has no
733
- // equivalent hole.
734
- const tgEvent = Object.prototype.hasOwnProperty.call(COPILOT_TO_TG_EVENT, copilotEvent)
735
- ? COPILOT_TO_TG_EVENT[copilotEvent]
736
- : undefined
737
- if (!tgEvent) {
738
- process.stdout.write('{}')
739
- return
740
- }
741
-
742
- let raw = ''
743
- try {
744
- raw = require('node:fs').readFileSync(0, 'utf8')
745
- } catch {
746
- process.stdout.write('{}')
747
- return
748
- }
749
-
750
- let payload
751
- try {
752
- payload = JSON.parse(raw)
753
- } catch {
754
- process.stdout.write('{}')
755
- return
756
- }
757
-
758
- if (isVscodePayload(payload)) {
759
- process.stdout.write(await relayVscode(process.argv[3], tgEvent, payload))
760
- return
761
- }
762
-
763
- const toolName = payload && payload.toolName
764
- const canonical = {
765
- session_id:
766
- (payload && payload.sessionId) ||
767
- stableFallbackSessionId(payload && (payload.workingDirectory || payload.cwd)),
768
- cwd: payload && (payload.workingDirectory || payload.cwd),
769
- }
770
-
771
- // Subagent correlation and W3C Trace Context propagation from Copilot CLI payloads
772
- const agentId = payload && (payload.agent_id || payload.agentId)
773
- if (typeof agentId === 'string' && agentId !== '') {
774
- canonical.agent_id = agentId
775
- }
776
- const traceparent = payload && (payload.traceparent || payload.traceParent)
777
- if (typeof traceparent === 'string' && traceparent !== '') {
778
- canonical.traceparent = traceparent
779
- }
780
- const tracestate = payload && (payload.tracestate || payload.traceState)
781
- if (typeof tracestate === 'string' && tracestate !== '') {
782
- canonical.tracestate = tracestate
783
- }
784
-
785
- // userPromptSubmitted only: Copilot declares \`prompt\` required on UserPromptSubmittedHookInput.
786
- // hooks_session.ts's userPromptSubmitHandler reads it as \`event.raw['prompt']\` and gates every
787
- // branch it has on the text, so without this it saw '' on every Copilot prompt and the
788
- // embedded-skill dedup hint could never fire. Same shape as the postToolUseFailure \`error\`
789
- // drop: a required field the canonical builder simply did not list.
790
- if (typeof (payload && payload.prompt) === 'string' && payload.prompt !== '') {
791
- canonical.prompt = payload.prompt
792
- }
793
- let originalToolArgs = {}
794
- if (toolName) {
795
- originalToolArgs = parseMaybeJsonObject(payload && payload.toolArgs)
796
- canonical.tool_name = resolveCanonicalToolName(toolName)
797
- canonical.tool_input = remapToolInput(toolName, originalToolArgs)
798
- }
799
-
800
- // postToolUse only: confirmed via https://docs.github.com/en/copilot/reference/hooks-reference
801
- // that Copilot's toolResult is an object ({resultType, textResultForLlm}), not a bare string
802
- // or array. Without this, token-goat's tool_response consumers (hooks_read.ts's
803
- // extractReadOutput, hooks_bash.ts's extractBashOutput, etc. -- all of which check for a
804
- // string or an object keyed by output/content/text/body) never see any content, so
805
- // post-read/post-bash stats stay empty no matter how many tool calls happen. Extract the
806
- // LLM-facing text directly rather than forwarding the raw object, since textResultForLlm
807
- // isn't one of those recognized object keys.
808
- // postToolUseFailure only: Copilot's PostToolUseFailureHookInput (copilot-sdk/types.d.ts:1042)
809
- // carries {toolName, toolArgs, error} and no toolResult at all -- the failure text lives in
810
- // \`error\`, a plain string. Without forwarding it, hooks_tool_failure.ts's extractFailureText
811
- // finds nothing to key on and the repeat-failure brake returns pass on every single call: wired,
812
- // green, and doing nothing. Found by driving the installed shim rather than by a test, because
813
- // the handler's own tests hand it a raw payload that already has the field.
814
- if (typeof (payload && payload.error) === 'string' && payload.error !== '') {
815
- canonical.error = payload.error
816
- }
817
-
818
- const rawResult = payload && payload.toolResult
819
- if (rawResult && typeof rawResult === 'object') {
820
- const tr = rawResult
821
- // text_result_for_llm: Copilot's docs also describe a "VS Code compatible" snake_case
822
- // wire format (tool_result.text_result_for_llm) alongside the camelCase one above; try
823
- // both rather than assuming only the camelCase shape ever reaches this shim.
824
- const text = typeof tr.textResultForLlm === 'string' ? tr.textResultForLlm : tr.text_result_for_llm
825
- if (typeof text === 'string') canonical.tool_response = text
826
- }
827
-
828
- // process.argv[3], when present, is the absolute path to the token-goat CLI entry that ran
829
- // \`token-goat install --copilot\` (baked in by hookCommandFor in copilot_cli_install.ts).
830
- // Invoking it directly via process.execPath sidesteps PATH/cmd.exe resolution for this
831
- // inner call too -- confirmed live-production root cause of an *intermittent* (not the
832
- // original, already-fixed github/copilot-cli#4001) "(hook errored)" deny-all: even once the
833
- // outer command launches via its own baked process.execPath, this inner call still shelled
834
- // out to a bare \`token-goat\`, which depends on the npm global bin being on whatever PATH
835
- // Copilot spawns this hook subprocess with. Falls back to the old PATH-based shell:true
836
- // invocation when argv[3] is absent (an older cached hook config still pointing at a
837
- // freshly-reinstalled shim, or a direct dev/test invocation), so this is a pure hardening,
838
- // never a behavior break for configs installed before this fix. An args array (not a
839
- // template string) is safe here specifically because entryPath and tgEvent never touch a
840
- // shell -- no DEP0190 concern, unlike the shell:true fallback below.
841
- const entryPath = process.argv[3]
842
- // Try the in-process hook lib first (tryInProcess above), which avoids spawning a
843
- // second node process altogether. If that's unavailable, invoking the entry directly
844
- // via process.execPath sidesteps PATH/cmd.exe resolution for this inner call, per the
845
- // note above this function's original single-spawn form documented. A 3000ms
846
- // timeout/killSignal on both spawnSync fallbacks keeps them well under Copilot CLI's
847
- // own hook timeout budget (~30000ms), so token-goat degrades to its own fail-open
848
- // '{}' rather than being force-killed by Copilot first.
849
- let stdout = await tryInProcess(entryPath, tgEvent, canonical, 'copilot_cli')
850
- if (stdout === undefined) {
851
- const res = entryPath
852
- ? spawnSync(process.execPath, [entryPath, 'hook', tgEvent], {
853
- input: JSON.stringify(canonical),
854
- encoding: 'utf8',
855
- windowsHide: true,
856
- timeout: 3000,
857
- killSignal: 'SIGKILL',
858
- maxBuffer: 32 * 1024 * 1024,
859
- env: Object.assign({}, process.env, { TOKEN_GOAT_HARNESS_OVERRIDE: 'copilot_cli' }),
860
- })
861
- : spawnSync('token-goat hook ' + tgEvent, {
862
- input: JSON.stringify(canonical),
863
- encoding: 'utf8',
864
- shell: true,
865
- windowsHide: true,
866
- timeout: 3000,
867
- killSignal: 'SIGKILL',
868
- maxBuffer: 32 * 1024 * 1024,
869
- env: Object.assign({}, process.env, { TOKEN_GOAT_HARNESS_OVERRIDE: 'copilot_cli' }),
870
- })
871
- if (res.status !== 0 || !res.stdout) {
872
- process.stdout.write('{}')
873
- return
874
- }
875
- stdout = res.stdout
876
- }
877
-
878
- let resp
879
- try {
880
- resp = JSON.parse(stdout)
881
- } catch {
882
- process.stdout.write('{}')
883
- return
884
- }
885
-
886
- process.stdout.write(JSON.stringify(translate(copilotEvent, resp, toolName, originalToolArgs, payload)))
887
- }
888
-
889
- ${MATERIALIZE_SHRUNK_IMAGE_JS}
890
-
891
- function translate(copilotEvent, resp, toolName, originalToolArgs, payload) {
892
- if (copilotEvent === 'preToolUse') {
893
- const hso = resp && resp.hookSpecificOutput
894
- const denied = resp && (resp.decision === 'block' || (hso && hso.permissionDecision === 'deny'))
895
- if (denied) {
896
- const reason =
897
- (resp && resp.reason) || (hso && hso.permissionDecisionReason) || 'blocked by token-goat'
898
- return { permissionDecision: 'deny', permissionDecisionReason: reason }
899
- }
900
- const updated = hso && hso.updatedInput
901
- if (updated && typeof updated === 'object') {
902
- return { modifiedArgs: updated }
903
- }
904
- // Image shrink has no context channel on this event (Copilot's preToolUse output schema carries no additionalContext; docs/hook-channel-matrix.md footnote 7) -- translate it into a rewritten view path pointing at a materialized shrunk copy instead, via the same modifiedArgs channel the updatedInput branch above already uses. modifiedArgs REPLACES the tool call's args wholesale (ESr in the 1.0.80 bundle, see this module's header docblock), so the full original toolArgs are spread and only Copilot's own path key is swapped.
905
- if (toolName === 'view' || foldToolName(toolName) === 'view') {
906
- const shrunkPath = materializeShrunkImage(extractContext(resp))
907
- if (shrunkPath) return { modifiedArgs: Object.assign({}, originalToolArgs, { path: shrunkPath }) }
908
- }
909
- return {}
910
- }
911
-
912
- if (copilotEvent === 'postToolUse') {
913
- // modifiedResult is honored on success: rewriteOutput (compression, fencing, image shrink) reaches LLM.
914
- // additionalContext is dropped on Copilot CLI JS path; we fold context into modifiedResult.textResultForLlm.
915
- const hso = resp && resp.hookSpecificOutput
916
- const updatedToolOutput = hso && hso.updatedToolOutput
917
- const context = extractContext(resp)
918
- const out = {}
919
- if (typeof updatedToolOutput === 'string') {
920
- out.modifiedResult = { resultType: 'success', textResultForLlm: updatedToolOutput }
921
- } else if (context) {
922
- const rawResult = payload && payload.toolResult
923
- const originalText = rawResult && (typeof rawResult.textResultForLlm === 'string' ? rawResult.textResultForLlm : typeof rawResult.text_result_for_llm === 'string' ? rawResult.text_result_for_llm : undefined)
924
- if (typeof originalText === 'string') {
925
- out.modifiedResult = { resultType: 'success', textResultForLlm: originalText + '\\n\\n[token-goat: ' + context + ']' }
926
- }
927
- }
928
- if (context) out.additionalContext = context
929
- return out
930
- }
931
-
932
- if (copilotEvent === 'postToolUseFailure') {
933
- // The failed-tool twin of postToolUse, and the only response field it accepts is
934
- // additionalContext: the shipped copilot-sdk/types.d.ts says "modifiedResult or suppressOutput
935
- // are not honored for failure hooks", so nothing here can fence, compress or shrink the failed
936
- // output -- that gap is real and stays open. What is NOT open, and was the reason this event
937
- // went unwired for so long, is whether additionalContext reaches the model at all. It does:
938
- // app.js 1.0.80 at offset 2043380 either folds it into textResultForLlm (when
939
- // appendFailureContextToToolResult is set) or has the native
940
- // hookAppendPostToolUseFailureContext push {content, source:'system'} onto
941
- // toolResult.newMessages. That is the exact opposite of postToolUse, whose additionalContext
942
- // the JS side drops on the floor, so neither event's behaviour generalises to the other.
943
- //
944
- // Because this channel spends tokens rather than saving them, the handler behind it
945
- // (hooks_tool_failure.ts) is silent on a first failure and speaks only on an exact repeat.
946
- const context = extractContext(resp)
947
- if (context) return { additionalContext: context }
948
- return {}
949
- }
950
-
951
- if (copilotEvent === 'sessionStart') {
952
- // sessionStart has no tool result to modify -- only additionalContext applies, and it's the
953
- // one channel that reaches the model before it picks its first read tool, so this is where
954
- // the routing reminder has to land.
955
- const context = extractContext(resp)
956
- if (context) return { additionalContext: context }
957
- return {}
958
- }
959
-
960
- if (copilotEvent === 'agentStop' || copilotEvent === 'subagentStop') {
961
- // Confirmed against the hooks reference doc: the only accepted response
962
- // shape for these two events is {decision, reason} -- additionalContext
963
- // is not part of their schema and Copilot silently ignores it there.
964
- // token-goat's internal 'stop'/'subagent_stop' handlers never return a
965
- // real deny today (subagentStopHandler only ever logs and passes), but a
966
- // future deny is mapped through here rather than silently dropped.
967
- if (resp && resp.decision === 'block') {
968
- const reason = (resp && resp.reason) || 'blocked by token-goat'
969
- return { decision: 'block', reason: reason }
970
- }
971
- return { decision: 'allow' }
972
- }
973
-
974
- if (copilotEvent === 'userPromptSubmitted') {
975
- // Copilot's own hooks reference says command-hook output here "is dropped, including
976
- // modifiedPrompt", and this branch used to believe it and return nothing. That is wrong for
977
- // additionalContext on 1.0.80, established by experiment rather than by reading: a config-file
978
- // command hook (at <cwd>/.github/hooks/, the project scope -- the user scope ~/.copilot/hooks/
979
- // was never exercised) returned {"additionalContext":"<marker>"} and the marker turned up
980
- // verbatim inside the session's user.message.transformedContent, wrapped in a
981
- // <system_reminder> block. What settles that it reached the model rather than only the on-disk
982
- // record is the billing: the provider's returned usage charged ~140 input tokens for a turn
983
- // whose raw content field is 35 bytes, so the marker's bytes were paid for whichever field
984
- // carried them. (The weaker argument first offered for this -- that transformedContent carries
985
- // an envelope the content field lacks, 29 B vs 195 B -- was measured on the control turn that
986
- // had no marker in it, and proves nothing about the marker.) That the model sees it is the
987
- // whole point: hook.start/hook.end records also persist and reach nothing.
988
- //
989
- // Scope of the finding, stated honestly: demonstrated ONCE on 1.0.80, not shown to be
990
- // reliable. Of two turns in that experiment, one delivered the marker and one fired a
991
- // userPromptSubmitted hook that produced no output and never ran the script; no explanation
992
- // was established and the rate is unknown. A hint that silently fails to arrive costs nothing
993
- // and breaks nothing here, which is why the direct return is still the right default.
994
- //
995
- // modifiedPrompt is NOT claimed to work and is not wanted: rewriting a user's prompt is far
996
- // more invasive than anything token-goat does, so only additionalContext is forwarded.
997
- //
998
- // This is also the write end of any post-compaction channel. Copilot has no postCompact hook.
999
- // Whether the summary is recoverable from events.jsonl is NOT settled -- the emit()/
1000
- // emitEphemeral() distinction does not gate the writer, and the real decision is in native
1001
- // code; see COPILOT_NO_POST_COMPACT_REASON in ../bridges_status.ts for what was and was not
1002
- // established. The read end that IS confirmed is preCompact, which fires as a notification, so
1003
- // a manifest can be built there and drained here without reading the event log at all.
1004
- const context = extractContext(resp)
1005
- if (context) return { additionalContext: context }
1006
- return {}
1007
- }
1008
-
1009
- // preCompact is the genuine notification-only case, and the contrast with userPromptSubmitted
1010
- // above is why this fallthrough is worth a comment at all. The hooks reference marks it
1011
- // "No -- notification only" for output processing, and unlike the additionalContext claim that
1012
- // turned out to be false, this one is confirmed in the shipping bundle: both preCompact call
1013
- // sites in app.js (1.0.79 and re-checked in 1.0.80) await the hook and never assign its result.
1014
- // There is no field to aim at here, so nothing to reconsider on the next version bump.
1015
- return {}
1016
- }
1017
-
1018
- function extractContext(resp) {
1019
- const hso = resp && resp.hookSpecificOutput
1020
- if (hso && typeof hso.additionalContext === 'string') return hso.additionalContext
1021
- if (resp && typeof resp.systemMessage === 'string') return resp.systemMessage
1022
- return undefined
1023
- }
1024
-
1025
- // Hard outer safety net, on top of main()'s own per-step try/catch fallbacks
1026
- // (JSON.parse, readFileSync, spawnSync): a hook error must never itself cause
1027
- // Copilot's fail-closed "(hook errored)" behavior, which denies EVERY tool
1028
- // call unconditionally (the exact live-production failure mode behind
1029
- // github/copilot-cli#4001). Any uncaught exception anywhere in this script --
1030
- // including one a future code path adds that the existing per-step guards
1031
- // don't anticipate -- still guarantees stdout gets valid JSON and the
1032
- // process exits 0. process.exitCode is set explicitly and unconditionally at
1033
- // the very end of every path so nothing upstream (e.g. an unhandled-rejection
1034
- // warning in some Node versions nudging exit-code inference) can flip it.
1035
- main()
1036
- .catch(() => {
1037
- try {
1038
- process.stdout.write('{}')
1039
- } catch {
1040
- // stdout itself is broken; nothing more can be done here.
1041
- }
1042
- })
1043
- .finally(() => {
1044
- process.exitCode = 0
1045
- })
1046
- `;
1047
- function foldToolName(name) {
1048
- return typeof name === "string" ? name.toLowerCase().replace(/[_-]/g, "") : "";
1049
- }
1050
- var TOOL_TO_TG_MAPPING = {
1051
- bash: "Bash",
1052
- powershell: "Bash",
1053
- read_bash: "BashOutput",
1054
- read_powershell: "BashOutput",
1055
- view: "Read",
1056
- create: "Write",
1057
- edit: "Edit",
1058
- web_fetch: "WebFetch",
1059
- web_search: "WebSearch",
1060
- grep: "Grep",
1061
- glob: "Glob",
1062
- skill: "Skill",
1063
- exit_plan_mode: "ExitPlanMode"
1064
- };
1065
- var FOLDED_MAPPING = {};
1066
- for (const [k, v] of Object.entries(TOOL_TO_TG_MAPPING)) {
1067
- FOLDED_MAPPING[foldToolName(k)] = v;
1068
- FOLDED_MAPPING[foldToolName(v)] = v;
1069
- }
1070
-
1071
- // src/bridges/copilot_cli_install.ts
1072
- var COPILOT_CLI_HOOK_EVENTS = [
1073
- "sessionStart",
1074
- "preToolUse",
1075
- "postToolUse",
1076
- "preCompact",
1077
- "agentStop",
1078
- "subagentStop",
1079
- "userPromptSubmitted",
1080
- "postToolUseFailure"
1081
- ];
1082
- function copilotCliUserRoot() {
1083
- const override = process.env["COPILOT_HOME"];
1084
- if (override !== void 0 && override.trim() !== "") return path3.resolve(override);
1085
- return path3.join(os2.homedir(), ".copilot");
1086
- }
1087
- function copilotCliUserHooksDir() {
1088
- return path3.join(copilotCliUserRoot(), "hooks");
1089
- }
1090
- function copilotCliProjectHooksDir() {
1091
- return path3.join(process.cwd(), ".github", "hooks");
1092
- }
1093
- function copilotCliCacheRoot() {
1094
- const override = process.env["COPILOT_CACHE_HOME"];
1095
- if (override !== void 0 && override.trim() !== "") return path3.resolve(override);
1096
- const home = os2.homedir();
1097
- if (process.platform === "darwin") return path3.join(home, "Library", "Caches", "copilot");
1098
- if (process.platform === "win32") {
1099
- const local = process.env["LOCALAPPDATA"];
1100
- const base2 = local !== void 0 && local.trim() !== "" ? local : path3.join(home, ".cache");
1101
- return path3.join(base2, "copilot");
1102
- }
1103
- const xdg = process.env["XDG_CACHE_HOME"];
1104
- const base = xdg !== void 0 && xdg.trim() !== "" ? xdg : path3.join(home, ".cache");
1105
- return path3.join(base, "copilot");
1106
- }
1107
- function copilotCliMcpToolsDir() {
1108
- return path3.join(copilotCliCacheRoot(), "mcp-tools");
1109
- }
1110
- function copilotCliHooksDir(opts = {}) {
1111
- return opts.local === true ? copilotCliProjectHooksDir() : copilotCliUserHooksDir();
1112
- }
1113
- function copilotCliConfigPath(opts = {}) {
1114
- return path3.join(copilotCliHooksDir(opts), HOOKS_CONFIG_FILE);
1115
- }
1116
- function copilotCliScriptPath(opts = {}) {
1117
- return path3.join(copilotCliHooksDir(opts), HOOKS_SCRIPT_FILE);
1118
- }
1119
- function copilotCliInstructionsPath(opts = {}) {
1120
- return path3.join(path3.dirname(copilotCliHooksDir(opts)), "copilot-instructions.md");
1121
- }
1122
- var COPILOT_INSTRUCTIONS_BEGIN = "<!-- token-goat-begin -->";
1123
- var COPILOT_INSTRUCTIONS_END = "<!-- token-goat-end -->";
1124
- function buildCopilotInstructionsBlock() {
1125
- return stripInlineCodeSpans(
1126
- buildGuidanceBlock({
1127
- beginMarker: COPILOT_INSTRUCTIONS_BEGIN,
1128
- endMarker: COPILOT_INSTRUCTIONS_END,
1129
- fallbackToolClause: "Copilot CLI's native `view`, `grep`, and `glob` tools (with PowerShell commands `Get-Content`/`Select-String` as search fallbacks)",
1130
- gdrive: loadConfig().gdrive.enabled
1131
- })
1132
- );
1133
- }
1134
- function stripInlineCodeSpans(text) {
1135
- return text.replace(/`([^`]+)`/g, "$1");
1136
- }
1137
- function writeCopilotInstructionsBlock(p) {
1138
- const changed = upsertDelimitedBlock(p, COPILOT_INSTRUCTIONS_BEGIN, COPILOT_INSTRUCTIONS_END, buildCopilotInstructionsBlock());
1139
- const visualStudioChanged = syncVisualStudioProjectGuidance(p);
1140
- return changed || visualStudioChanged;
1141
- }
1142
- function stripCopilotInstructionsBlock(p) {
1143
- const removed = stripDelimitedBlock(p, COPILOT_INSTRUCTIONS_BEGIN, COPILOT_INSTRUCTIONS_END);
1144
- if (removed) syncVisualStudioProjectGuidance(p);
1145
- return removed;
1146
- }
1147
- function hookPowershellCommandFor(scriptPath, event) {
1148
- return `& ${hookPowershellCommand(scriptPath, event)}`;
1149
- }
1150
- var HOOK_TIMEOUT_SEC = 60;
1151
- var ALLOWED_ENV_VARS = [
1152
- "TRACEPARENT",
1153
- "TRACESTATE",
1154
- "COPILOT_HOME",
1155
- "COPILOT_CACHE_HOME",
1156
- "TOKEN_GOAT_LOG"
1157
- ];
1158
- function buildConfig(scriptPath) {
1159
- const hooks = {};
1160
- for (const event of COPILOT_CLI_HOOK_EVENTS) {
1161
- hooks[event] = [
1162
- {
1163
- type: "command",
1164
- command: hookCommandFor(scriptPath, event),
1165
- bash: hookCommandFor(scriptPath, event),
1166
- powershell: hookPowershellCommandFor(scriptPath, event),
1167
- timeoutSec: HOOK_TIMEOUT_SEC,
1168
- allowedEnvVars: [...ALLOWED_ENV_VARS]
1169
- }
1170
- ];
1171
- }
1172
- return { version: 1, hooks };
1173
- }
1174
- var HOOKS_CONFIG_FILE = "token-goat.json";
1175
- var HOOKS_SCRIPT_FILE = "token-goat-shim.js";
1176
- var HOOKS_OWNERS_FILE = "token-goat.owners";
1177
- function copilotHooksOwnersPath(hooksDir) {
1178
- return path3.join(hooksDir, HOOKS_OWNERS_FILE);
1179
- }
1180
- function copilotHooksFilePaths(hooksDir) {
1181
- return [path3.join(hooksDir, HOOKS_CONFIG_FILE), path3.join(hooksDir, HOOKS_SCRIPT_FILE), copilotHooksOwnersPath(hooksDir)];
1182
- }
1183
- function readCopilotHooksOwners(hooksDir) {
1184
- const owners = /* @__PURE__ */ new Set();
1185
- let text;
1186
- try {
1187
- text = fs3.readFileSync(copilotHooksOwnersPath(hooksDir), "utf8");
1188
- } catch {
1189
- if (fs3.existsSync(path3.join(hooksDir, HOOKS_CONFIG_FILE))) owners.add("copilot");
1190
- return owners;
1191
- }
1192
- for (const line of text.split(/\r?\n/)) {
1193
- const name = line.trim();
1194
- if (name === "copilot" || name === "vscode") owners.add(name);
1195
- }
1196
- return owners;
1197
- }
1198
- function installCopilotHooksFile(hooksDir, owner) {
1199
- const configPath = path3.join(hooksDir, HOOKS_CONFIG_FILE);
1200
- const scriptPath = path3.join(hooksDir, HOOKS_SCRIPT_FILE);
1201
- const owners = readCopilotHooksOwners(hooksDir);
1202
- const scriptChanged = writeIfDifferent(scriptPath, COPILOT_CLI_HOOK_SCRIPT);
1203
- const desiredText = JSON.stringify(buildConfig(scriptPath), null, 2) + "\n";
1204
- const configChanged = writeIfDifferent(configPath, desiredText, true);
1205
- recordCreatedConfig(configPath);
1206
- owners.add(owner);
1207
- const ownersChanged = writeIfDifferent(copilotHooksOwnersPath(hooksDir), [...owners].sort().join("\n") + "\n");
1208
- return { configPath, scriptPath, changed: scriptChanged || configChanged || ownersChanged };
1209
- }
1210
- function releaseCopilotHooksFile(hooksDir, owner, keepBackups = false) {
1211
- const ownersPath = copilotHooksOwnersPath(hooksDir);
1212
- const hadSidecar = fs3.existsSync(ownersPath);
1213
- const owners = readCopilotHooksOwners(hooksDir);
1214
- if (!owners.has(owner) && (hadSidecar || owner !== "copilot")) return false;
1215
- owners.delete(owner);
1216
- if (owners.size > 0) {
1217
- writeIfDifferent(ownersPath, [...owners].sort().join("\n") + "\n");
1218
- return true;
1219
- }
1220
- const configRemoved = removeFileInScope(path3.join(hooksDir, HOOKS_CONFIG_FILE));
1221
- if (!keepBackups) removeCreatedBackups(path3.join(hooksDir, HOOKS_CONFIG_FILE));
1222
- takeCreatedConfig(path3.join(hooksDir, HOOKS_CONFIG_FILE));
1223
- const scriptRemoved = removeFileInScope(path3.join(hooksDir, HOOKS_SCRIPT_FILE));
1224
- const ownersRemoved = removeFileInScope(ownersPath);
1225
- return configRemoved || scriptRemoved || ownersRemoved;
1226
- }
1227
- function installCopilotCli(opts = {}) {
1228
- return withInstallScope(projectScopeRoot(opts), () => installCopilotCliScoped(opts));
1229
- }
1230
- function installCopilotCliScoped(opts) {
1231
- const instructionsPath = copilotCliInstructionsPath(opts);
1232
- const hooks = installCopilotHooksFile(copilotCliHooksDir(opts), "copilot");
1233
- const instructionsChanged = writeCopilotInstructionsBlock(instructionsPath);
1234
- return {
1235
- configPath: hooks.configPath,
1236
- scriptPath: hooks.scriptPath,
1237
- instructionsPath,
1238
- alreadyInstalled: !hooks.changed && !instructionsChanged
1239
- };
1240
- }
1241
- function uninstallCopilotCliScope(opts) {
1242
- return withInstallScope(projectScopeRoot(opts), () => uninstallCopilotCliScopeInner(opts));
1243
- }
1244
- function uninstallCopilotCliScopeInner(opts) {
1245
- let removedAny = releaseCopilotHooksFile(copilotCliHooksDir(opts), "copilot");
1246
- if (stripCopilotInstructionsBlock(copilotCliInstructionsPath(opts))) {
1247
- removedAny = true;
1248
- }
1249
- return removedAny;
1250
- }
1251
- function uninstallCopilotCli(opts = {}) {
1252
- if (opts.local === true) {
1253
- return uninstallCopilotCliScope({ local: true });
1254
- }
1255
- const userRemoved = uninstallCopilotCliScope({ local: false });
1256
- const localRemoved = uninstallCopilotCliScope({ local: true });
1257
- return userRemoved || localRemoved;
1258
- }
1259
- function isCopilotCliInstalled(opts = {}) {
1260
- if (!fs3.existsSync(copilotCliConfigPath(opts)) || !fs3.existsSync(copilotCliScriptPath(opts))) {
1261
- return false;
1262
- }
1263
- if (!readCopilotHooksOwners(copilotCliHooksDir(opts)).has("copilot")) return false;
1264
- let instructions;
1265
- try {
1266
- instructions = fs3.readFileSync(copilotCliInstructionsPath(opts), "utf8");
1267
- } catch {
1268
- return false;
1269
- }
1270
- return instructions.includes(COPILOT_INSTRUCTIONS_BEGIN) && instructions.includes(COPILOT_INSTRUCTIONS_END);
1271
- }
1272
-
1273
- // src/hooks_cli.ts
1274
- init_define_import_meta_env();
1275
-
1276
- // src/copilot_mcp_names.ts
1277
- init_define_import_meta_env();
1278
-
1279
- // src/copilot_mcp_tools.ts
1280
- init_define_import_meta_env();
1281
- import fs4 from "node:fs";
1282
- import path4 from "node:path";
1283
-
1284
- // src/overflow_guard.ts
1285
- init_define_import_meta_env();
1286
42
 
1287
43
  // src/bash_compress.ts
1288
44
  init_define_import_meta_env();
@@ -1353,853 +109,7 @@ function stripAnsiCodes(text) {
1353
109
  return stripAnsiEscapes(text);
1354
110
  }
1355
111
 
1356
- // src/overflow_guard.ts
1357
- function estimateTokensFromLength(length) {
1358
- return Math.max(1, Math.floor(Math.max(0, length) / 3) + 1);
1359
- }
1360
- function estimateTokens(text) {
1361
- return estimateTokensFromLength(stripAnsiCodes(text).length);
1362
- }
1363
- function trimToBudget(text, budgetTokens, command) {
1364
- const markerMarginTokens = 64;
1365
- const totalTokens = estimateTokens(text);
1366
- if (totalTokens <= budgetTokens) {
1367
- return text;
1368
- }
1369
- const lines = text.split("\n");
1370
- if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
1371
- const totalLines = lines.length;
1372
- const bodyBudget = Math.max(1, budgetTokens - markerMarginTokens);
1373
- const charBudget = bodyBudget * 3;
1374
- const kept = [];
1375
- let used = 0;
1376
- for (const ln of lines) {
1377
- const stripped = stripAnsiCodes(ln);
1378
- const cost = ln.length + 1;
1379
- if (kept.length === 0 && cost > charBudget) {
1380
- const truncated = safeSlice(stripped, charBudget);
1381
- kept.push(truncated);
1382
- break;
1383
- }
1384
- if (kept.length > 0 && used + cost > charBudget) {
1385
- break;
1386
- }
1387
- kept.push(ln);
1388
- used += cost;
1389
- }
1390
- const shown = kept.length;
1391
- const hint = getHintFor(command);
1392
- const marker = `[token-goat: output capped at ~${budgetTokens} tokens to protect context \u2014 showing ${shown} of ${totalLines} lines. ${hint}]`;
1393
- return kept.join("\n") + "\n" + marker;
1394
- }
1395
- function capJsonRows(items, budgetTokens) {
1396
- const totalCount = items.length;
1397
- const charBudget = Math.max(1, budgetTokens * 3);
1398
- const kept = [];
1399
- let used = 0;
1400
- for (const item of items) {
1401
- const cost = JSON.stringify(item).length + 2;
1402
- if (kept.length > 0 && used + cost > charBudget) break;
1403
- kept.push(item);
1404
- used += cost;
1405
- }
1406
- return { items: kept, truncated: kept.length < totalCount, totalCount };
1407
- }
1408
- function getHintFor(command) {
1409
- const cmd = (command || "").toLowerCase().trim();
1410
- if (cmd === "symbol") {
1411
- return "Request a specific method (file.py::Class.method) or use --json for structured access.";
1412
- }
1413
- if (cmd === "heading" || cmd === "section") {
1414
- return "Request a narrower sub-heading, e.g. 'doc.md::Section#2'.";
1415
- }
1416
- if (cmd === "lines") {
1417
- return "Request a smaller line range, e.g. 'file.py@100-150'.";
1418
- }
1419
- if (cmd === "bash-output" || cmd === "web-output") {
1420
- return "Use --grep PATTERN, --section HEADING, or --tail N to narrow the cached output.";
1421
- }
1422
- if (cmd === "semantic") {
1423
- return "Narrow your query text or pass --limit to reduce the number of matches returned.";
1424
- }
1425
- return "Narrow your query or raise overflow_guard max_tokens in config.";
1426
- }
1427
-
1428
- // src/copilot_mcp_tools.ts
1429
- function wireShapeLength(tools) {
1430
- const wire = tools.map((tool) => {
1431
- const out = {};
1432
- if (typeof tool.name === "string") out["name"] = tool.name;
1433
- if (typeof tool.description === "string") out["description"] = tool.description;
1434
- if (tool.inputSchema !== void 0) out["inputSchema"] = tool.inputSchema;
1435
- return out;
1436
- });
1437
- return JSON.stringify(wire).length;
1438
- }
1439
- function readCopilotMcpTools(dir = copilotCliMcpToolsDir()) {
1440
- let entries;
1441
- try {
1442
- entries = fs4.readdirSync(dir);
1443
- } catch {
1444
- return { cacheFound: false, servers: [], unreadable: 0 };
1445
- }
1446
- const newest = /* @__PURE__ */ new Map();
1447
- let unreadable = 0;
1448
- for (const entry of entries) {
1449
- if (!entry.toLowerCase().endsWith(".json")) continue;
1450
- let parsed;
1451
- try {
1452
- parsed = JSON.parse(fs4.readFileSync(path4.join(dir, entry), "utf8"));
1453
- } catch {
1454
- unreadable += 1;
1455
- continue;
1456
- }
1457
- if (typeof parsed !== "object" || parsed === null) {
1458
- unreadable += 1;
1459
- continue;
1460
- }
1461
- const record = parsed;
1462
- const serverName = typeof record.serverName === "string" ? record.serverName.trim() : "";
1463
- if (serverName === "") {
1464
- unreadable += 1;
1465
- continue;
1466
- }
1467
- const tools = Array.isArray(record.tools) ? record.tools : [];
1468
- const updatedAt = typeof record.updatedAt === "string" ? record.updatedAt : "";
1469
- const definitionBytes = wireShapeLength(tools);
1470
- const candidate = {
1471
- serverName,
1472
- toolCount: tools.length,
1473
- definitionBytes,
1474
- estimatedTokens: estimateTokensFromLength(definitionBytes),
1475
- updatedAt,
1476
- // `tools` is an unchecked cast off a JSON array, so every element is
1477
- // treated as untrusted here rather than trusted to match CachedTool.
1478
- toolNames: tools.map((t) => t?.name).filter((n) => typeof n === "string" && n !== "")
1479
- };
1480
- const existing = newest.get(serverName);
1481
- if (existing === void 0 || candidate.updatedAt > existing.updatedAt) newest.set(serverName, candidate);
1482
- }
1483
- const servers = [...newest.values()].sort(
1484
- (a, b) => b.definitionBytes - a.definitionBytes || a.serverName.localeCompare(b.serverName)
1485
- );
1486
- return { cacheFound: true, servers, unreadable };
1487
- }
1488
-
1489
- // src/copilot_mcp_names.ts
1490
- var _cached = null;
1491
- function copilotMcpToolNameMap() {
1492
- if (_cached) return _cached;
1493
- const map = /* @__PURE__ */ new Map();
1494
- try {
1495
- for (const server of readCopilotMcpTools().servers) {
1496
- for (const toolName of server.toolNames) {
1497
- map.set(`${server.serverName}-${toolName}`, `mcp__${server.serverName}__${toolName}`);
1498
- }
1499
- }
1500
- } catch {
1501
- }
1502
- _cached = map;
1503
- return map;
1504
- }
1505
- function canonicalizeCopilotMcpToolName(toolName) {
1506
- return copilotMcpToolNameMap().get(toolName) ?? toolName;
1507
- }
1508
-
1509
- // src/hooks_cli.ts
1510
- var _LOG = {
1511
- warn: (msg, ...args) => console.warn(`[hooks_cli] ${msg}`, ...args),
1512
- debug: (msg, ...args) => console.error(`[hooks_cli] ${msg}`, ...args),
1513
- error: (msg, err, ...args) => console.error(`[hooks_cli] ${msg}`, ...err ? [err] : [], ...args)
1514
- };
1515
- var CODEX_TOOL_NAME_MAP = {
1516
- bash: "Bash",
1517
- // Codex's actual shell tool is named 'shell', per this repo's own Codex user-facing guidance text (codex_install.ts's buildAgentsBlock fallbackToolClause: "Codex's native `shell`, `apply_patch`, and `view_image` tools") -- the pre-existing 'bash' key above was never matched by a real Codex payload, so every real Codex shell call fell through unmapped and never reached hooks_bash.ts's Bash-only handlers; kept 'bash' too in case some Codex version or fork still sends it.
1518
- shell: "Bash",
1519
- edit_file: "Edit",
1520
- edit: "Edit",
1521
- apply_patch: "Edit",
1522
- write_file: "Write",
1523
- search_files: "Grep",
1524
- grep: "Grep",
1525
- list_files: "Glob",
1526
- glob: "Glob",
1527
- web_search: "WebFetch",
1528
- // Codex's only wired image-viewing tool (CODEX_MATCHERS in codex_install.ts wires the matcher 'view_image|Bash', with the codex_install.ts docstring calling out that 'view_image|Bash' covers image reads and shell execution together, mirroring Claude Code's Read handling) -- without this entry, tool_name stayed the literal 'view_image', which matches neither preReadHandler's nor image_shrink.ts's preReadImageHandler's toolName: 'Read' filter, so every Codex image view silently skipped image shrinking and read-session tracking entirely.
1529
- view_image: "Read"
1530
- };
1531
- var GEMINI_TOOL_NAME_MAP = {
1532
- run_shell_command: "Bash",
1533
- read_file: "Read",
1534
- read_many_files: "Read",
1535
- // Deliberately 'Read', not 'Glob', even though list_directory enumerates a directory the way a Glob call does: Grok's equivalent list_dir maps to 'Glob' instead, but Grok's GROK_INPUT_KEY_MAP never remaps a 'pattern' key for it either, so on Grok this call is inert (preGlobHandler/globSignature require a non-empty toolInput['pattern'], which a plain directory listing never sends). Gemini's Read mapping, by contrast, is confirmed live: preReadHandler's node_modules deny fires on a list_directory call targeting node_modules (dogfooded in commit f1fc02de), which Glob's handler has no equivalent check for. Read was the considered choice, not an oversight; kept even though the two harnesses disagree.
1536
- list_directory: "Read",
1537
- write_file: "Write",
1538
- replace: "Edit",
1539
- glob: "Glob",
1540
- grep_search: "Grep",
1541
- // 'search_file_content' was GREP_TOOL_NAME's value before gemini-cli renamed it to 'grep_search' (confirmed against gemini-cli's current base-declarations.ts, which now defines GREP_TOOL_NAME = 'grep_search'; some of gemini-cli's own docs pages still lag the rename). Kept as a backward-compat entry for older installed gemini-cli versions that still emit the pre-rename name -- not dead/hallucinated, just legacy.
1542
- search_file_content: "Grep",
1543
- // Gemini's real web-search tool is registered as 'google_web_search' (WEB_SEARCH_TOOL_NAME in gemini-cli's tool-names.ts) -- 'web_search' is not a tool name Gemini CLI ever emits, so the old entry here silently never matched a real invocation.
1544
- // Previously mapped to 'WebFetch' as a workaround for GEMINI_PRE_TOOLS/POST_TOOLS (gemini_install.ts) excluding 'WebSearch' -- that made the hook fire under the installer's matcher, but hooks_fetch.ts's preFetchHandler/postFetchHandler require a `url` tool-input key, which a search call never sends (it sends `query`), so every google_web_search call silently no-op'd through WebFetch's handler: no URL policy check ever applied to it (it has no URL to check) and no search-result dedup ever ran. Now mapped to the dedicated 'WebSearch' handler (hooks_websearch.ts), which reads `query` directly; the installer gate above was extended to include 'WebSearch' so the hook stays wired.
1545
- google_web_search: "WebSearch",
1546
- web_fetch: "WebFetch"
1547
- };
1548
- var GEMINI_INPUT_KEY_MAP = {
1549
- // Gemini's real write_file/replace tool schemas already use 'file_path' (and replace's 'old_string'/'new_string') verbatim -- identical to token-goat's own canonical keys (see getFilePath() in hooks_common.ts, which reads event.toolInput['file_path']) -- confirmed against gemini-cli's own EditToolParams/WriteFileToolParams interfaces. No remap is needed for Write/Edit; the previous entries here actively renamed a key that was already correct (file_path -> path), which would have silently corrupted the path argument on every real Gemini Write/Edit call.
1550
- // 'Read' covers THREE distinct raw Gemini tools (see GEMINI_TOOL_NAME_MAP above), and they do NOT share one schema: read_file already uses 'file_path' verbatim, no remap needed; list_directory uses 'dir_path' (confirmed against gemini-cli's LSToolParams) -- remapped to 'file_path' below, exactly mirroring Grep's existing dir_path->path fix; read_many_files uses 'include' (confirmed against gemini-cli's ReadManyFilesParams), an ARRAY of glob patterns, not a single file path -- there is no single string to remap it to, so it is deliberately left unmapped. getFilePath() (hooks_common.ts) will return undefined for this call, and preReadHandler/postReadHandler already fall back to passOutput() on an undefined path (see preReadHandler's `if (filePath === undefined) return passOutput()`), so a real read_many_files call still succeeds normally; it just doesn't get session-dedup tracking or read-count hints. Accepted limitation, not a bug to force-fit.
1551
- Read: { dir_path: "file_path" },
1552
- // Grep's real tool (grep_search) calls its target-directory argument 'dir_path', which preReadHandler's Grep fallback (event.toolInput['path']) doesn't recognize, so that one remains remapped.
1553
- Grep: { dir_path: "path" }
1554
- };
1555
- var GROK_TOOL_NAME_MAP = {
1556
- read_file: "Read",
1557
- write: "Write",
1558
- search_replace: "Edit",
1559
- run_terminal_command: "Bash",
1560
- // The grok 0.2.93 binary registers its shell tool as `tool.run_terminal_cmd` (tracing-id table in ~/.grok/bin/agent.exe) and its newer embedded hooks doc's PreToolUse example sends `"toolName": "run_terminal_cmd"`, while the older embedded doc revision and a 2026-07-09 live capture on the same version both show `run_terminal_command`. Which spelling arrives is profile/version dependent, so BOTH are mapped: an unmatched extra entry is a harmless no-op, a missing one silently kills every Bash hook (compression, wrap, hints) on that profile.
1561
- run_terminal_cmd: "Bash",
1562
- grep: "Grep",
1563
- list_dir: "Glob",
1564
- // `glob` is a distinct registered grok tool (tool.glob in the 0.2.93 binary's tracing-id table), separate from list_dir; unmapped it arrived as lowercase 'glob' and matched no handler.
1565
- glob: "Glob",
1566
- // web_fetch/web_search were entirely absent from this map, so grok's URL fetches bypassed token-goat's WebFetch pipeline (URL-policy deny included) and its searches bypassed the WebSearch dedup/compression. Verified against the 0.2.93 binary: tool.web_fetch / tool.web_search registered ids, grok_build WebFetchInput's field described as "The URL to fetch content from" (i.e. `url`), grok_build WebSearchInput's fields query/citations/allowed_domains -- all matching the keys hooks_fetch.ts / hooks_websearch.ts already read, so a name map alone revives both.
1567
- web_fetch: "WebFetch",
1568
- web_search: "WebSearch",
1569
- // The Hashline prompt profile registers hashline_read/hashline_edit/hashline_grep (tool.hashline_* in the binary; grok's own hook alias table pairs them with Read/Edit/Grep). Their input key shapes were not individually verified: if one differs, the mapped handler degrades to the same no-op as an unmapped name, never a wrong rewrite. The *_concise twins are the GrokBuildConcise profile's registrations of the same three core tools.
1570
- hashline_read: "Read",
1571
- hashline_edit: "Edit",
1572
- hashline_grep: "Grep",
1573
- read_file_concise: "Read",
1574
- search_replace_concise: "Edit",
1575
- run_terminal_cmd_concise: "Bash"
1576
- };
1577
- var GROK_INPUT_KEY_MAP = {
1578
- Read: { target_file: "file_path" }
1579
- };
1580
- var KIMI_TOOL_NAME_MAP = {
1581
- FetchURL: "WebFetch",
1582
- ReadMediaFile: "Read"
1583
- };
1584
- var KIMI_INPUT_KEY_MAP = {
1585
- // line_offset/n_lines are Kimi's Read paging arguments (ReadInputSchema in MoonshotAI/kimi-code packages/agent-core-v2/src/agent/tools/os/read/read.ts: `path`, `line_offset` "the line number to start reading from", `n_lines` "the number of lines to read"). Unmapped, every ranged Kimi read looked unbounded to hooks_read.ts's estimateRequestedSlice and was gated on the whole file's size -- a small slice of a big file could draw the large-file deny meant for full reads. line_offset's 1-indexed positive form matches Read's own offset semantics exactly; its negative tail-read form has no token-goat equivalent and is clamped to 1 by estimateRequestedSlice's `offset >= 1` guard, degrading to a window of the right SIZE (which is all the gate consumes). ReadMediaFile shares this map via its Read rename and carries only `path`, so the extra entries never touch it.
1586
- Read: { path: "file_path", line_offset: "offset", n_lines: "limit" },
1587
- Write: { path: "file_path" },
1588
- Edit: { path: "file_path" }
1589
- };
1590
- var QWEN_TOOL_NAME_MAP = {
1591
- read_file: "Read",
1592
- write_file: "Write",
1593
- edit: "Edit",
1594
- replace: "Edit",
1595
- notebook_edit: "NotebookEdit",
1596
- run_shell_command: "Bash",
1597
- grep_search: "Grep",
1598
- search_file_content: "Grep",
1599
- glob: "Glob",
1600
- web_fetch: "WebFetch",
1601
- web_search: "WebSearch",
1602
- // Same deliberate 'Read' choice as Gemini's list_directory entry above (see its comment): node_modules deny fires, Grok's rival 'Glob' mapping is inert for the equivalent tool anyway.
1603
- list_directory: "Read"
1604
- };
1605
- var QWEN_INPUT_KEY_MAP = {
1606
- Read: { path: "file_path" }
1607
- };
1608
- var VSCODE_TOOL_NAME_MAP = {
1609
- read_file: "Read",
1610
- view_image: "Read",
1611
- list_dir: "Read",
1612
- grep_search: "Grep",
1613
- file_search: "Glob",
1614
- create_file: "Write",
1615
- replace_string_in_file: "Edit",
1616
- insert_edit_into_file: "Edit",
1617
- edit_notebook_file: "NotebookEdit",
1618
- run_in_terminal: "Bash"
1619
- };
1620
- var VSCODE_INPUT_KEY_MAP = {
1621
- read_file: { filePath: "file_path" },
1622
- view_image: { filePath: "file_path" },
1623
- list_dir: { path: "file_path" },
1624
- grep_search: { query: "pattern", includePattern: "glob" },
1625
- file_search: { query: "pattern" },
1626
- create_file: { filePath: "file_path" },
1627
- replace_string_in_file: { filePath: "file_path", oldString: "old_string", newString: "new_string" },
1628
- insert_edit_into_file: { filePath: "file_path" },
1629
- edit_notebook_file: { filePath: "notebook_path" },
1630
- run_in_terminal: {}
1631
- };
1632
- var VSCODE_TOOL_NAME_KEY = "_tg_vscode_tool_name";
1633
- function vscodeNativeToolInput(vscodeToolName2, canonicalInput) {
1634
- const inverse = {};
1635
- for (const [nativeKey, canonicalKey] of Object.entries(VSCODE_INPUT_KEY_MAP[vscodeToolName2] ?? {})) {
1636
- inverse[canonicalKey] = nativeKey;
1637
- }
1638
- return remapInputKeys(canonicalInput, inverse);
1639
- }
1640
- function remapInputKeys(input, keyMap) {
1641
- const newInput = {};
1642
- for (const [k, v] of Object.entries(input)) {
1643
- newInput[keyMap[k] || k] = v;
1644
- }
1645
- return newInput;
1646
- }
1647
- function remapToolName(obj, toolName, nameMap, inputKeyMap) {
1648
- const mapped = nameMap[toolName] ?? toolName;
1649
- const result = { ...obj };
1650
- result["tool_name"] = mapped;
1651
- const rawInput = obj["tool_input"];
1652
- const keyMap = inputKeyMap[mapped];
1653
- if (keyMap && typeof rawInput === "object" && rawInput !== null && !Array.isArray(rawInput)) {
1654
- result["tool_input"] = remapInputKeys(rawInput, keyMap);
1655
- }
1656
- return result;
1657
- }
1658
- function grokToCanonicalWire(obj) {
1659
- const wire = { ...obj };
1660
- if (typeof wire["toolName"] === "string" && wire["tool_name"] === void 0) {
1661
- wire["tool_name"] = wire["toolName"];
1662
- }
1663
- if (typeof wire["toolInput"] === "object" && wire["toolInput"] !== null && !Array.isArray(wire["toolInput"]) && wire["tool_input"] === void 0) {
1664
- wire["tool_input"] = wire["toolInput"];
1665
- }
1666
- if (typeof wire["sessionId"] === "string" && wire["session_id"] === void 0) {
1667
- wire["session_id"] = wire["sessionId"];
1668
- }
1669
- const toolResult = wire["toolResult"];
1670
- if (toolResult !== null && typeof toolResult === "object" && !Array.isArray(toolResult)) {
1671
- const tr = toolResult;
1672
- if (typeof tr["output_for_prompt"] === "string") {
1673
- wire["tool_response"] = { content: tr["output_for_prompt"], exit_code: tr["exit_code"] };
1674
- } else {
1675
- const firstString = Object.entries(tr).find(([k, v]) => k !== "type" && typeof v === "string");
1676
- if (firstString) {
1677
- wire["tool_response"] = { content: firstString[1] };
1678
- }
1679
- }
1680
- }
1681
- return wire;
1682
- }
1683
- function aliasLlmContentToOutput(result) {
1684
- const toolResponse = result["tool_response"];
1685
- if (toolResponse === null || typeof toolResponse !== "object" || Array.isArray(toolResponse)) return;
1686
- const responseRecord = toolResponse;
1687
- if (typeof responseRecord["llmContent"] === "string" && responseRecord["output"] === void 0) {
1688
- result["tool_response"] = { ...responseRecord, output: responseRecord["llmContent"] };
1689
- }
1690
- }
1691
- function normalizePayload(payload, harness = "claude") {
1692
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
1693
- _LOG.warn("normalizePayload: payload is not a dict; received %s", typeof payload);
1694
- return {};
1695
- }
1696
- const rawObj = payload;
1697
- const obj = harness === "grok" ? grokToCanonicalWire(rawObj) : rawObj;
1698
- const toolName = obj["tool_name"];
1699
- if (typeof toolName !== "string" || !toolName.trim()) {
1700
- _LOG.debug("normalizePayload: tool_name missing or invalid; received %s", toolName);
1701
- return {};
1702
- }
1703
- if (harness === "grok") {
1704
- const result2 = remapToolName(obj, toolName, GROK_TOOL_NAME_MAP, GROK_INPUT_KEY_MAP);
1705
- result2["_tg_harness"] = harness;
1706
- return result2;
1707
- }
1708
- if (harness === "codex") {
1709
- const mapped = CODEX_TOOL_NAME_MAP[toolName];
1710
- const result2 = { ...obj };
1711
- if (mapped) {
1712
- result2["tool_name"] = mapped;
1713
- }
1714
- result2["_tg_harness"] = harness;
1715
- return result2;
1716
- }
1717
- if (harness === "copilot_cli") {
1718
- const result2 = { ...obj };
1719
- result2["tool_name"] = canonicalizeCopilotMcpToolName(toolName);
1720
- result2["_tg_harness"] = harness;
1721
- return result2;
1722
- }
1723
- if (harness === "vscode") {
1724
- const result2 = { ...obj };
1725
- result2["tool_name"] = VSCODE_TOOL_NAME_MAP[toolName] ?? toolName;
1726
- const keyMap = VSCODE_INPUT_KEY_MAP[toolName];
1727
- const rawInput = obj["tool_input"];
1728
- if (keyMap && typeof rawInput === "object" && rawInput !== null && !Array.isArray(rawInput)) {
1729
- result2["tool_input"] = remapInputKeys(rawInput, keyMap);
1730
- }
1731
- result2[VSCODE_TOOL_NAME_KEY] = toolName;
1732
- result2["_tg_harness"] = harness;
1733
- return result2;
1734
- }
1735
- if (harness === "kimi") {
1736
- const result2 = remapToolName(obj, toolName, KIMI_TOOL_NAME_MAP, KIMI_INPUT_KEY_MAP);
1737
- result2["_tg_harness"] = harness;
1738
- return result2;
1739
- }
1740
- if (harness === "qwen") {
1741
- const result2 = remapToolName(obj, toolName, QWEN_TOOL_NAME_MAP, QWEN_INPUT_KEY_MAP);
1742
- aliasLlmContentToOutput(result2);
1743
- result2["_tg_harness"] = harness;
1744
- return result2;
1745
- }
1746
- if (harness === "gemini") {
1747
- const result2 = remapToolName(obj, toolName, GEMINI_TOOL_NAME_MAP, GEMINI_INPUT_KEY_MAP);
1748
- if ("functionCallId" in result2 && !("toolUseId" in result2)) {
1749
- result2["toolUseId"] = result2["functionCallId"];
1750
- delete result2["functionCallId"];
1751
- }
1752
- aliasLlmContentToOutput(result2);
1753
- result2["_tg_harness"] = harness;
1754
- return result2;
1755
- }
1756
- const result = { ...obj };
1757
- result["_tg_harness"] = harness;
1758
- return result;
1759
- }
1760
-
1761
- // src/hook_registry.ts
1762
- init_define_import_meta_env();
1763
-
1764
- // src/hooks_common.ts
1765
- init_define_import_meta_env();
1766
- function getToolName(event) {
1767
- return event.toolName;
1768
- }
1769
- function getToolInput(event) {
1770
- return event.toolInput;
1771
- }
1772
- function getFilePath(event) {
1773
- const value = event.toolInput["file_path"];
1774
- if (typeof value === "string" && value !== "") return value;
1775
- const notebookValue = event.toolInput["notebook_path"];
1776
- return typeof notebookValue === "string" && notebookValue !== "" ? notebookValue : void 0;
1777
- }
1778
- function getCwd(event) {
1779
- const value = event.raw && typeof event.raw === "object" ? event.raw["cwd"] : void 0;
1780
- return typeof value === "string" && value !== "" ? value : void 0;
1781
- }
1782
- function getTranscriptPath(event) {
1783
- const value = event.raw && typeof event.raw === "object" ? event.raw["transcript_path"] : void 0;
1784
- return typeof value === "string" && value !== "" ? value : void 0;
1785
- }
1786
- function extractToolResponseField(raw, keys) {
1787
- const resp = raw["tool_response"];
1788
- if (typeof resp === "string") return resp;
1789
- if (resp !== null && typeof resp === "object") {
1790
- const r = resp;
1791
- const path12 = resolveToolResponseFieldPath(r, keys);
1792
- if (path12 !== null) return readAtPath(r, path12);
1793
- }
1794
- return "";
1795
- }
1796
- var NESTED_TOOL_RESPONSE_PATHS = [["file", "content"]];
1797
- function readAtPath(resp, path12) {
1798
- let cur = resp;
1799
- for (const seg of path12) {
1800
- if (cur === null || typeof cur !== "object") return "";
1801
- cur = cur[seg];
1802
- }
1803
- return typeof cur === "string" ? cur : "";
1804
- }
1805
- function resolveToolResponseFieldPath(resp, keys) {
1806
- for (const key of keys) {
1807
- if (typeof resp[key] === "string" && resp[key] !== "") return [key];
1808
- }
1809
- for (const path12 of NESTED_TOOL_RESPONSE_PATHS) {
1810
- if (readAtPath(resp, path12) !== "") return path12;
1811
- }
1812
- return null;
1813
- }
1814
- function replaceToolResponseField(resp, keys, newText) {
1815
- const path12 = resolveToolResponseFieldPath(resp, keys);
1816
- if (path12 === null) return null;
1817
- const root = { ...resp };
1818
- let cur = root;
1819
- for (let i = 0; i < path12.length - 1; i++) {
1820
- const seg = path12[i];
1821
- const child = cur[seg];
1822
- if (child === null || typeof child !== "object") return null;
1823
- const cloned = { ...child };
1824
- cur[seg] = cloned;
1825
- cur = cloned;
1826
- }
1827
- cur[path12[path12.length - 1]] = newText;
1828
- return root;
1829
- }
1830
- var OUTPUT_FIRST_TOOL_RESPONSE_KEYS = ["output", "content", "text", "body", "stdout", "stderr"];
1831
- var BODY_FIRST_TOOL_RESPONSE_KEYS = ["output", "body", "text", "content", "result"];
1832
- function isMcpErrorResponse(raw) {
1833
- const tr = raw["tool_response"];
1834
- if (!tr || typeof tr !== "object") return false;
1835
- return tr["isError"] === true;
1836
- }
1837
- var MCP_CONTENT_BLOCK_TYPES = /* @__PURE__ */ new Set(["text", "image", "audio", "resource", "resource_link"]);
1838
- function blocksToText(blocks) {
1839
- if (!Array.isArray(blocks)) return null;
1840
- const parts = [];
1841
- let recognised = false;
1842
- for (const block of blocks) {
1843
- if (!block || typeof block !== "object") continue;
1844
- const rec = block;
1845
- const type = rec["type"];
1846
- if (typeof type === "string" && MCP_CONTENT_BLOCK_TYPES.has(type)) recognised = true;
1847
- if (typeof rec["text"] === "string") {
1848
- parts.push(rec["text"]);
1849
- continue;
1850
- }
1851
- const resource = rec["resource"];
1852
- if (resource !== null && typeof resource === "object") {
1853
- const nested = resource["text"];
1854
- if (typeof nested === "string") parts.push(nested);
1855
- continue;
1856
- }
1857
- if (rec["type"] === "resource_link") {
1858
- const link = ["name", "uri", "description"].map((k) => rec[k]).filter((v) => typeof v === "string" && v !== "");
1859
- if (link.length > 0) parts.push(link.join(" "));
1860
- }
1861
- }
1862
- if (parts.length > 0) return parts.join("\n");
1863
- return recognised ? "" : null;
1864
- }
1865
- function extractToolResultText(raw) {
1866
- const tr = raw["tool_response"];
1867
- if (typeof tr === "string") return tr;
1868
- if (!tr || typeof tr !== "object") return "";
1869
- const bare = blocksToText(tr);
1870
- if (bare !== null) return bare;
1871
- const resp = tr;
1872
- const content = resp["content"];
1873
- const joined = blocksToText(content);
1874
- if (joined !== null) return joined;
1875
- for (const key of ["output", "text", "body"]) {
1876
- if (typeof resp[key] === "string") return resp[key];
1877
- }
1878
- if (typeof content === "string") return content;
1879
- try {
1880
- return JSON.stringify(resp);
1881
- } catch {
1882
- return "";
1883
- }
1884
- }
1885
- function asContentBlocks(value) {
1886
- if (!Array.isArray(value)) return null;
1887
- const blocks = value.filter((b) => b !== null && typeof b === "object");
1888
- const typed = blocks.some((b) => typeof b["type"] === "string" && MCP_CONTENT_BLOCK_TYPES.has(b["type"]));
1889
- return typed ? blocks : null;
1890
- }
1891
- function mcpContentBlocks(raw) {
1892
- const tr = raw["tool_response"];
1893
- if (tr === null || typeof tr !== "object") return null;
1894
- return asContentBlocks(tr) ?? asContentBlocks(tr["content"]);
1895
- }
1896
- function isTextualContentBlock(block) {
1897
- if (block === null || typeof block !== "object") return false;
1898
- const rec = block;
1899
- if (typeof rec["text"] === "string") return true;
1900
- if (rec["type"] === "resource_link") return true;
1901
- const resource = rec["resource"];
1902
- return resource !== null && typeof resource === "object" && typeof resource["text"] === "string";
1903
- }
1904
- function withPreservedBlocks(output, blocks) {
1905
- if (output.hookType !== "rewriteOutput" || blocks.length === 0) return output;
1906
- return { ...output, updatedBlocks: [{ type: "text", text: output.updatedOutput }, ...blocks] };
1907
- }
1908
- function passOutput() {
1909
- return { hookType: "pass" };
1910
- }
1911
- function denyOutput(message) {
1912
- return { hookType: "deny", message: `[tg] ${neutralizeOutsideFences(message)}` };
1913
- }
1914
- function contextOutput(context) {
1915
- return { hookType: "context", context };
1916
- }
1917
- function emitRewrite(updatedOutput, detail, savings, redaction = "count-here") {
1918
- if (detectHarness() === "vscode") return passOutput();
1919
- if (redaction === "count-here") {
1920
- const count = countRedactionPlaceholders(updatedOutput);
1921
- if (count > 0) recordStat("secret_redacted", 0, count, void 0, detail);
1922
- }
1923
- if (savings !== void 0) {
1924
- const bytesSaved = savings.originalBytes - Buffer.byteLength(updatedOutput, "utf-8");
1925
- if (bytesSaved > 0) recordStat(savings.kind, bytesSaved, savedTokensFromBytes(bytesSaved), void 0, savings.detail);
1926
- }
1927
- return { hookType: "rewriteOutput", updatedOutput };
1928
- }
1929
- function emitRewriteIfChanged(original, emitted, detail) {
1930
- if (emitted === original) return passOutput();
1931
- return emitRewrite(emitted, detail);
1932
- }
1933
- function countNonEmptyLines(text) {
1934
- return text.split(/\r\n|\r|\n/).filter((line) => line.length > 0).length;
1935
- }
1936
- function estimateResultCount(text) {
1937
- const firstLine = text.split(/\r\n|\r|\n/).find((line) => line.trim().length > 0);
1938
- if (firstLine !== void 0) {
1939
- const trimmed = firstLine.trim();
1940
- if (/^No (?:files|matches) found\.?$/i.test(trimmed)) return 0;
1941
- const found = /^Found (\d+) (?:files?|matches?)\.?$/i.exec(trimmed);
1942
- if (found) return Number.parseInt(found[1], 10);
1943
- }
1944
- return countNonEmptyLines(text);
1945
- }
1946
- function makeDedupHintHandlers(opts) {
1947
- const post = (event) => {
1948
- try {
1949
- if (getToolName(event) !== opts.toolName) return passOutput();
1950
- const signature = opts.buildSignature(getToolInput(event));
1951
- if (signature === null) return passOutput();
1952
- const text = extractToolResponseField(event.raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
1953
- opts.recordQuery(signature, estimateResultCount(text));
1954
- return passOutput();
1955
- } catch {
1956
- return passOutput();
1957
- }
1958
- };
1959
- const pre = (event) => {
1960
- try {
1961
- if (getToolName(event) !== opts.toolName) return passOutput();
1962
- const toolInput = getToolInput(event);
1963
- const signature = opts.buildSignature(toolInput);
1964
- if (signature === null) return passOutput();
1965
- const priorCount = opts.getMatchCount(signature);
1966
- if (priorCount === null) return passOutput();
1967
- if (priorCount < loadConfig().hints[opts.minMatchesConfigKey]) return passOutput();
1968
- recordStat(opts.statName, 0, 0);
1969
- const pattern = displaySafeText(typeof toolInput["pattern"] === "string" ? toolInput["pattern"] : "");
1970
- return contextOutput(
1971
- "Note: an identical " + opts.toolName + ' for "' + pattern + '" already ran this session and returned ' + priorCount + (priorCount === 1 ? " match" : " matches") + ". If that result already answers this, you can skip re-running it."
1972
- );
1973
- } catch {
1974
- return passOutput();
1975
- }
1976
- };
1977
- return { post, pre };
1978
- }
1979
-
1980
- // src/bridges/vscode_hooks.ts
1981
- init_define_import_meta_env();
1982
- import * as crypto from "node:crypto";
1983
- import * as fs5 from "node:fs";
1984
- import * as os3 from "node:os";
1985
- import * as path5 from "node:path";
1986
- function vscodeToolName(event) {
1987
- const name = event?.raw[VSCODE_TOOL_NAME_KEY];
1988
- return typeof name === "string" ? name : void 0;
1989
- }
1990
- var SHRINK_PREFIX = "token-goat-shrink-";
1991
- var MATERIALIZED_MAX_AGE_MS = 60 * 60 * 1e3;
1992
- function pruneMaterialized() {
1993
- const now = Date.now();
1994
- try {
1995
- const dir = os3.tmpdir();
1996
- for (const file of fs5.readdirSync(dir)) {
1997
- if (!file.startsWith(SHRINK_PREFIX)) continue;
1998
- const full = path5.join(dir, file);
1999
- try {
2000
- const st = fs5.statSync(full);
2001
- if (st.isFile() && now - st.mtimeMs > MATERIALIZED_MAX_AGE_MS) fs5.unlinkSync(full);
2002
- } catch {
2003
- }
2004
- }
2005
- } catch {
2006
- }
2007
- }
2008
- function materializeShrunkImageFile(context) {
2009
- const idx = context.indexOf("data:image/");
2010
- if (idx === -1) return void 0;
2011
- const match = /^data:image\/([a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)$/.exec(context.slice(idx).trim());
2012
- if (!match) return void 0;
2013
- try {
2014
- pruneMaterialized();
2015
- const file = path5.join(os3.tmpdir(), `${SHRINK_PREFIX}${process.pid}-${Date.now()}-${crypto.randomUUID()}.${match[1]}`);
2016
- fs5.writeFileSync(file, Buffer.from(match[2], "base64"), { mode: 384 });
2017
- return file;
2018
- } catch {
2019
- return void 0;
2020
- }
2021
- }
2022
- var NESTED_DECISION_EVENTS = /* @__PURE__ */ new Set(["stop", "subagent_stop"]);
2023
- function serializeVscodeOutput(output, eventName, hookEventName, event) {
2024
- switch (output.hookType) {
2025
- case "deny":
2026
- if (eventName === "pre_tool_use") {
2027
- return JSON.stringify({
2028
- hookSpecificOutput: { hookEventName, permissionDecision: "deny", permissionDecisionReason: output.message }
2029
- });
2030
- }
2031
- if (NESTED_DECISION_EVENTS.has(eventName)) {
2032
- return JSON.stringify({ hookSpecificOutput: { hookEventName, decision: "block", reason: output.message } });
2033
- }
2034
- return JSON.stringify({ decision: "block", reason: output.message });
2035
- case "context": {
2036
- if (NESTED_DECISION_EVENTS.has(eventName)) return "{}";
2037
- if (eventName === "pre_compact" || eventName === "notification") return JSON.stringify({ systemMessage: output.context });
2038
- if (eventName === "pre_tool_use" && output.context.includes("data:image/")) return "{}";
2039
- return JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext: output.context } });
2040
- }
2041
- case "rewriteInput": {
2042
- const tool = vscodeToolName(event);
2043
- const updatedInput = tool === void 0 ? output.updatedInput : vscodeNativeToolInput(tool, output.updatedInput);
2044
- return JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", updatedInput } });
2045
- }
2046
- case "rewriteOutput":
2047
- return "{}";
2048
- case "pass":
2049
- return "{}";
2050
- }
2051
- }
2052
-
2053
- // src/hook_registry.ts
2054
- function sessionStateKey(event) {
2055
- return event.agentId !== void 0 ? `${event.sessionId}:agent:${event.agentId}` : event.sessionId;
2056
- }
2057
- var _handlers = /* @__PURE__ */ new Map();
2058
- function registerHook(eventName, handler, opts) {
2059
- let list = _handlers.get(eventName);
2060
- if (list === void 0) {
2061
- list = [];
2062
- _handlers.set(eventName, list);
2063
- }
2064
- list.push({
2065
- handler,
2066
- toolName: opts?.toolName,
2067
- toolPattern: opts?.toolPattern,
2068
- advisory: opts?.advisory === true,
2069
- followsMatcher: opts?.followsMatcher === true
2070
- });
2071
- }
2072
- function toolMatcherFor(eventName) {
2073
- const list = _handlers.get(eventName);
2074
- if (list === void 0 || list.length === 0) return null;
2075
- const parts = [];
2076
- for (const { toolName, toolPattern, followsMatcher } of list) {
2077
- if (followsMatcher) continue;
2078
- if (toolName === void 0 && toolPattern === void 0) return null;
2079
- for (const part of [toolName === void 0 ? void 0 : `^${toolName}$`, toolPattern]) {
2080
- if (part !== void 0 && part !== "" && !parts.includes(part)) parts.push(part);
2081
- }
2082
- }
2083
- if (parts.length === 0) return null;
2084
- return parts.join("|");
2085
- }
2086
- function foldToolName2(name) {
2087
- return name.toLowerCase().replace(/[_-]/g, "");
2088
- }
2089
- function noteUnrecognizedTool(event, list) {
2090
- const toolName = event.toolName;
2091
- if (typeof toolName !== "string" || toolName === "") return;
2092
- const named = [];
2093
- for (const { toolName: want } of list) {
2094
- if (want === void 0) continue;
2095
- if (want === toolName) return;
2096
- named.push(want);
2097
- }
2098
- if (named.length === 0) return;
2099
- const folded = foldToolName2(toolName);
2100
- const nearMiss = named.find((n) => foldToolName2(n) === folded) ?? null;
2101
- recordUnmappedTool(toolName, event.eventName, nearMiss);
2102
- }
2103
- async function runHook(event) {
2104
- const list = _handlers.get(event.eventName);
2105
- if (list === void 0) return { hookType: "pass" };
2106
- noteUnrecognizedTool(event, list);
2107
- let advisoryResult;
2108
- for (const { handler, toolName, advisory } of list) {
2109
- if (toolName !== void 0 && toolName !== event.toolName) continue;
2110
- let result;
2111
- try {
2112
- result = await handler(event);
2113
- } catch {
2114
- continue;
2115
- }
2116
- if (result.hookType !== "pass") {
2117
- if (advisory) {
2118
- advisoryResult = result;
2119
- continue;
2120
- }
2121
- return result;
2122
- }
2123
- }
2124
- return advisoryResult ?? { hookType: "pass" };
2125
- }
2126
- function clearHooks() {
2127
- _handlers.clear();
2128
- }
2129
- registerReset(clearHooks);
2130
- var CLAUDE_CODE_EVENT_NAMES = {
2131
- pre_tool_use: "PreToolUse",
2132
- post_tool_use: "PostToolUse",
2133
- notification: "Notification",
2134
- stop: "Stop",
2135
- pre_compact: "PreCompact",
2136
- post_compact: "PostCompact",
2137
- user_prompt_submit: "UserPromptSubmit",
2138
- subagent_stop: "SubagentStop",
2139
- session_start: "SessionStart",
2140
- // Claude Code has no separate failure event -- a failed tool arrives on PostToolUse there,
2141
- // and only Copilot splits it out. This entry exists because the map is exhaustive over
2142
- // HookEventName, and it is a spelling for the response envelope rather than a claim that
2143
- // Claude Code will ever send this event. Nothing iterates this map to build install config,
2144
- // so naming an event Claude Code does not have cannot register one.
2145
- post_tool_use_failure: "PostToolUseFailure"
2146
- };
2147
- var EVENTS_WITHOUT_ADDITIONAL_CONTEXT = /* @__PURE__ */ new Set([
2148
- "notification",
2149
- "pre_compact"
2150
- ]);
2151
- var EVENTS_WITH_RAW_STDOUT_CONTEXT = /* @__PURE__ */ new Set(["pre_compact"]);
2152
- var BODY_FIRST_REWRITE_TOOLS = /* @__PURE__ */ new Set(["WebFetch", "Skill"]);
2153
- function shapedUpdatedToolOutput(updatedOutput, harness, event) {
2154
- if (harness !== "claudecode") return updatedOutput;
2155
- const resp = event?.raw["tool_response"];
2156
- if (resp === null || resp === void 0 || typeof resp !== "object") return updatedOutput;
2157
- const keys = BODY_FIRST_REWRITE_TOOLS.has(event?.toolName ?? "") ? BODY_FIRST_TOOL_RESPONSE_KEYS : OUTPUT_FIRST_TOOL_RESPONSE_KEYS;
2158
- return replaceToolResponseField(resp, keys, updatedOutput) ?? updatedOutput;
2159
- }
2160
- function serializeOutput(output, eventName, harness, event) {
2161
- if (harness === "vscode") return serializeVscodeOutput(output, eventName, CLAUDE_CODE_EVENT_NAMES[eventName], event);
2162
- switch (output.hookType) {
2163
- case "deny":
2164
- return JSON.stringify({ decision: "block", reason: output.message });
2165
- case "context":
2166
- if (EVENTS_WITH_RAW_STDOUT_CONTEXT.has(eventName) && harness === "claudecode") {
2167
- return output.context;
2168
- }
2169
- if (EVENTS_WITHOUT_ADDITIONAL_CONTEXT.has(eventName)) {
2170
- return JSON.stringify({ systemMessage: output.context });
2171
- }
2172
- return JSON.stringify({
2173
- hookSpecificOutput: {
2174
- hookEventName: CLAUDE_CODE_EVENT_NAMES[eventName],
2175
- additionalContext: output.context
2176
- }
2177
- });
2178
- case "rewriteInput":
2179
- return JSON.stringify({
2180
- hookSpecificOutput: {
2181
- hookEventName: "PreToolUse",
2182
- permissionDecision: "allow",
2183
- updatedInput: output.updatedInput
2184
- }
2185
- });
2186
- case "rewriteOutput":
2187
- return JSON.stringify({
2188
- hookSpecificOutput: {
2189
- hookEventName: "PostToolUse",
2190
- // A block array is emitted only when the handler built one, and only for Claude Code: every other harness token-goat bridges to reads this field as a plain string into a text block of its own, so handing it an array there is the same mistake in the other direction. See HookOutput's `updatedBlocks` for the capture that proves Claude Code takes the array.
2191
- updatedToolOutput: output.updatedBlocks !== void 0 && harness === "claudecode" ? output.updatedBlocks : shapedUpdatedToolOutput(output.updatedOutput, harness, event)
2192
- }
2193
- });
2194
- case "pass":
2195
- return JSON.stringify({});
2196
- }
2197
- }
2198
-
2199
112
  // src/tool_filters/helpers.ts
2200
- init_define_import_meta_env();
2201
- import * as fs6 from "node:fs";
2202
- import * as path6 from "node:path";
2203
113
  var DEFAULT_MAX_LINES = 1e3;
2204
114
  var DEFAULT_MAX_BYTES = 64 * 1024;
2205
115
  var MAX_INSPECT_BYTES = 2 * 1024 * 1024;
@@ -2680,7 +590,7 @@ function loadPackageScripts(candidate, stat) {
2680
590
  if (cached && cached.mtimeMs === stat.mtimeMs) return cached.scripts;
2681
591
  let scripts = {};
2682
592
  try {
2683
- const parsed = JSON.parse(fs6.readFileSync(candidate, "utf8"));
593
+ const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
2684
594
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2685
595
  const raw = parsed["scripts"];
2686
596
  if (raw && typeof raw === "object" && !Array.isArray(raw)) scripts = raw;
@@ -2693,13 +603,13 @@ function loadPackageScripts(candidate, stat) {
2693
603
  function findNearestPackageScripts(startDir) {
2694
604
  let dir = startDir;
2695
605
  for (; ; ) {
2696
- const candidate = path6.join(dir, "package.json");
606
+ const candidate = path.join(dir, "package.json");
2697
607
  try {
2698
- const stat = fs6.statSync(candidate);
608
+ const stat = fs.statSync(candidate);
2699
609
  if (stat.isFile()) return loadPackageScripts(candidate, stat);
2700
610
  } catch {
2701
611
  }
2702
- const parent = path6.dirname(dir);
612
+ const parent = path.dirname(dir);
2703
613
  if (parent === dir) return null;
2704
614
  dir = parent;
2705
615
  }
@@ -2992,8 +902,8 @@ function byteLength(s) {
2992
902
 
2993
903
  // src/bash_output_cache.ts
2994
904
  init_define_import_meta_env();
2995
- import { readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
2996
- import { resolve as resolve4 } from "path";
905
+ import { readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
906
+ import { resolve } from "path";
2997
907
 
2998
908
  // src/hints/lang_patterns.ts
2999
909
  init_define_import_meta_env();
@@ -3132,15 +1042,15 @@ var LOCK_FILE_COUNT = LOCK_FILE_NAMES.size;
3132
1042
  var MANIFEST_FILE_COUNT = MANIFEST_FILE_NAMES.size + MANIFEST_EXTENSIONS.size + MANIFEST_BASENAME_PATTERNS.length;
3133
1043
  var BUILD_DIR_COUNT = BUILD_DIR_NAMES.size;
3134
1044
  var GENERATED_EXT_COUNT = ALWAYS_GENERATED_EXTS.size + CONDITIONALLY_GENERATED_EXTS.size;
3135
- function isLockFile(basename2) {
3136
- return LOCK_FILE_NAMES.has(basename2.toLowerCase());
1045
+ function isLockFile(basename) {
1046
+ return LOCK_FILE_NAMES.has(basename.toLowerCase());
3137
1047
  }
3138
- function isManifestFile(basename2) {
3139
- const lower = basename2.toLowerCase();
1048
+ function isManifestFile(basename) {
1049
+ const lower = basename.toLowerCase();
3140
1050
  if (MANIFEST_FILE_NAMES.has(lower)) return true;
3141
1051
  const dot = lower.lastIndexOf(".");
3142
1052
  if (dot !== -1 && MANIFEST_EXTENSIONS.has(lower.slice(dot))) return true;
3143
- if (MANIFEST_BASENAME_PATTERNS.some((re) => re.test(basename2))) return true;
1053
+ if (MANIFEST_BASENAME_PATTERNS.some((re) => re.test(basename))) return true;
3144
1054
  return false;
3145
1055
  }
3146
1056
  function pathSegments(filePath) {
@@ -3272,14 +1182,14 @@ function getMonitoringRecallHint(cmd) {
3272
1182
 
3273
1183
  // src/recall_index.ts
3274
1184
  init_define_import_meta_env();
3275
- import * as fs7 from "node:fs";
1185
+ import * as fs2 from "node:fs";
3276
1186
  function subdirForCacheType(cacheType) {
3277
1187
  return cacheType === "web" ? "web_outputs" : "bash_outputs";
3278
1188
  }
3279
1189
  function blobStillExists(cacheType, id) {
3280
1190
  try {
3281
1191
  const p = blobPath(subdirForCacheType(cacheType), id);
3282
- return p !== null && fs7.existsSync(p);
1192
+ return p !== null && fs2.existsSync(p);
3283
1193
  } catch {
3284
1194
  return false;
3285
1195
  }
@@ -3496,11 +1406,11 @@ function gitStateFingerprintSync(cwd) {
3496
1406
  }
3497
1407
  }
3498
1408
  var DIR_FINGERPRINT_LISTING_CAP_ENTRIES = 1e4;
3499
- function dirStateFingerprintSync(path12) {
1409
+ function dirStateFingerprintSync(path2) {
3500
1410
  try {
3501
- const stat = statSync3(path12);
1411
+ const stat = statSync2(path2);
3502
1412
  if (!stat.isDirectory()) return null;
3503
- const entries = readdirSync2(path12);
1413
+ const entries = readdirSync(path2);
3504
1414
  if (entries.length <= DIR_FINGERPRINT_LISTING_CAP_ENTRIES) {
3505
1415
  return shortFingerprint(entries.slice().sort().join("\0"));
3506
1416
  }
@@ -3510,12 +1420,12 @@ function dirStateFingerprintSync(path12) {
3510
1420
  }
3511
1421
  }
3512
1422
  var FILE_FINGERPRINT_CONTENT_CAP_BYTES = 2 * 1024 * 1024;
3513
- function fileStateFingerprintSync(path12) {
1423
+ function fileStateFingerprintSync(path2) {
3514
1424
  try {
3515
- const stat = statSync3(path12);
1425
+ const stat = statSync2(path2);
3516
1426
  if (!stat.isFile()) return null;
3517
1427
  if (stat.size <= FILE_FINGERPRINT_CONTENT_CAP_BYTES) {
3518
- return shortFingerprint(readFileSync5(path12));
1428
+ return shortFingerprint(readFileSync2(path2));
3519
1429
  }
3520
1430
  return shortFingerprint(`${stat.mtimeMs}\0${stat.size}`);
3521
1431
  } catch {
@@ -3531,7 +1441,7 @@ function depLockfileFingerprintSync(cmd, cwd) {
3531
1441
  if (!candidates) return null;
3532
1442
  for (const lockfile of candidates) {
3533
1443
  try {
3534
- const content = readFileSync5(resolve4(cwd, lockfile));
1444
+ const content = readFileSync2(resolve(cwd, lockfile));
3535
1445
  return shortFingerprint(content);
3536
1446
  } catch {
3537
1447
  continue;
@@ -3607,1690 +1517,179 @@ function commandHashSync(command, cwd = null) {
3607
1517
  }
3608
1518
  return shortFingerprint(key);
3609
1519
  }
3610
- function commandHash(command, cwd = null) {
3611
- return Promise.resolve(commandHashSync(command, cwd));
3612
- }
3613
- function computeBashFingerprints(command, cwd) {
3614
- const fingerprints = {};
3615
- if (cwd && (isGitMutableCommand(command) || isGitPushCommand(command) || isTestRunnerCommand2(command) || isLintCommand(command) || isBuildCommand(command) || isNpmRunScriptCommand(command))) {
3616
- const fp = gitStateFingerprintSync(cwd);
3617
- if (fp) fingerprints.git = fp;
3618
- }
3619
- if (cwd && isDirListingCommand(command)) {
3620
- const target = extractFirstPathArg(command, cwd, cwd);
3621
- if (target) {
3622
- const fp = dirStateFingerprintSync(target);
3623
- if (fp) fingerprints.dir = fp;
3624
- }
3625
- }
3626
- if (isDepListCommand(command) || cwd && (isNpmInstallCommand(command) || isNpmAuditCommand(command) || isNpmOutdatedCommand(command))) {
3627
- const fp = depLockfileFingerprintSync(command, cwd);
3628
- if (fp) fingerprints.lockfile = fp;
3629
- }
3630
- if (cwd && isCatCommand(command)) {
3631
- const target = extractFirstPathArg(command, cwd, null);
3632
- if (target) {
3633
- const fp = fileStateFingerprintSync(target);
3634
- if (fp) fingerprints.file = fp;
3635
- }
3636
- }
3637
- return Object.keys(fingerprints).length > 0 ? fingerprints : void 0;
3638
- }
3639
- function isBashEntryStale(entry, command, cwd) {
3640
- const stored = entry.fingerprints;
3641
- if (!stored) return false;
3642
- const current = computeBashFingerprints(command, cwd);
3643
- if (stored.git !== void 0 && stored.git !== current?.git) return true;
3644
- if (stored.dir !== void 0 && stored.dir !== current?.dir) return true;
3645
- if (stored.lockfile !== void 0 && stored.lockfile !== current?.lockfile) return true;
3646
- if (stored.file !== void 0 && stored.file !== current?.file) return true;
3647
- return false;
3648
- }
3649
- function tokenizeShellArgs(cmd) {
3650
- const trimmed = cmd.trim();
3651
- const tokens = [];
3652
- let current = "";
3653
- let quoteChar = null;
3654
- let hasToken = false;
3655
- for (let i = 0; i < trimmed.length; i++) {
3656
- const ch = trimmed[i];
3657
- if (quoteChar !== null) {
3658
- if (ch === quoteChar) {
3659
- quoteChar = null;
3660
- } else {
3661
- current += ch;
3662
- }
3663
- continue;
3664
- }
3665
- if (ch === '"' || ch === "'") {
3666
- quoteChar = ch;
3667
- hasToken = true;
3668
- continue;
3669
- }
3670
- if (/\s/.test(ch)) {
3671
- if (hasToken) {
3672
- tokens.push(current);
3673
- current = "";
3674
- hasToken = false;
3675
- }
3676
- continue;
3677
- }
3678
- current += ch;
3679
- hasToken = true;
3680
- }
3681
- if (hasToken) tokens.push(current);
3682
- return tokens;
3683
- }
3684
- function extractFirstPathArg(cmd, cwd, fallback) {
3685
- const tokens = tokenizeShellArgs(cmd);
3686
- for (let i = 1; i < tokens.length; i++) {
3687
- const token = tokens[i];
3688
- if (!token.startsWith("-")) {
3689
- if (!token.startsWith("/")) {
3690
- return resolve4(cwd, token);
3691
- }
3692
- return token;
3693
- }
3694
- }
3695
- return fallback;
3696
- }
3697
- var ISSUE_LINE_PATTERN = /\b(?:error|fail(?:ed|ure)?|warning)\b/i;
3698
- function summarizeOutputDelta(oldOutput, newOutput) {
3699
- if (oldOutput === newOutput) return null;
3700
- const oldLines = oldOutput.split("\n");
3701
- const newLines = newOutput.split("\n");
3702
- const oldIssueLines = oldLines.filter((l) => ISSUE_LINE_PATTERN.test(l));
3703
- if (oldIssueLines.length > 0) {
3704
- const newIssueLines = newLines.filter((l) => ISSUE_LINE_PATTERN.test(l));
3705
- const priorTotal = oldIssueLines.length;
3706
- const availableCounts = /* @__PURE__ */ new Map();
3707
- for (const l of newIssueLines) availableCounts.set(l, (availableCounts.get(l) ?? 0) + 1);
3708
- let resolved = 0;
3709
- for (const l of oldIssueLines) {
3710
- const avail = availableCounts.get(l) ?? 0;
3711
- if (avail > 0) {
3712
- availableCounts.set(l, avail - 1);
3713
- } else {
3714
- resolved++;
3715
- }
3716
- }
3717
- const remaining = newIssueLines.length;
3718
- return `[token-goat: delta] ${resolved} of ${priorTotal} prior issues resolved; remaining: ${remaining}`;
3719
- }
3720
- return `[token-goat: delta] output changed: ${oldLines.length} -> ${newLines.length} lines`;
3721
- }
3722
- function storeBashOutputSync(command, output, exitCode, cwd = null) {
3723
- const id = commandHashSync(command, cwd);
3724
- const fingerprints = computeBashFingerprints(command, cwd);
3725
- const redactedOutput = redactSecrets(output).text;
3726
- const redactedCommand = redactSecrets(command).text;
3727
- const entry = {
3728
- id,
3729
- command: redactedCommand,
3730
- output: redactedOutput,
3731
- exitCode,
3732
- storedAt: Date.now(),
3733
- sizeBytes: Buffer.byteLength(redactedOutput, "utf-8"),
3734
- ...fingerprints ? { fingerprints } : {}
3735
- };
3736
- _byId.set(id, entry);
3737
- storeBlob(BASH_OUTPUT_SUBDIR, id, entry);
3738
- indexRecallEntry("bash", id, redactedCommand, `${redactedCommand}
3739
- ${redactedOutput}`, entry.storedAt);
3740
- return id;
3741
- }
3742
- function storeBashOutput(command, output, exitCode, cwd = null) {
3743
- return Promise.resolve(storeBashOutputSync(command, output, exitCode, cwd));
3744
- }
3745
- function coerceBashEntry(raw) {
3746
- if (raw === null || typeof raw !== "object") return null;
3747
- const o = raw;
3748
- if (typeof o["id"] !== "string" || typeof o["command"] !== "string" || typeof o["output"] !== "string" || typeof o["exitCode"] !== "number" || typeof o["storedAt"] !== "number" || typeof o["sizeBytes"] !== "number") {
3749
- return null;
3750
- }
3751
- const entry = {
3752
- id: o["id"],
3753
- command: o["command"],
3754
- output: o["output"],
3755
- exitCode: o["exitCode"],
3756
- storedAt: o["storedAt"],
3757
- sizeBytes: o["sizeBytes"]
3758
- };
3759
- const rawFingerprints = o["fingerprints"];
3760
- if (rawFingerprints !== null && typeof rawFingerprints === "object") {
3761
- const f = rawFingerprints;
3762
- const fingerprints = {};
3763
- if (typeof f["git"] === "string") fingerprints.git = f["git"];
3764
- if (typeof f["dir"] === "string") fingerprints.dir = f["dir"];
3765
- if (typeof f["lockfile"] === "string") fingerprints.lockfile = f["lockfile"];
3766
- if (typeof f["file"] === "string") fingerprints.file = f["file"];
3767
- if (Object.keys(fingerprints).length > 0) return { ...entry, fingerprints };
3768
- }
3769
- return entry;
3770
- }
3771
- function getBashOutput(id) {
3772
- const hit = _byId.get(id);
3773
- if (hit !== void 0) return hit;
3774
- if (isBlobStale(BASH_OUTPUT_SUBDIR, id)) return null;
3775
- const entry = coerceBashEntry(loadBlob(BASH_OUTPUT_SUBDIR, id));
3776
- if (entry === null) return null;
3777
- _byId.set(id, entry);
3778
- return entry;
3779
- }
3780
- registerReset(() => {
3781
- _byId = /* @__PURE__ */ new Map();
3782
- });
3783
-
3784
- // src/install.ts
3785
- init_define_import_meta_env();
3786
- import * as fs8 from "node:fs";
3787
- import * as os4 from "node:os";
3788
- import * as path7 from "node:path";
3789
-
3790
- // src/bridges/claudecode.ts
3791
- init_define_import_meta_env();
3792
-
3793
- // src/bridges/shim_common.ts
3794
- init_define_import_meta_env();
3795
- var SHIM_REQUIRES = `const { spawnSync } = require('node:child_process')
3796
- const path = require('node:path')
3797
- const { pathToFileURL } = require('node:url')`;
3798
- var SHIM_VALID_HOOK_EVENTS = `// Keep in sync with HOOK_EVENTS in src/types.ts. eventName is validated against this closed
3799
- // set before being concatenated into a shell command string, so a hostile argv (e.g.
3800
- // 'pre_tool_use & calc.exe') can never reach the shell parser.
3801
- const VALID_HOOK_EVENTS = new Set([
3802
- 'pre_tool_use',
3803
- 'post_tool_use',
3804
- 'notification',
3805
- 'stop',
3806
- 'pre_compact',
3807
- 'post_compact',
3808
- 'user_prompt_submit',
3809
- 'subagent_stop',
3810
- 'session_start',
3811
- 'post_tool_use_failure',
3812
- ])`;
3813
- var SHIM_TRY_IN_PROCESS = `// Attempts the in-process hook call: import()s dist/token-goat-hook.mjs (a sibling of
3814
- // the baked token-goat entry path, built with zero load-time side effects -- unlike
3815
- // the CLI entry, which runs the full argv-parsing CLI as a side effect of being
3816
- // loaded) and calls its exported relayInProcess() directly, avoiding a second node
3817
- // process spawn entirely. Returns undefined (triggering the spawnSync fallback below)
3818
- // when entryPath is absent, the sibling file doesn't exist (an older install predating
3819
- // this file), or anything else goes wrong -- this must never throw.
3820
- async function tryInProcess(entryPath, eventName, input) {
3821
- if (!entryPath) return undefined
3822
- try {
3823
- const hookLibPath = path.join(path.dirname(entryPath), 'token-goat-hook.mjs')
3824
- if (!require('node:fs').existsSync(hookLibPath)) return undefined
3825
- const mod = await import(pathToFileURL(hookLibPath).href)
3826
- const payload = JSON.parse(input)
3827
- return await mod.relayInProcess(eventName, payload)
3828
- } catch {
3829
- return undefined
3830
- }
3831
- }`;
3832
- var SHIM_SPAWN_LADDER = ` const res = entryPath
3833
- ? spawnSync(process.execPath, [entryPath, 'hook', eventName], {
3834
- input,
3835
- encoding: 'utf8',
3836
- timeout: 3000,
3837
- killSignal: 'SIGKILL',
3838
- maxBuffer: SHIM_MAX_BUFFER_BYTES,
3839
- })`;
3840
- var SHIM_MAX_BUFFER_CONST = `const SHIM_MAX_BUFFER_BYTES = 32 * 1024 * 1024`;
3841
-
3842
- // src/bridges/claudecode.ts
3843
- var CLAUDECODE_HOOK_SCRIPT = `#!/usr/bin/env node
3844
- // token-goat Claude Code hook shim. Reads the hook payload on stdin, forwards it to \`token-goat hook <event>\`, and relays the response on stdout.
3845
- 'use strict'
3846
- // Cache V8's compiled bytecode for the 3.4 MB hook bundle this shim import()s below. Without it
3847
- // every hook invocation recompiles that bundle from source, which profiled at ~40ms of the ~148ms
3848
- // a single hook takes -- and a hook fires on every tool call. Worth ~23ms per invocation, measured
3849
- // twice on the installed shim by interleaving both variants: 148ms to 125ms, and later 186ms to
3850
- // 163ms on a busier machine. The floor moves with load; the 23ms it removes does not.
3851
- //
3852
- // It has to be here rather than inside the bundle: a module is compiled before any of its own code
3853
- // runs, so a bundle cannot enable the cache for itself. This shim is small enough to compile in
3854
- // well under a millisecond, and the dynamic import() happens after this line, so the bundle is
3855
- // compiled with the cache already active. Same reason the MCP SDK could not be deferred from
3856
- // inside mcp_server.ts -- what a module can affect starts after it is already compiled.
3857
- //
3858
- // enableCompileCache landed in Node 22.1 and package.json allows >=22.0.0, so a miss is possible;
3859
- // it is also best-effort by nature (read-only cache dir, full disk). Swallow everything: this shim
3860
- // must never fail a tool call to save itself 23ms.
3861
- try { require('node:module').enableCompileCache() } catch { /* older Node, or an unwritable cache dir: run uncached */ }
3862
- ${SHIM_REQUIRES}
3863
-
3864
- ${SHIM_VALID_HOOK_EVENTS}
3865
-
3866
- ${SHIM_TRY_IN_PROCESS}
3867
-
3868
- ${SHIM_MAX_BUFFER_CONST}
3869
-
3870
- async function main() {
3871
- const eventName = process.argv[2] || ''
3872
- if (!VALID_HOOK_EVENTS.has(eventName)) {
3873
- process.stdout.write('{}')
3874
- return
3875
- }
3876
- let input = ''
3877
- try {
3878
- input = require('node:fs').readFileSync(0, 'utf8')
3879
- } catch {
3880
- process.stdout.write('{}')
3881
- return
3882
- }
3883
- // process.argv[3], when present, is the absolute path to the running token-goat CLI
3884
- // entry (mirrors the same argv[3]-baked-entry convention used by the Codex/Copilot
3885
- // CLI shims). Try the in-process hook lib first (tryInProcess above), which avoids
3886
- // spawning a second node process altogether; fall back to the entry path directly via
3887
- // process.execPath, then finally to the old PATH-based shell:true invocation (an
3888
- // older cached hook config with no argv[3], or a config wired directly to
3889
- // \`token-goat hook <event>\` rather than through this shim). A 3000ms
3890
- // timeout/killSignal on both spawnSync fallbacks means token-goat degrades to its own
3891
- // fail-open '{}' rather than being force-killed by Claude Code's own hook timeout.
3892
- const entryPath = process.argv[3]
3893
- let stdout = await tryInProcess(entryPath, eventName, input)
3894
- if (stdout === undefined) {
3895
- ${SHIM_SPAWN_LADDER}
3896
- : spawnSync('token-goat hook ' + eventName, {
3897
- input,
3898
- encoding: 'utf8',
3899
- shell: true,
3900
- timeout: 3000,
3901
- killSignal: 'SIGKILL',
3902
- maxBuffer: SHIM_MAX_BUFFER_BYTES,
3903
- })
3904
- if (res.status !== 0 || !res.stdout) {
3905
- process.stdout.write('{}')
3906
- return
3907
- }
3908
- stdout = res.stdout
3909
- }
3910
- process.stdout.write(stdout)
3911
- }
3912
-
3913
- main().catch(() => {
3914
- process.stdout.write('{}')
3915
- })
3916
- `;
3917
-
3918
- // src/install.ts
3919
- var HOOK_EVENT_MAP = [
3920
- ["PreToolUse", "pre_tool_use"],
3921
- ["PostToolUse", "post_tool_use"],
3922
- ["PreCompact", "pre_compact"],
3923
- ["PostCompact", "post_compact"],
3924
- ["UserPromptSubmit", "user_prompt_submit"],
3925
- ["SubagentStop", "subagent_stop"],
3926
- ["SessionStart", "session_start"]
3927
- ];
3928
- var SHIM_COMMAND_MARKER = "token-goat-shim";
3929
- var COMMAND_MARKER = "token-goat hook";
3930
- var LEGACY_COMMAND_MARKERS = ["tokenwise", "token_goat", "tg-hook", "token-goat-hook"];
3931
- function anchoredMarkerPattern(marker) {
3932
- const escaped = escapeRegExp(marker);
3933
- return new RegExp(`(?<![a-zA-Z0-9_-])${escaped}(?![a-zA-Z0-9_-])`);
3934
- }
3935
- var HOOK_MARKER_PATTERNS = [SHIM_COMMAND_MARKER, COMMAND_MARKER, ...LEGACY_COMMAND_MARKERS].map(anchoredMarkerPattern);
3936
- function isTokenGoatHookCommand(command) {
3937
- return HOOK_MARKER_PATTERNS.some((pattern) => pattern.test(command));
3938
- }
3939
- function claudeHookScriptPath() {
3940
- return path7.join(os4.homedir(), ".claude", "hooks", "token-goat-shim.js");
3941
- }
3942
- function anyScopeReferencesShim(scriptPath, currentScope, alreadyStripped) {
3943
- const referencesIn = (map) => {
3944
- for (const groups of Object.values(map ?? {})) {
3945
- for (const group of groups) {
3946
- for (const h of group.hooks ?? []) {
3947
- if (h.command.includes(scriptPath)) return true;
3948
- }
3949
- }
3950
- }
3951
- return false;
3952
- };
3953
- if (referencesIn(alreadyStripped)) return true;
3954
- for (const scope of ["user", "project"]) {
3955
- if (scope === currentScope) continue;
3956
- if (referencesIn(readSettings(settingsPath(scope)).hooks)) return true;
3957
- }
3958
- return false;
3959
- }
3960
- function settingsPath(scope) {
3961
- const root = scope === "user" ? os4.homedir() : normalizeDarwinSystemAlias(process.cwd());
3962
- const base = path7.join(root, ".claude");
3963
- return path7.join(base, "settings.json");
3964
- }
3965
- var SettingsParseError = class extends Error {
3966
- };
3967
- function readSettings(p, opts = {}) {
3968
- let raw;
3969
- try {
3970
- raw = fs8.readFileSync(p, "utf8");
3971
- } catch {
3972
- return {};
3973
- }
3974
- let parsed;
3975
- try {
3976
- parsed = JSON.parse(raw);
3977
- } catch {
3978
- if (opts.strict === true) {
3979
- throw new SettingsParseError(
3980
- `settings file '${p}' exists but contains invalid JSON. Fix or back up the file before running install.`
3981
- );
3982
- }
3983
- return {};
3984
- }
3985
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
3986
- return parsed;
3987
- }
3988
- if (opts.strict === true) {
3989
- throw new SettingsParseError(
3990
- `settings file '${p}' does not contain a JSON object at the top level. Fix or back up the file before running install.`
3991
- );
3992
- }
3993
- return {};
3994
- }
3995
- function groupHasTokenGoat(groups, predicate) {
3996
- if (groups === void 0) return false;
3997
- for (const group of groups) {
3998
- for (const h of group.hooks ?? []) {
3999
- if (predicate(h.command)) return true;
4000
- }
4001
- }
4002
- return false;
4003
- }
4004
- function installHooks(scope = "user") {
4005
- return withInstallScope(hookScopeRoot(scope), () => installHooksScoped(scope));
4006
- }
4007
- function hookScopeRoot(scope) {
4008
- return scope === "project" ? normalizeDarwinSystemAlias(process.cwd()) : void 0;
4009
- }
4010
- function installHooksScoped(scope) {
4011
- const p = settingsPath(scope);
4012
- const settings = readSettings(p, { strict: true });
4013
- const hooks = settings.hooks ?? {};
4014
- const scriptPath = claudeHookScriptPath();
4015
- const scriptChanged = withInstallScope(void 0, () => {
4016
- ensureDirSync(path7.dirname(scriptPath));
4017
- return writeIfDifferent(scriptPath, CLAUDECODE_HOOK_SCRIPT);
4018
- });
4019
- let settingsChanged = false;
4020
- for (const [eventKey, eventArg] of HOOK_EVENT_MAP) {
4021
- const expectedCommand = hookCommandFor(scriptPath, eventArg);
4022
- const existingGroups = hooks[eventKey] ?? [];
4023
- const groups = [];
4024
- let strippedStale = false;
4025
- for (const group of existingGroups) {
4026
- const keptHooks = (group.hooks ?? []).filter((h) => {
4027
- const isStale = isTokenGoatHookCommand(h.command) && h.command !== expectedCommand;
4028
- if (isStale) strippedStale = true;
4029
- return !isStale;
4030
- });
4031
- if (keptHooks.length > 0) {
4032
- groups.push({ ...group, hooks: keptHooks });
4033
- } else if ((group.hooks ?? []).length === 0) {
4034
- groups.push(group);
4035
- }
4036
- }
4037
- const isOurs = (command) => command === expectedCommand;
4038
- if (groupHasTokenGoat(groups, isOurs)) {
4039
- const narrowed = toolMatcherFor(eventArg);
4040
- let renarrowed = false;
4041
- if (narrowed !== null) {
4042
- for (let i = 0; i < groups.length; i++) {
4043
- const group = groups[i];
4044
- if (group === void 0) continue;
4045
- const ownHooks = group.hooks ?? [];
4046
- const isOwnGroup = ownHooks.length > 0 && ownHooks.every((h) => isOurs(h.command));
4047
- if (isOwnGroup && group.matcher !== narrowed) {
4048
- groups[i] = { ...group, matcher: narrowed };
4049
- renarrowed = true;
4050
- }
4051
- }
4052
- }
4053
- if (strippedStale || renarrowed) {
4054
- hooks[eventKey] = groups;
4055
- settingsChanged = true;
4056
- }
4057
- continue;
4058
- }
4059
- const matcher = toolMatcherFor(eventArg) ?? "";
4060
- groups.push({ matcher, hooks: [{ type: "command", command: expectedCommand }] });
4061
- hooks[eventKey] = groups;
4062
- settingsChanged = true;
4063
- }
4064
- if (!settingsChanged && !scriptChanged) {
4065
- return { scope, settingsPath: p, alreadyInstalled: true };
4066
- }
4067
- if (settingsChanged) {
4068
- settings.hooks = hooks;
4069
- writeJsonSettings(p, settings);
4070
- }
4071
- return { scope, settingsPath: p, alreadyInstalled: false };
4072
- }
4073
- function uninstallHooks(scope = "user") {
4074
- return withInstallScope(hookScopeRoot(scope), () => uninstallHooksScoped(scope));
4075
- }
4076
- function uninstallHooksScoped(scope) {
4077
- const p = settingsPath(scope);
4078
- const settings = readSettings(p);
4079
- const hooks = settings.hooks;
4080
- if (hooks === void 0) return false;
4081
- const removed = stripOwnHooksFromMap(hooks, isTokenGoatHookCommand);
4082
- const scriptPath = claudeHookScriptPath();
4083
- let removedScript = false;
4084
- if (!anyScopeReferencesShim(scriptPath, scope, hooks)) {
4085
- removedScript = withInstallScope(void 0, () => removeFileInScope(scriptPath));
4086
- }
4087
- if (!removed) return removedScript;
4088
- if (Object.keys(hooks).length === 0) {
4089
- delete settings.hooks;
4090
- } else {
4091
- settings.hooks = hooks;
4092
- }
4093
- writeJsonSettings(p, settings);
4094
- removeCreatedBackups(p);
4095
- return true;
4096
- }
4097
- function isInstalled(scope = "user") {
4098
- const settings = readSettings(settingsPath(scope));
4099
- const hooks = settings.hooks;
4100
- if (hooks === void 0) return false;
4101
- const scriptPath = claudeHookScriptPath();
4102
- if (!fs8.existsSync(scriptPath)) return false;
4103
- for (const [eventKey, eventArg] of HOOK_EVENT_MAP) {
4104
- const expectedCommand = hookCommandFor(scriptPath, eventArg);
4105
- if (!groupHasTokenGoat(hooks[eventKey], (c) => c === expectedCommand)) return false;
4106
- }
4107
- return true;
4108
- }
4109
- var CLAUDE_MD_BEGIN = "<!-- token-goat-begin -->";
4110
- var CLAUDE_MD_END = "<!-- token-goat-end -->";
4111
- function claudeMdPath() {
4112
- return path7.join(os4.homedir(), ".claude", "CLAUDE.md");
4113
- }
4114
- function buildClaudeMdBlock() {
4115
- return buildGuidanceBlock({
4116
- beginMarker: CLAUDE_MD_BEGIN,
4117
- endMarker: CLAUDE_MD_END,
4118
- fallbackToolClause: "Claude Code's own Read, Grep, and Glob preference rules",
4119
- gdrive: loadConfig().gdrive.enabled
4120
- });
4121
- }
4122
- function writeClaudeMdBlock(p) {
4123
- return upsertDelimitedBlock(p, CLAUDE_MD_BEGIN, CLAUDE_MD_END, buildClaudeMdBlock());
4124
- }
4125
- function stripClaudeMdBlock(p) {
4126
- return stripDelimitedBlock(p, CLAUDE_MD_BEGIN, CLAUDE_MD_END);
4127
- }
4128
- function installClaudeMd() {
4129
- const p = claudeMdPath();
4130
- const changed = writeClaudeMdBlock(p);
4131
- return { path: p, alreadyInstalled: !changed };
4132
- }
4133
- function uninstallClaudeMd() {
4134
- return stripClaudeMdBlock(claudeMdPath());
4135
- }
4136
- function findStrayClaudeMdBlocks(searchRoot) {
4137
- const root = searchRoot ?? path7.join(os4.homedir(), ".claude");
4138
- const canonical = path7.resolve(claudeMdPath());
4139
- const found = [];
4140
- if (!fs8.existsSync(root)) return found;
4141
- const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
4142
- const MAX_DEPTH = 6;
4143
- const hasRealBlock = (text) => {
4144
- let sawBegin = false;
4145
- for (const line of text.split(/\r?\n/)) {
4146
- const trimmed = line.trim();
4147
- if (trimmed === CLAUDE_MD_BEGIN) sawBegin = true;
4148
- else if (sawBegin && trimmed === CLAUDE_MD_END) return true;
4149
- }
4150
- return false;
4151
- };
4152
- const walk = (dir, depth) => {
4153
- if (depth > MAX_DEPTH) return;
4154
- let entries;
4155
- try {
4156
- entries = fs8.readdirSync(dir, { withFileTypes: true });
4157
- } catch {
4158
- return;
4159
- }
4160
- for (const entry of entries) {
4161
- const full = path7.join(dir, entry.name);
4162
- if (entry.isDirectory()) {
4163
- if (!SKIP_DIRS.has(entry.name)) walk(full, depth + 1);
4164
- continue;
4165
- }
4166
- if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
4167
- if (path7.resolve(full) === canonical) continue;
4168
- let text;
4169
- try {
4170
- text = fs8.readFileSync(full, "utf8");
4171
- } catch {
4172
- continue;
4173
- }
4174
- if (hasRealBlock(text)) found.push(full);
4175
- }
4176
- };
4177
- walk(root, 0);
4178
- return found.sort();
4179
- }
4180
- function skillMdFrontmatter(gdrive) {
4181
- return `---
4182
- name: token-goat
4183
- ${skillDescriptionLine(gdrive)}
4184
- allowed-tools:
4185
- - Bash
4186
- - Read
4187
- - Grep
4188
- - Glob
4189
- ---`;
4190
- }
4191
- function skillMdContent() {
4192
- const gdrive = loadConfig().gdrive.enabled;
4193
- return `${skillMdFrontmatter(gdrive)}
4194
-
4195
- ${buildGuidanceBody("Claude Code's own Read, Grep, and Glob preference rules", { gdrive })}
4196
- `;
4197
- }
4198
- function skillDir() {
4199
- return path7.join(os4.homedir(), ".claude", "skills", "token-goat");
4200
- }
4201
- function skillPath() {
4202
- return path7.join(skillDir(), "SKILL.md");
4203
- }
4204
- function installSkill() {
4205
- const p = skillPath();
4206
- let existing = null;
4207
- try {
4208
- existing = fs8.readFileSync(p, "utf8");
4209
- } catch {
4210
- }
4211
- const skillContent = skillMdContent();
4212
- if (existing === skillContent) return { path: p, alreadyInstalled: true };
4213
- ensureDirSync(skillDir());
4214
- backupFile(p);
4215
- atomicWriteText(p, skillContent);
4216
- return { path: p, alreadyInstalled: false };
4217
- }
4218
- function uninstallSkill() {
4219
- const dir = skillDir();
4220
- if (!fs8.existsSync(dir)) return false;
4221
- removeCreatedBackups(skillPath());
4222
- assertWriteInScope(dir);
4223
- fs8.rmSync(dir, { recursive: true, force: true });
4224
- return true;
4225
- }
4226
-
4227
- // src/bridges/codex_install.ts
4228
- init_define_import_meta_env();
4229
- import * as crypto2 from "node:crypto";
4230
- import * as fs9 from "node:fs";
4231
- import * as os5 from "node:os";
4232
- import * as path8 from "node:path";
4233
-
4234
- // src/bridges/codex.ts
4235
- init_define_import_meta_env();
4236
- var CODEX_HOOK_SCRIPT = `#!/usr/bin/env node
4237
- // token-goat Codex hook shim. Forwards the hook payload to \`token-goat hook <event>\`, then strips _tg_* keys and injects hookEventName so the response satisfies Codex's strict (additionalProperties:false) schema.
4238
- 'use strict'
4239
- ${SHIM_REQUIRES}
4240
-
4241
- ${SHIM_VALID_HOOK_EVENTS}
4242
-
4243
- // Keep in sync with CLAUDE_CODE_EVENT_NAMES in src/hook_registry.ts -- the
4244
- // live/wired 'token-goat hook <event>' path (src/relay.ts) always emits this
4245
- // PascalCase spelling for hookSpecificOutput.hookEventName, never the raw
4246
- // snake_case event name.
4247
- const HOOK_EVENT_NAME_MAP = {
4248
- pre_tool_use: 'PreToolUse',
4249
- post_tool_use: 'PostToolUse',
4250
- notification: 'Notification',
4251
- stop: 'Stop',
4252
- pre_compact: 'PreCompact',
4253
- post_compact: 'PostCompact',
4254
- user_prompt_submit: 'UserPromptSubmit',
4255
- subagent_stop: 'SubagentStop',
4256
- session_start: 'SessionStart',
4257
- post_tool_use_failure: 'PostToolUseFailure',
4258
- }
4259
-
4260
- function stripTg(value) {
4261
- if (Array.isArray(value)) return value.map(stripTg)
4262
- if (value && typeof value === 'object') {
4263
- const out = {}
4264
- for (const [k, v] of Object.entries(value)) {
4265
- if (k.startsWith('_tg_')) continue
4266
- out[k] = stripTg(v)
4267
- }
4268
- return out
4269
- }
4270
- return value
4271
- }
4272
-
4273
- ${SHIM_TRY_IN_PROCESS}
4274
-
4275
- ${SHIM_MAX_BUFFER_CONST}
4276
-
4277
- async function main() {
4278
- const eventName = process.argv[2] || ''
4279
- if (!VALID_HOOK_EVENTS.has(eventName)) {
4280
- process.stdout.write('{}')
4281
- return
4282
- }
4283
- let input = ''
4284
- try {
4285
- input = require('node:fs').readFileSync(0, 'utf8')
4286
- } catch {
4287
- process.stdout.write('{}')
4288
- return
4289
- }
4290
- // process.argv[3], when present, is the absolute path to the token-goat CLI entry
4291
- // that ran 'token-goat install --codex' (baked in by hookCommandFor in
4292
- // codex_install.ts). Try the in-process hook lib first (tryInProcess above) --
4293
- // this avoids spawning a second node process altogether. If that's unavailable,
4294
- // invoking the entry directly via process.execPath sidesteps PATH/shell resolution
4295
- // for this inner call, the same single-point-of-failure class fixed for the Copilot
4296
- // CLI bridge's inner hook call (a bare 'token-goat' on PATH failing to resolve
4297
- // crashes this call, and Codex -- like Copilot -- fails closed on a non-zero-exit
4298
- // hook). Falls back further to the old PATH-based shell:true invocation when argv[3]
4299
- // is absent (an older cached hook config). A 3000ms timeout/killSignal on both
4300
- // spawnSync fallbacks bounds them well under Codex's own hook timeout budget, so
4301
- // token-goat degrades to its own fail-open '{}' rather than being force-killed by
4302
- // Codex first.
4303
- const entryPath = process.argv[3]
4304
- let stdout = await tryInProcess(entryPath, eventName, input)
4305
- if (stdout === undefined) {
4306
- ${SHIM_SPAWN_LADDER}
4307
- : spawnSync('token-goat hook ' + eventName, {
4308
- input,
4309
- encoding: 'utf8',
4310
- shell: true,
4311
- timeout: 3000,
4312
- killSignal: 'SIGKILL',
4313
- maxBuffer: SHIM_MAX_BUFFER_BYTES,
4314
- })
4315
- if (res.status !== 0 || !res.stdout) {
4316
- process.stdout.write('{}')
4317
- return
4318
- }
4319
- stdout = res.stdout
4320
- }
4321
- let parsed
4322
- try {
4323
- parsed = JSON.parse(stdout)
4324
- } catch {
4325
- process.stdout.write('{}')
4326
- return
4327
- }
4328
- parsed = stripTg(parsed)
4329
- const hso = parsed && parsed.hookSpecificOutput
4330
- if (hso && typeof hso === 'object' && !hso.hookEventName) {
4331
- hso.hookEventName = HOOK_EVENT_NAME_MAP[eventName] || eventName
4332
- }
4333
- process.stdout.write(JSON.stringify(parsed))
4334
- }
4335
-
4336
- main().catch(() => {
4337
- process.stdout.write('{}')
4338
- })
4339
- `;
4340
-
4341
- // src/bridges/matcher_group.ts
4342
- init_define_import_meta_env();
4343
- function findTokenGoatEntryPosition(groups, matcher, predicate) {
4344
- if (groups === void 0) return void 0;
4345
- for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
4346
- const group = groups[groupIndex];
4347
- if (group.matcher !== matcher) continue;
4348
- const hookList = group.hooks ?? [];
4349
- for (let hookIndex = 0; hookIndex < hookList.length; hookIndex++) {
4350
- if (predicate(hookList[hookIndex].command)) return { groupIndex, hookIndex };
4351
- }
4352
- }
4353
- return void 0;
4354
- }
4355
- function findAnyTokenGoatEntryPosition(groups, predicate) {
4356
- if (groups === void 0) return void 0;
4357
- for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
4358
- const hookList = groups[groupIndex].hooks ?? [];
4359
- for (let hookIndex = 0; hookIndex < hookList.length; hookIndex++) {
4360
- if (predicate(hookList[hookIndex].command)) return { groupIndex, hookIndex };
4361
- }
4362
- }
4363
- return void 0;
4364
- }
4365
- function groupHasTokenGoat2(groups, matcher, predicate) {
4366
- return findTokenGoatEntryPosition(groups, matcher, predicate) !== void 0;
4367
- }
4368
-
4369
- // src/bridges/codex_install.ts
4370
- var CODEX_COMMAND_MARKER = "token-goat-shim";
4371
- var CODEX_MATCHERS = ["view_image|shell|bash", "apply_patch", "web_search"];
4372
- var CODEX_HOOK_EVENTS = ["PreToolUse", "PostToolUse"];
4373
- var CODEX_EVENT_ARG = {
4374
- PreToolUse: "pre_tool_use",
4375
- PostToolUse: "post_tool_use"
4376
- };
4377
- var CODEX_GLOBAL_HOOK_EVENTS = ["PreCompact", "UserPromptSubmit", "SubagentStop"];
4378
- var CODEX_GLOBAL_EVENT_ARG = {
4379
- PreCompact: "pre_compact",
4380
- UserPromptSubmit: "user_prompt_submit",
4381
- SubagentStop: "subagent_stop"
4382
- };
4383
- var CodexConfigParseError = class extends Error {
4384
- };
4385
- function codexConfigPath() {
4386
- return path8.join(os5.homedir(), ".codex", "config.toml");
4387
- }
4388
- function codexAgentsPath() {
4389
- return path8.join(os5.homedir(), ".codex", "AGENTS.md");
4390
- }
4391
- function codexHookScriptPath() {
4392
- return path8.join(os5.homedir(), ".codex", "hooks", "token-goat-shim.js");
4393
- }
4394
- function readCodexConfig(p, opts = {}) {
4395
- let raw;
4396
- try {
4397
- raw = fs9.readFileSync(p, "utf8");
4398
- } catch {
4399
- return {};
4400
- }
4401
- try {
4402
- const parsed = parse(raw);
4403
- return parsed;
4404
- } catch (e) {
4405
- if (opts.strict === true) {
4406
- throw new CodexConfigParseError(
4407
- `Codex config file '${p}' exists but contains invalid TOML. Fix or back up the file before running install. (${extractErrorMessage(e)})`
4408
- );
4409
- }
4410
- return {};
4411
- }
4412
- }
4413
- var CODEX_MARKER_PATTERN = anchoredMarkerPattern(CODEX_COMMAND_MARKER);
4414
- function isCodexTokenGoatCommand(command) {
4415
- return typeof command === "string" && CODEX_MARKER_PATTERN.test(command);
4416
- }
4417
- function anyGroupHasTokenGoat(groups, predicate = isCodexTokenGoatCommand) {
4418
- return findAnyTokenGoatEntryPosition(groups, predicate) !== void 0;
4419
- }
4420
- function codexHookCommandFor(scriptPath, eventArg) {
4421
- const base = hookCommandFor(scriptPath, eventArg);
4422
- return process.platform === "win32" ? `& ${base}` : base;
4423
- }
4424
- function computeCodexHookHash(eventArg, command, matcher) {
4425
- const norm = {
4426
- event_name: eventArg,
4427
- hooks: [
4428
- {
4429
- async: false,
4430
- command,
4431
- timeout: 600,
4432
- type: "command"
4433
- }
4434
- ]
4435
- };
4436
- if (matcher !== void 0) {
4437
- norm["matcher"] = matcher;
4438
- }
4439
- const serialized = JSON.stringify(norm);
4440
- const hash = crypto2.createHash("sha256").update(serialized).digest("hex");
4441
- return `sha256:${hash}`;
4442
- }
4443
- function installCodex() {
4444
- const configPath = codexConfigPath();
4445
- const agentsPath = codexAgentsPath();
4446
- const scriptPath = codexHookScriptPath();
4447
- ensureDirSync(path8.dirname(scriptPath));
4448
- atomicWriteText(scriptPath, CODEX_HOOK_SCRIPT);
4449
- const config = readCodexConfig(configPath, { strict: true });
4450
- const hooks = config.hooks ?? {};
4451
- let hooksChanged = false;
4452
- const staleStateKeysToRemove = [];
4453
- for (const event of CODEX_HOOK_EVENTS) {
4454
- const eventArg = CODEX_EVENT_ARG[event];
4455
- const expectedCommand = codexHookCommandFor(scriptPath, eventArg);
4456
- const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
4457
- let removedGroups = 0;
4458
- for (let idx = 0; idx < groups.length; idx++) {
4459
- const g = groups[idx];
4460
- if (g.matcher === void 0 || CODEX_MATCHERS.includes(g.matcher)) continue;
4461
- const hookList = g.hooks ?? [];
4462
- const keptHooks = hookList.filter((h) => !isCodexTokenGoatCommand(h.command));
4463
- if (keptHooks.length === hookList.length) continue;
4464
- const originalIdx = idx + removedGroups;
4465
- hookList.forEach((h, hi) => {
4466
- if (isCodexTokenGoatCommand(h.command)) staleStateKeysToRemove.push(`${configPath}:${eventArg}:${originalIdx}:${hi}`);
4467
- });
4468
- hooksChanged = true;
4469
- if (keptHooks.length > 0) {
4470
- groups[idx] = { ...g, hooks: keptHooks };
4471
- } else {
4472
- groups.splice(idx, 1);
4473
- idx--;
4474
- removedGroups++;
4475
- }
4476
- }
4477
- for (const matcher of CODEX_MATCHERS) {
4478
- if (groupHasTokenGoat2(groups, matcher, (c) => c === expectedCommand)) continue;
4479
- const nextGroups = stripStaleGroupHooks(groups, isCodexTokenGoatCommand, { matcher });
4480
- groups.length = 0;
4481
- groups.push(...nextGroups);
4482
- groups.push({ matcher, hooks: [{ type: "command", command: expectedCommand }] });
4483
- hooksChanged = true;
4484
- }
4485
- hooks[event] = groups;
4486
- }
4487
- for (const event of CODEX_GLOBAL_HOOK_EVENTS) {
4488
- const eventArg = CODEX_GLOBAL_EVENT_ARG[event];
4489
- const expectedCommand = codexHookCommandFor(scriptPath, eventArg);
4490
- const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
4491
- if (anyGroupHasTokenGoat(groups, (c) => c === expectedCommand)) {
4492
- hooks[event] = groups;
4493
- continue;
4494
- }
4495
- const nextGroups = stripStaleGroupHooks(groups, isCodexTokenGoatCommand);
4496
- nextGroups.push({ hooks: [{ type: "command", command: expectedCommand }] });
4497
- hooks[event] = nextGroups;
4498
- hooksChanged = true;
4499
- }
4500
- const agentsChanged = writeAgentsBlock(agentsPath);
4501
- const hooksState = hooks["state"] ?? {};
4502
- for (const key of staleStateKeysToRemove) {
4503
- if (key in hooksState) {
4504
- delete hooksState[key];
4505
- hooksChanged = true;
4506
- }
4507
- }
4508
- for (const event of CODEX_HOOK_EVENTS) {
4509
- const eventArg = CODEX_EVENT_ARG[event];
4510
- const groups = hooks[event] ?? [];
4511
- for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
4512
- const g = groups[groupIndex];
4513
- if (g && g.matcher && CODEX_MATCHERS.includes(g.matcher)) {
4514
- const hookList = g.hooks ?? [];
4515
- for (let hookIndex = 0; hookIndex < hookList.length; hookIndex++) {
4516
- const h = hookList[hookIndex];
4517
- if (h && isCodexTokenGoatCommand(h.command)) {
4518
- const stateKey = `${configPath}:${eventArg}:${groupIndex}:${hookIndex}`;
4519
- const hash = computeCodexHookHash(eventArg, h.command, g.matcher);
4520
- if (hooksState[stateKey]?.trusted_hash !== hash) {
4521
- hooksState[stateKey] = { ...hooksState[stateKey], trusted_hash: hash };
4522
- hooksChanged = true;
4523
- }
4524
- }
4525
- }
4526
- }
4527
- }
4528
- }
4529
- for (const event of CODEX_GLOBAL_HOOK_EVENTS) {
4530
- const eventArg = CODEX_GLOBAL_EVENT_ARG[event];
4531
- const groups = hooks[event] ?? [];
4532
- for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
4533
- const g = groups[groupIndex];
4534
- if (!g) continue;
4535
- const hookList = g.hooks ?? [];
4536
- for (let hookIndex = 0; hookIndex < hookList.length; hookIndex++) {
4537
- const h = hookList[hookIndex];
4538
- if (h && isCodexTokenGoatCommand(h.command)) {
4539
- const stateKey = `${configPath}:${eventArg}:${groupIndex}:${hookIndex}`;
4540
- const hash = computeCodexHookHash(eventArg, h.command);
4541
- if (hooksState[stateKey]?.trusted_hash !== hash) {
4542
- hooksState[stateKey] = { ...hooksState[stateKey], trusted_hash: hash };
4543
- hooksChanged = true;
4544
- }
4545
- }
4546
- }
4547
- }
4548
- }
4549
- if (Object.keys(hooksState).length > 0) {
4550
- hooks["state"] = hooksState;
4551
- }
4552
- if (hooksChanged) {
4553
- config.hooks = hooks;
4554
- ensureDirSync(path8.dirname(configPath));
4555
- backupFile(configPath);
4556
- atomicWriteText(configPath, stringify(config));
4557
- }
4558
- return {
4559
- configPath,
4560
- agentsPath,
4561
- hookScriptPath: scriptPath,
4562
- alreadyInstalled: !hooksChanged && !agentsChanged
4563
- };
4564
- }
4565
- function uninstallCodex() {
4566
- const configPath = codexConfigPath();
4567
- const agentsPath = codexAgentsPath();
4568
- const scriptPath = codexHookScriptPath();
4569
- let removedAny = false;
4570
- const config = readCodexConfig(configPath);
4571
- const hooks = config.hooks;
4572
- if (hooks !== void 0) {
4573
- const hooksRemoved = stripOwnHooksFromMap(hooks, isCodexTokenGoatCommand);
4574
- const hooksState = hooks["state"];
4575
- let stateRemoved = false;
4576
- if (hooksState && typeof hooksState === "object") {
4577
- const prefix = `${configPath}:`;
4578
- for (const key of Object.keys(hooksState)) {
4579
- if (key.startsWith(prefix)) {
4580
- delete hooksState[key];
4581
- stateRemoved = true;
4582
- }
4583
- }
4584
- if (Object.keys(hooksState).length === 0) {
4585
- delete hooks["state"];
4586
- }
4587
- }
4588
- if (hooksRemoved || stateRemoved) {
4589
- if (Object.keys(hooks).length === 0) {
4590
- delete config.hooks;
4591
- } else {
4592
- config.hooks = hooks;
4593
- }
4594
- backupFile(configPath);
4595
- atomicWriteText(configPath, stringify(config));
4596
- removedAny = true;
4597
- }
4598
- }
4599
- if (stripAgentsBlock(agentsPath)) {
4600
- removedAny = true;
4601
- }
4602
- try {
4603
- fs9.unlinkSync(scriptPath);
4604
- removedAny = true;
4605
- } catch {
4606
- }
4607
- removeCreatedBackups(configPath);
4608
- return removedAny;
4609
- }
4610
- function isCodexInstalled() {
4611
- const configPath = codexConfigPath();
4612
- const config = readCodexConfig(configPath);
4613
- const hooks = config.hooks;
4614
- if (hooks === void 0) return false;
4615
- const scriptPath = codexHookScriptPath();
4616
- const hooksState = hooks["state"] ?? {};
4617
- for (const event of CODEX_HOOK_EVENTS) {
4618
- const eventArg = CODEX_EVENT_ARG[event];
4619
- const expectedCommand = codexHookCommandFor(scriptPath, eventArg);
4620
- const groups = hooks[event] ?? [];
4621
- for (const matcher of CODEX_MATCHERS) {
4622
- const position = findTokenGoatEntryPosition(groups, matcher, (c) => c === expectedCommand);
4623
- if (position === void 0) return false;
4624
- const stateKey = `${configPath}:${eventArg}:${position.groupIndex}:${position.hookIndex}`;
4625
- const expectedHash = computeCodexHookHash(eventArg, expectedCommand, matcher);
4626
- if (hooksState[stateKey]?.trusted_hash !== expectedHash) return false;
4627
- }
4628
- }
4629
- for (const event of CODEX_GLOBAL_HOOK_EVENTS) {
4630
- const eventArg = CODEX_GLOBAL_EVENT_ARG[event];
4631
- const expectedCommand = codexHookCommandFor(scriptPath, eventArg);
4632
- const groups = hooks[event] ?? [];
4633
- const position = findAnyTokenGoatEntryPosition(groups, (c) => c === expectedCommand);
4634
- if (position === void 0) return false;
4635
- const stateKey = `${configPath}:${eventArg}:${position.groupIndex}:${position.hookIndex}`;
4636
- const expectedHash = computeCodexHookHash(eventArg, expectedCommand);
4637
- if (hooksState[stateKey]?.trusted_hash !== expectedHash) return false;
4638
- }
4639
- if (!fs9.existsSync(scriptPath)) return false;
4640
- let agents;
4641
- try {
4642
- agents = fs9.readFileSync(codexAgentsPath(), "utf8");
4643
- } catch {
4644
- return false;
4645
- }
4646
- return agents.includes(AGENTS_BEGIN) && agents.includes(AGENTS_END);
4647
- }
4648
- var AGENTS_BEGIN = "<!-- token-goat-codex-begin -->";
4649
- var AGENTS_END = "<!-- token-goat-codex-end -->";
4650
- function buildAgentsBlock() {
4651
- return buildGuidanceBlock({
4652
- beginMarker: AGENTS_BEGIN,
4653
- endMarker: AGENTS_END,
4654
- fallbackToolClause: "Codex's native `shell`, `apply_patch`, and `view_image` tools (shell commands like `cat`/`type` run inside `shell`)",
4655
- gdrive: loadConfig().gdrive.enabled
4656
- });
4657
- }
4658
- function writeAgentsBlock(p) {
4659
- return upsertDelimitedBlock(p, AGENTS_BEGIN, AGENTS_END, buildAgentsBlock());
4660
- }
4661
- function stripAgentsBlock(p) {
4662
- return stripDelimitedBlock(p, AGENTS_BEGIN, AGENTS_END);
4663
- }
4664
-
4665
- // src/bridges/kimi_install.ts
4666
- init_define_import_meta_env();
4667
- import * as fs10 from "node:fs";
4668
- import * as os6 from "node:os";
4669
- import * as path9 from "node:path";
4670
-
4671
- // src/bridges/kimi.ts
4672
- init_define_import_meta_env();
4673
- var KIMI_HOOK_SCRIPT = `#!/usr/bin/env node
4674
- // token-goat Kimi Code hook shim. Forwards the hook payload to \`token-goat hook <event>\`, then rewrites the response into Kimi's own contract: permissionDecision "deny" for a block, top-level "message" for a hint, empty stdout for a no-op.
4675
- 'use strict'
4676
- ${SHIM_REQUIRES}
4677
-
4678
- ${SHIM_VALID_HOOK_EVENTS}
4679
-
4680
- // Kimi Code publishes no ambient per-session env var identifying its own hook
4681
- // subprocesses, so the harness identity has to be injected here for
4682
- // detectHarness() (src/bridges/registry.ts) to resolve 'kimi'.
4683
- process.env.TOKEN_GOAT_HARNESS_OVERRIDE = 'kimi'
4684
-
4685
- // Pull the hint text out of a token-goat response: hookSpecificOutput.additionalContext
4686
- // (most events) or the top-level systemMessage (notification/pre_compact, which Claude
4687
- // Code rejects additionalContext on). Kimi reads neither, so both fold into its 'message'.
4688
- function hintText(parsed) {
4689
- if (!parsed || typeof parsed !== 'object') return ''
4690
- const hso = parsed.hookSpecificOutput
4691
- if (hso && typeof hso === 'object' && typeof hso.additionalContext === 'string') return hso.additionalContext
4692
- if (typeof parsed.systemMessage === 'string') return parsed.systemMessage
4693
- return ''
4694
- }
4695
-
4696
- // Translate a token-goat (Claude Code shaped) response into Kimi's contract.
4697
- // Returns '' for anything Kimi cannot act on, which the caller writes as empty
4698
- // stdout rather than '{}' -- see this module's docstring for why that matters
4699
- // on UserPromptSubmit.
4700
- function toKimi(parsed) {
4701
- if (!parsed || typeof parsed !== 'object') return ''
4702
- if (parsed.decision === 'block' && typeof parsed.reason === 'string' && parsed.reason) {
4703
- return JSON.stringify({
4704
- message: parsed.reason,
4705
- hookSpecificOutput: { permissionDecision: 'deny', permissionDecisionReason: parsed.reason },
4706
- })
4707
- }
4708
- const hint = hintText(parsed)
4709
- if (hint) return JSON.stringify({ message: hint })
4710
- return ''
4711
- }
4712
-
4713
- ${SHIM_TRY_IN_PROCESS}
4714
-
4715
- ${SHIM_MAX_BUFFER_CONST}
4716
-
4717
- async function main() {
4718
- const eventName = process.argv[2] || ''
4719
- if (!VALID_HOOK_EVENTS.has(eventName)) return
4720
- let input = ''
4721
- try {
4722
- input = require('node:fs').readFileSync(0, 'utf8')
4723
- } catch {
4724
- return
4725
- }
4726
- // process.argv[3], when present, is the absolute path to the token-goat CLI entry that ran
4727
- // 'token-goat install --kimi' (baked in by hookCommandFor in kimi_install.ts). Same three-step
4728
- // ladder as the Codex shim: in-process hook lib, then the baked entry via process.execPath,
4729
- // then a PATH-based shell:true invocation for an older cached hook config. The 3000ms
4730
- // timeout keeps both spawn fallbacks well inside Kimi's own 30s default hook timeout
4731
- // (docs/en/customization/hooks.md), so token-goat degrades to its own fail-open no-op.
4732
- const entryPath = process.argv[3]
4733
- let stdout = await tryInProcess(entryPath, eventName, input)
4734
- if (stdout === undefined) {
4735
- ${SHIM_SPAWN_LADDER}
4736
- : spawnSync('token-goat hook ' + eventName, {
4737
- input,
4738
- encoding: 'utf8',
4739
- shell: true,
4740
- timeout: 3000,
4741
- killSignal: 'SIGKILL',
4742
- maxBuffer: SHIM_MAX_BUFFER_BYTES,
4743
- })
4744
- if (res.status !== 0 || !res.stdout) return
4745
- stdout = res.stdout
4746
- }
4747
- let parsed
4748
- try {
4749
- parsed = JSON.parse(stdout)
4750
- } catch {
4751
- return
4752
- }
4753
- const out = toKimi(parsed)
4754
- if (out) process.stdout.write(out)
4755
- }
4756
-
4757
- main().catch(() => {
4758
- // Fail open: write nothing, exit 0. Kimi treats a silent hook as "allow".
4759
- })
4760
- `;
4761
-
4762
- // src/bridges/kimi_install.ts
4763
- var KIMI_EVENT_ARG = {
4764
- PreToolUse: "pre_tool_use",
4765
- PostToolUse: "post_tool_use",
4766
- PreCompact: "pre_compact",
4767
- UserPromptSubmit: "user_prompt_submit",
4768
- SubagentStop: "subagent_stop",
4769
- SessionStart: "session_start"
4770
- };
4771
- var KIMI_HOOK_EVENTS = Object.keys(KIMI_EVENT_ARG);
4772
- var KimiConfigParseError = class extends Error {
4773
- };
4774
- function kimiHome() {
4775
- const override = process.env["KIMI_CODE_HOME"];
4776
- if (override !== void 0 && override.trim() !== "") return path9.resolve(override);
4777
- return path9.join(os6.homedir(), ".kimi-code");
4778
- }
4779
- function kimiConfigPath() {
4780
- return path9.join(kimiHome(), "config.toml");
4781
- }
4782
- function kimiAgentsPath() {
4783
- return path9.join(kimiHome(), "AGENTS.md");
4784
- }
4785
- function kimiHookScriptPath() {
4786
- return path9.join(kimiHome(), "hooks", "token-goat-shim.js");
4787
- }
4788
- function kimiSkillDir() {
4789
- return path9.join(kimiHome(), "skills", "token-goat");
4790
- }
4791
- function kimiSkillPath() {
4792
- return path9.join(kimiSkillDir(), "SKILL.md");
4793
- }
4794
- function readKimiConfig(p, opts = {}) {
4795
- let raw;
4796
- try {
4797
- raw = fs10.readFileSync(p, "utf8");
4798
- } catch {
4799
- return {};
4800
- }
4801
- try {
4802
- return parse(raw);
4803
- } catch (e) {
4804
- if (opts.strict === true) {
4805
- throw new KimiConfigParseError(
4806
- `Kimi Code config file '${p}' exists but contains invalid TOML. Fix or back up the file before running install. (${extractErrorMessage(e)})`
4807
- );
4808
- }
4809
- return {};
4810
- }
4811
- }
4812
- var KIMI_COMMAND_MARKER = "token-goat";
4813
- var KIMI_MARKER_PATTERN = anchoredMarkerPattern(KIMI_COMMAND_MARKER);
4814
- function isKimiTokenGoatCommand(command) {
4815
- return typeof command === "string" && KIMI_MARKER_PATTERN.test(command);
4816
- }
4817
- var AGENTS_BEGIN2 = "<!-- token-goat-kimi-begin -->";
4818
- var AGENTS_END2 = "<!-- token-goat-kimi-end -->";
4819
- function buildKimiAgentsBlock() {
4820
- return buildGuidanceBlock({
4821
- beginMarker: AGENTS_BEGIN2,
4822
- endMarker: AGENTS_END2,
4823
- fallbackToolClause: "Kimi Code's native `Read`, `Grep`, `Glob`, and `ReadMediaFile` tools (shell commands like `cat`/`type` run inside `Bash`)",
4824
- gdrive: loadConfig().gdrive.enabled
4825
- });
4826
- }
4827
- function writeKimiAgentsBlock(p) {
4828
- return upsertDelimitedBlock(p, AGENTS_BEGIN2, AGENTS_END2, buildKimiAgentsBlock());
4829
- }
4830
- function stripKimiAgentsBlock(p) {
4831
- return stripDelimitedBlock(p, AGENTS_BEGIN2, AGENTS_END2);
4832
- }
4833
- function kimiSkillContent() {
4834
- const gdrive = loadConfig().gdrive.enabled;
4835
- const frontmatter = [
4836
- "---",
4837
- "name: token-goat",
4838
- skillDescriptionLine(gdrive),
4839
- "---"
4840
- ].join("\n");
4841
- const body = buildGuidanceBody("Kimi Code's own Read, Grep, and Glob preference rules", { gdrive });
4842
- return `${frontmatter}
4843
-
4844
- ${body}
4845
- `;
4846
- }
4847
- function writeKimiSkill() {
4848
- return writeIfDifferent(kimiSkillPath(), kimiSkillContent(), true);
4849
- }
4850
- function installKimi() {
4851
- const configPath = kimiConfigPath();
4852
- const agentsPath = kimiAgentsPath();
4853
- const scriptPath = kimiHookScriptPath();
4854
- ensureDirSync(path9.dirname(scriptPath));
4855
- atomicWriteText(scriptPath, KIMI_HOOK_SCRIPT);
4856
- const config = readKimiConfig(configPath, { strict: true });
4857
- const existing = Array.isArray(config.hooks) ? config.hooks : [];
4858
- const desired = KIMI_HOOK_EVENTS.map((event) => ({
4859
- event,
4860
- command: hookCommandFor(scriptPath, KIMI_EVENT_ARG[event] ?? "")
4861
- }));
4862
- const foreign = existing.filter((h) => !isKimiTokenGoatCommand(h?.command));
4863
- const ours = existing.filter((h) => isKimiTokenGoatCommand(h?.command));
4864
- const hooksChanged = JSON.stringify(ours) !== JSON.stringify(desired);
4865
- const agentsChanged = writeKimiAgentsBlock(agentsPath);
4866
- const skillChanged = writeKimiSkill();
4867
- if (hooksChanged) {
4868
- config.hooks = [...foreign, ...desired];
4869
- ensureDirSync(path9.dirname(configPath));
4870
- backupFile(configPath);
4871
- atomicWriteText(configPath, stringify(config));
4872
- }
4873
- return {
4874
- configPath,
4875
- agentsPath,
4876
- hookScriptPath: scriptPath,
4877
- skillPath: kimiSkillPath(),
4878
- alreadyInstalled: !hooksChanged && !agentsChanged && !skillChanged
4879
- };
4880
- }
4881
- function uninstallKimi() {
4882
- const configPath = kimiConfigPath();
4883
- const config = readKimiConfig(configPath);
4884
- let removed = false;
4885
- if (Array.isArray(config.hooks)) {
4886
- const kept = config.hooks.filter((h) => !isKimiTokenGoatCommand(h?.command));
4887
- if (kept.length !== config.hooks.length) {
4888
- if (kept.length === 0) {
4889
- delete config.hooks;
4890
- } else {
4891
- config.hooks = kept;
4892
- }
4893
- backupFile(configPath);
4894
- atomicWriteText(configPath, stringify(config));
4895
- removed = true;
4896
- }
4897
- }
4898
- if (stripKimiAgentsBlock(kimiAgentsPath())) removed = true;
4899
- try {
4900
- if (fs10.existsSync(kimiSkillDir())) {
4901
- removeCreatedBackups(kimiSkillPath());
4902
- fs10.rmSync(kimiSkillDir(), { recursive: true, force: true });
4903
- removed = true;
4904
- }
4905
- } catch {
4906
- }
4907
- try {
4908
- if (fs10.existsSync(kimiHookScriptPath())) {
4909
- fs10.rmSync(kimiHookScriptPath(), { force: true });
4910
- removed = true;
4911
- }
4912
- } catch {
4913
- }
4914
- removeCreatedBackups(configPath);
4915
- return removed;
4916
- }
4917
- function isKimiInstalled() {
4918
- const config = readKimiConfig(kimiConfigPath());
4919
- if (!Array.isArray(config.hooks)) return false;
4920
- const scriptPath = kimiHookScriptPath();
4921
- for (const event of KIMI_HOOK_EVENTS) {
4922
- const expected = hookCommandFor(scriptPath, KIMI_EVENT_ARG[event] ?? "");
4923
- if (!config.hooks.some((h) => h?.event === event && h?.command === expected)) return false;
4924
- }
4925
- return true;
4926
- }
4927
-
4928
- // src/bridges/grok_install.ts
4929
- init_define_import_meta_env();
4930
- import * as fs11 from "node:fs";
4931
- import * as os7 from "node:os";
4932
- import * as path10 from "node:path";
4933
-
4934
- // src/bridges/grok.ts
4935
- init_define_import_meta_env();
4936
- var GROK_HOOK_SCRIPT = `#!/usr/bin/env node
4937
- // token-goat Grok CLI hook shim. Forwards the hook payload to \`token-goat hook <event>\`, then -- for pre_tool_use only -- translates token-goat's {"decision":"block",...} deny shape into Grok's documented {"decision":"deny",...} shape and sets exit code 2.
4938
- 'use strict'
4939
- ${SHIM_REQUIRES}
4940
-
4941
- // Keep in sync with HOOK_EVENTS in src/types.ts.
4942
- const VALID_HOOK_EVENTS = new Set([
4943
- 'pre_tool_use',
4944
- 'post_tool_use',
4945
- 'notification',
4946
- 'stop',
4947
- 'pre_compact',
4948
- 'user_prompt_submit',
4949
- 'subagent_stop',
4950
- ])
4951
-
4952
- function allowResponse(eventName) {
4953
- return eventName === 'pre_tool_use' ? '{"decision":"allow"}' : '{}'
4954
- }
4955
-
4956
- ${SHIM_TRY_IN_PROCESS}
4957
-
4958
- ${SHIM_MAX_BUFFER_CONST}
4959
-
4960
- async function main() {
4961
- const eventName = process.argv[2] || ''
4962
- if (!VALID_HOOK_EVENTS.has(eventName)) {
4963
- process.stdout.write('{}')
4964
- return
4965
- }
4966
- let input = ''
4967
- try {
4968
- input = require('node:fs').readFileSync(0, 'utf8')
4969
- } catch {
4970
- process.stdout.write(allowResponse(eventName))
4971
- return
4972
- }
4973
- // process.argv[3], when present, is the absolute path to the token-goat CLI entry
4974
- // that ran 'token-goat install --grok' (baked in by hookCommandFor in
4975
- // grok_install.ts). Try the in-process hook lib first (tryInProcess above) -- this
4976
- // avoids spawning a second node process altogether. If that's unavailable, invoking
4977
- // the entry directly via process.execPath sidesteps PATH resolution for this inner
4978
- // call, the same reasoning as every other bridge shim's inner call. Falls back
4979
- // further to the old PATH-based shell:true invocation when argv[3] is absent. A
4980
- // 3000ms timeout/killSignal on both spawnSync fallbacks bounds them well under
4981
- // Grok's own hook timeout (default 5s, per the hooks doc), so token-goat degrades to
4982
- // its own fail-open response rather than being force-killed by Grok first.
4983
- const entryPath = process.argv[3]
4984
- let stdout = await tryInProcess(entryPath, eventName, input)
4985
- if (stdout === undefined) {
4986
- ${SHIM_SPAWN_LADDER}
4987
- : spawnSync('token-goat hook ' + eventName, {
4988
- input,
4989
- encoding: 'utf8',
4990
- shell: true,
4991
- timeout: 3000,
4992
- killSignal: 'SIGKILL',
4993
- maxBuffer: SHIM_MAX_BUFFER_BYTES,
4994
- })
4995
- if (res.status !== 0 || !res.stdout) {
4996
- process.stdout.write(allowResponse(eventName))
4997
- return
4998
- }
4999
- stdout = res.stdout
5000
- }
5001
- let parsed
5002
- try {
5003
- parsed = JSON.parse(stdout)
5004
- } catch {
5005
- process.stdout.write(allowResponse(eventName))
5006
- return
5007
- }
5008
-
5009
- if (eventName !== 'pre_tool_use') {
5010
- // Passive events: Grok ignores stdout entirely (per the hooks doc), so the exact
5011
- // shape doesn't matter -- forward token-goat's response verbatim.
5012
- process.stdout.write(JSON.stringify(parsed))
5013
- return
5014
- }
5015
-
5016
- if (parsed && parsed.decision === 'block') {
5017
- process.stdout.write(JSON.stringify({ decision: 'deny', reason: parsed.reason || 'blocked by token-goat' }))
5018
- process.exitCode = 2
5019
- return
5020
- }
5021
- process.stdout.write('{"decision":"allow"}')
5022
- }
5023
-
5024
- main().catch((err) => {
5025
- try {
5026
- process.stdout.write(allowResponse(process.argv[2] || ''))
5027
- } catch {
5028
- // stdout itself is broken; nothing more can be done here.
5029
- }
5030
- })
5031
- `;
5032
-
5033
- // src/bridges/grok_install.ts
5034
- var GROK_HOOK_EVENTS = ["PreToolUse", "PostToolUse", "PreCompact", "UserPromptSubmit", "SubagentStop"];
5035
- var GROK_EVENT_ARG = {
5036
- PreToolUse: "pre_tool_use",
5037
- PostToolUse: "post_tool_use",
5038
- PreCompact: "pre_compact",
5039
- UserPromptSubmit: "user_prompt_submit",
5040
- SubagentStop: "subagent_stop"
5041
- };
5042
- function grokHooksDir() {
5043
- return path10.join(os7.homedir(), ".grok", "hooks");
5044
- }
5045
- function grokConfigPath() {
5046
- return path10.join(grokHooksDir(), "token-goat.json");
5047
- }
5048
- function grokHookScriptPath() {
5049
- return path10.join(grokHooksDir(), "token-goat-shim.js");
5050
- }
5051
- function buildConfig2(scriptPath) {
5052
- const hooks = {};
5053
- for (const event of GROK_HOOK_EVENTS) {
5054
- hooks[event] = [
5055
- { matcher: "", hooks: [{ type: "command", command: hookCommandFor(scriptPath, GROK_EVENT_ARG[event]) }] }
5056
- ];
5057
- }
5058
- return { hooks };
5059
- }
5060
- function installGrok() {
5061
- const configPath = grokConfigPath();
5062
- const scriptPath = grokHookScriptPath();
5063
- const scriptChanged = writeIfDifferent(scriptPath, GROK_HOOK_SCRIPT);
5064
- const desiredText = `${JSON.stringify(buildConfig2(scriptPath), null, 2)}
5065
- `;
5066
- const configChanged = writeIfDifferent(configPath, desiredText, true);
5067
- return { configPath, hookScriptPath: scriptPath, alreadyInstalled: !scriptChanged && !configChanged };
1520
+ function commandHash(command, cwd = null) {
1521
+ return Promise.resolve(commandHashSync(command, cwd));
5068
1522
  }
5069
- function uninstallGrok() {
5070
- const configPath = grokConfigPath();
5071
- const scriptPath = grokHookScriptPath();
5072
- let removedAny = false;
5073
- try {
5074
- fs11.unlinkSync(configPath);
5075
- removedAny = true;
5076
- } catch {
1523
+ function computeBashFingerprints(command, cwd) {
1524
+ const fingerprints = {};
1525
+ if (cwd && (isGitMutableCommand(command) || isGitPushCommand(command) || isTestRunnerCommand2(command) || isLintCommand(command) || isBuildCommand(command) || isNpmRunScriptCommand(command))) {
1526
+ const fp = gitStateFingerprintSync(cwd);
1527
+ if (fp) fingerprints.git = fp;
5077
1528
  }
5078
- try {
5079
- fs11.unlinkSync(scriptPath);
5080
- removedAny = true;
5081
- } catch {
1529
+ if (cwd && isDirListingCommand(command)) {
1530
+ const target = extractFirstPathArg(command, cwd, cwd);
1531
+ if (target) {
1532
+ const fp = dirStateFingerprintSync(target);
1533
+ if (fp) fingerprints.dir = fp;
1534
+ }
5082
1535
  }
5083
- removeCreatedBackups(configPath);
5084
- return removedAny;
5085
- }
5086
- function isGrokInstalled() {
5087
- return fs11.existsSync(grokConfigPath()) && fs11.existsSync(grokHookScriptPath());
5088
- }
5089
-
5090
- // src/bridges/vscode_install.ts
5091
- init_define_import_meta_env();
5092
- import * as fs12 from "node:fs";
5093
- import * as os8 from "node:os";
5094
- import * as path11 from "node:path";
5095
- var VSCODE_GUIDANCE_BEGIN = "<!-- token-goat-vscode-begin -->";
5096
- var VSCODE_GUIDANCE_END = "<!-- token-goat-vscode-end -->";
5097
- var BEGIN = VSCODE_GUIDANCE_BEGIN;
5098
- var END = VSCODE_GUIDANCE_END;
5099
- function vscodeUserConfigDir() {
5100
- if (process.platform === "win32") {
5101
- const appData = process.env["APPDATA"];
5102
- const base = appData !== void 0 && appData.trim() !== "" ? appData : path11.join(os8.homedir(), "AppData", "Roaming");
5103
- return path11.join(base, "Code", "User");
5104
- }
5105
- if (process.platform === "darwin") {
5106
- return path11.join(os8.homedir(), "Library", "Application Support", "Code", "User");
5107
- }
5108
- return path11.join(os8.homedir(), ".config", "Code", "User");
5109
- }
5110
- function vscodeUserMcpPath() {
5111
- return path11.join(vscodeUserConfigDir(), "mcp.json");
5112
- }
5113
- function vscodeProjectMcpPath(projectRoot = process.cwd()) {
5114
- return path11.join(path11.resolve(projectRoot), ".vscode", "mcp.json");
5115
- }
5116
- function vscodeMcpPath(opts = {}) {
5117
- return opts.project === true ? vscodeProjectMcpPath(opts.projectRoot) : vscodeUserMcpPath();
5118
- }
5119
- function otherScopeMcpPath2(opts) {
5120
- return opts.project === true ? vscodeUserMcpPath() : vscodeProjectMcpPath(opts.projectRoot);
5121
- }
5122
- function vscodeInstructionsPath(opts = {}) {
5123
- return opts.project === true ? path11.join(path11.resolve(opts.projectRoot ?? process.cwd()), ".github", "copilot-instructions.md") : path11.join(os8.homedir(), ".copilot", "instructions", "token-goat.instructions.md");
5124
- }
5125
- var USER_INSTRUCTIONS_FRONTMATTER = "---\ndescription: 'token-goat: when to use its MCP tools and CLI instead of reading whole files'\napplyTo: '**'\n---\n";
5126
- function vscodeHooksDir(opts = {}) {
5127
- return opts.project === true ? path11.join(path11.resolve(opts.projectRoot ?? process.cwd()), ".github", "hooks") : path11.join(os8.homedir(), ".copilot", "hooks");
5128
- }
5129
- function vscodeScopeFromFlags(flags) {
5130
- return { project: flags.user !== true };
5131
- }
5132
- function vscodeHooksInstalled(opts = {}) {
5133
- return readCopilotHooksOwners(vscodeHooksDir(opts)).has("vscode");
5134
- }
5135
- function vscodeUserSettingsPath() {
5136
- return path11.join(vscodeUserConfigDir(), "settings.json");
5137
- }
5138
- function vscodeUsesClaudeHooks(settingsPath2 = vscodeUserSettingsPath()) {
5139
- let text;
5140
- try {
5141
- text = fs12.readFileSync(settingsPath2, "utf8");
5142
- } catch {
5143
- return false;
1536
+ if (isDepListCommand(command) || cwd && (isNpmInstallCommand(command) || isNpmAuditCommand(command) || isNpmOutdatedCommand(command))) {
1537
+ const fp = depLockfileFingerprintSync(command, cwd);
1538
+ if (fp) fingerprints.lockfile = fp;
1539
+ }
1540
+ if (cwd && isCatCommand(command)) {
1541
+ const target = extractFirstPathArg(command, cwd, null);
1542
+ if (target) {
1543
+ const fp = fileStateFingerprintSync(target);
1544
+ if (fp) fingerprints.file = fp;
1545
+ }
5144
1546
  }
5145
- const parsed = jsonc().parse(text, [], { allowTrailingComma: true, disallowComments: false });
5146
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return false;
5147
- return parsed["chat.useClaudeHooks"] === true;
1547
+ return Object.keys(fingerprints).length > 0 ? fingerprints : void 0;
5148
1548
  }
5149
- function readConfig(filePath) {
5150
- return readServersJson(filePath, "VS Code");
1549
+ function isBashEntryStale(entry, command, cwd) {
1550
+ const stored = entry.fingerprints;
1551
+ if (!stored) return false;
1552
+ const current = computeBashFingerprints(command, cwd);
1553
+ if (stored.git !== void 0 && stored.git !== current?.git) return true;
1554
+ if (stored.dir !== void 0 && stored.dir !== current?.dir) return true;
1555
+ if (stored.lockfile !== void 0 && stored.lockfile !== current?.lockfile) return true;
1556
+ if (stored.file !== void 0 && stored.file !== current?.file) return true;
1557
+ return false;
5151
1558
  }
5152
- var updateConfig = setTokenGoatServer;
5153
- function otherScopeHasManagedServer(opts) {
5154
- const otherPath = otherScopeMcpPath2(opts);
5155
- if (opts.project !== true && !projectPathIsConsultable(otherPath, path11.resolve(opts.projectRoot ?? process.cwd()))) return false;
5156
- if (!fs12.existsSync(otherPath)) return false;
5157
- try {
5158
- const config = readConfig(otherPath);
5159
- const servers = config.value["servers"];
5160
- if (servers === null || typeof servers !== "object" || Array.isArray(servers)) return false;
5161
- return isManagedServer(servers["token-goat"]);
5162
- } catch {
5163
- return false;
1559
+ function tokenizeShellArgs(cmd) {
1560
+ const trimmed = cmd.trim();
1561
+ const tokens = [];
1562
+ let current = "";
1563
+ let quoteChar = null;
1564
+ let hasToken = false;
1565
+ for (let i = 0; i < trimmed.length; i++) {
1566
+ const ch = trimmed[i];
1567
+ if (quoteChar !== null) {
1568
+ if (ch === quoteChar) {
1569
+ quoteChar = null;
1570
+ } else {
1571
+ current += ch;
1572
+ }
1573
+ continue;
1574
+ }
1575
+ if (ch === '"' || ch === "'") {
1576
+ quoteChar = ch;
1577
+ hasToken = true;
1578
+ continue;
1579
+ }
1580
+ if (/\s/.test(ch)) {
1581
+ if (hasToken) {
1582
+ tokens.push(current);
1583
+ current = "";
1584
+ hasToken = false;
1585
+ }
1586
+ continue;
1587
+ }
1588
+ current += ch;
1589
+ hasToken = true;
5164
1590
  }
1591
+ if (hasToken) tokens.push(current);
1592
+ return tokens;
5165
1593
  }
5166
- function vscodeDecoderConfigured(opts = {}) {
5167
- const checkedPaths = [vscodeUserMcpPath()];
5168
- if (opts.projectRoot !== void 0) {
5169
- const projectPath = vscodeProjectMcpPath(opts.projectRoot);
5170
- if (projectPathIsConsultable(projectPath, path11.resolve(opts.projectRoot))) checkedPaths.push(projectPath);
5171
- }
5172
- for (const candidate of checkedPaths) {
5173
- if (!fs12.existsSync(candidate)) continue;
5174
- try {
5175
- const config = readConfig(candidate);
5176
- const servers = config.value["servers"];
5177
- if (servers === null || typeof servers !== "object" || Array.isArray(servers)) continue;
5178
- if (isManagedServer(servers["token-goat"])) {
5179
- return { configured: true, checkedPaths };
1594
+ function extractFirstPathArg(cmd, cwd, fallback) {
1595
+ const tokens = tokenizeShellArgs(cmd);
1596
+ for (let i = 1; i < tokens.length; i++) {
1597
+ const token = tokens[i];
1598
+ if (!token.startsWith("-")) {
1599
+ if (!token.startsWith("/")) {
1600
+ return resolve(cwd, token);
5180
1601
  }
5181
- } catch {
1602
+ return token;
5182
1603
  }
5183
1604
  }
5184
- return { configured: false, checkedPaths };
5185
- }
5186
- function writeGuidance2(filePath, userScope) {
5187
- let created = false;
5188
- if (userScope && !fs12.existsSync(filePath)) {
5189
- ensureDirSync(path11.dirname(filePath));
5190
- atomicWriteText(filePath, USER_INSTRUCTIONS_FRONTMATTER);
5191
- created = true;
5192
- }
5193
- const body = [
5194
- BEGIN,
5195
- buildGuidanceBody("VS Code\u2019s supported MCP integration and its built-in file-read tools", { gdrive: loadConfig().gdrive.enabled }),
5196
- "",
5197
- "**Compressed payloads:** a message containing a token-goat payload block (recognizable by a `recovery: token-goat retrieve <id>` line) is compressed text, not an answer. Call the MCP tool `retrieve_text` with that id to recover the original text, then answer the question the message asks using the recovered text. Never present the raw payload to the user as the response; if the `retrieve_text` tool is unavailable (the MCP server is not running, or the chat is not in Agent mode), say so plainly and ask the user to switch to Agent mode or run `token-goat install --vscode`.",
5198
- "",
5199
- "VS Code support: token-goat install --vscode configures a stdio MCP server under the servers root key in your user-profile mcp.json by default (add --project for the workspace .vscode/mcp.json instead), and agent hooks that see VS Code\u2019s built-in tool calls. The hooks can deny a repeated read, add a hint, and shrink an image before view_image loads it; they cannot fold or trim what a built-in read returns, and they leave terminal commands unchanged.",
5200
- END
5201
- ].join("\n");
5202
- return upsertDelimitedBlock(filePath, BEGIN, END, body) || created;
1605
+ return fallback;
5203
1606
  }
5204
- function assertProjectTargetsAreInTheProject(opts) {
5205
- if (opts.project !== true) return;
5206
- const root = path11.resolve(opts.projectRoot ?? process.cwd());
5207
- for (const target of [vscodeMcpPath(opts), vscodeInstructionsPath(opts), ...copilotHooksFilePaths(vscodeHooksDir(opts))]) {
5208
- assertProjectScopeTarget(target, root);
1607
+ var ISSUE_LINE_PATTERN = /\b(?:error|fail(?:ed|ure)?|warning)\b/i;
1608
+ function summarizeOutputDelta(oldOutput, newOutput) {
1609
+ if (oldOutput === newOutput) return null;
1610
+ const oldLines = oldOutput.split("\n");
1611
+ const newLines = newOutput.split("\n");
1612
+ const oldIssueLines = oldLines.filter((l) => ISSUE_LINE_PATTERN.test(l));
1613
+ if (oldIssueLines.length > 0) {
1614
+ const newIssueLines = newLines.filter((l) => ISSUE_LINE_PATTERN.test(l));
1615
+ const priorTotal = oldIssueLines.length;
1616
+ const availableCounts = /* @__PURE__ */ new Map();
1617
+ for (const l of newIssueLines) availableCounts.set(l, (availableCounts.get(l) ?? 0) + 1);
1618
+ let resolved = 0;
1619
+ for (const l of oldIssueLines) {
1620
+ const avail = availableCounts.get(l) ?? 0;
1621
+ if (avail > 0) {
1622
+ availableCounts.set(l, avail - 1);
1623
+ } else {
1624
+ resolved++;
1625
+ }
1626
+ }
1627
+ const remaining = newIssueLines.length;
1628
+ return `[token-goat: delta] ${resolved} of ${priorTotal} prior issues resolved; remaining: ${remaining}`;
5209
1629
  }
1630
+ return `[token-goat: delta] output changed: ${oldLines.length} -> ${newLines.length} lines`;
1631
+ }
1632
+ function storeBashOutputSync(command, output, exitCode, cwd = null) {
1633
+ const id = commandHashSync(command, cwd);
1634
+ const fingerprints = computeBashFingerprints(command, cwd);
1635
+ const redactedOutput = redactSecrets(output).text;
1636
+ const redactedCommand = redactSecrets(command).text;
1637
+ const entry = {
1638
+ id,
1639
+ command: redactedCommand,
1640
+ output: redactedOutput,
1641
+ exitCode,
1642
+ storedAt: Date.now(),
1643
+ sizeBytes: Buffer.byteLength(redactedOutput, "utf-8"),
1644
+ ...fingerprints ? { fingerprints } : {}
1645
+ };
1646
+ _byId.set(id, entry);
1647
+ storeBlob(BASH_OUTPUT_SUBDIR, id, entry);
1648
+ indexRecallEntry("bash", id, redactedCommand, `${redactedCommand}
1649
+ ${redactedOutput}`, entry.storedAt);
1650
+ return id;
5210
1651
  }
5211
- function installVscode(opts = {}) {
5212
- return withInstallScope(projectScopeRoot(opts), () => installVscodeScoped(opts));
1652
+ function storeBashOutput(command, output, exitCode, cwd = null) {
1653
+ return Promise.resolve(storeBashOutputSync(command, output, exitCode, cwd));
5213
1654
  }
5214
- function installVscodeScoped(opts) {
5215
- assertProjectTargetsAreInTheProject(opts);
5216
- const scope = opts.project === true ? "project" : "user";
5217
- const mcpPath = vscodeMcpPath(opts);
5218
- const instructionsPath = vscodeInstructionsPath(opts);
5219
- let migratedFromUserScope = false;
5220
- if (scope === "project" && (otherScopeHasManagedServer(opts) || vscodeHooksInstalled())) {
5221
- migratedFromUserScope = uninstallVscode({ keepBackups: true });
5222
- } else if (otherScopeHasManagedServer(opts)) {
5223
- const otherPath = otherScopeMcpPath2(opts);
5224
- throw new Error(
5225
- `token-goat is already registered in VS Code project scope (${otherPath}). Installing into user scope too would register it twice and duplicate its tool schemas in this workspace. Run "token-goat uninstall --vscode --project" first if you want to move it, or drop --vscode from this run.`
5226
- );
1655
+ function coerceBashEntry(raw) {
1656
+ if (raw === null || typeof raw !== "object") return null;
1657
+ const o = raw;
1658
+ if (typeof o["id"] !== "string" || typeof o["command"] !== "string" || typeof o["output"] !== "string" || typeof o["exitCode"] !== "number" || typeof o["storedAt"] !== "number" || typeof o["sizeBytes"] !== "number") {
1659
+ return null;
5227
1660
  }
5228
- const config = readConfig(mcpPath);
5229
- const existingServers = config.value["servers"];
5230
- if (existingServers !== void 0 && (existingServers === null || typeof existingServers !== "object" || Array.isArray(existingServers))) {
5231
- throw new Error(`malformed VS Code MCP JSON at ${mcpPath}: servers must be an object`);
5232
- }
5233
- const servers = existingServers ?? {};
5234
- const current = servers["token-goat"];
5235
- if (current !== void 0 && !isManagedServer(current)) {
5236
- throw new Error(`VS Code MCP JSON at ${mcpPath} already has a non-token-goat-managed server named "token-goat"`);
5237
- }
5238
- const next = updateConfig(config.text, managedServer());
5239
- const mcpExisted = fs12.existsSync(mcpPath);
5240
- ensureDirSync(path11.dirname(mcpPath));
5241
- if (config.text !== next) {
5242
- backupFile(mcpPath);
5243
- atomicWriteText(mcpPath, next);
5244
- if (!mcpExisted) recordCreatedConfig(mcpPath);
5245
- }
5246
- const guidanceChanged = writeGuidance2(instructionsPath, scope === "user");
5247
- if (scope === "project") syncVisualStudioProjectGuidance(instructionsPath);
5248
- const hooks = installCopilotHooksFile(vscodeHooksDir(opts), "vscode");
5249
- return {
5250
- mcpPath,
5251
- instructionsPath,
5252
- hooksConfigPath: hooks.configPath,
5253
- alreadyInstalled: config.text === next && !guidanceChanged && !hooks.changed && !migratedFromUserScope,
5254
- scope,
5255
- migratedFromUserScope
1661
+ const entry = {
1662
+ id: o["id"],
1663
+ command: o["command"],
1664
+ output: o["output"],
1665
+ exitCode: o["exitCode"],
1666
+ storedAt: o["storedAt"],
1667
+ sizeBytes: o["sizeBytes"]
5256
1668
  };
1669
+ const rawFingerprints = o["fingerprints"];
1670
+ if (rawFingerprints !== null && typeof rawFingerprints === "object") {
1671
+ const f = rawFingerprints;
1672
+ const fingerprints = {};
1673
+ if (typeof f["git"] === "string") fingerprints.git = f["git"];
1674
+ if (typeof f["dir"] === "string") fingerprints.dir = f["dir"];
1675
+ if (typeof f["lockfile"] === "string") fingerprints.lockfile = f["lockfile"];
1676
+ if (typeof f["file"] === "string") fingerprints.file = f["file"];
1677
+ if (Object.keys(fingerprints).length > 0) return { ...entry, fingerprints };
1678
+ }
1679
+ return entry;
5257
1680
  }
5258
- function uninstallVscode(opts = {}) {
5259
- return withInstallScope(projectScopeRoot(opts), () => uninstallVscodeScoped(opts));
5260
- }
5261
- function uninstallVscodeScoped(opts) {
5262
- assertProjectTargetsAreInTheProject(opts);
5263
- const mcpPath = vscodeMcpPath(opts);
5264
- let removed = false;
5265
- if (fs12.existsSync(mcpPath)) {
5266
- const config = readConfig(mcpPath);
5267
- const servers = config.value["servers"];
5268
- if (servers !== void 0 && (servers === null || typeof servers !== "object" || Array.isArray(servers))) {
5269
- throw new Error(`malformed VS Code MCP JSON at ${mcpPath}: servers must be an object`);
5270
- }
5271
- if (servers && isManagedServer(servers["token-goat"])) {
5272
- const next = dropEmptyServers(updateConfig(config.text, void 0));
5273
- if (/^\s*\{\s*\}\s*$/.test(next) && takeCreatedConfig(mcpPath)) removeFileInScope(mcpPath);
5274
- else {
5275
- backupFile(mcpPath);
5276
- atomicWriteText(mcpPath, next);
5277
- }
5278
- if (opts.keepBackups !== true) removeCreatedBackups(mcpPath);
5279
- removed = true;
5280
- }
5281
- }
5282
- const instructionsPath = vscodeInstructionsPath(opts);
5283
- if (stripDelimitedBlock(instructionsPath, BEGIN, END, opts.keepBackups === true)) {
5284
- removed = true;
5285
- if (opts.project !== true && fs12.readFileSync(instructionsPath, "utf8").trim() === USER_INSTRUCTIONS_FRONTMATTER.trim()) removeFileInScope(instructionsPath);
5286
- }
5287
- if (opts.project === true) syncVisualStudioProjectGuidance(instructionsPath);
5288
- if (releaseCopilotHooksFile(vscodeHooksDir(opts), "vscode", opts.keepBackups === true)) removed = true;
5289
- return removed;
1681
+ function getBashOutput(id) {
1682
+ const hit = _byId.get(id);
1683
+ if (hit !== void 0) return hit;
1684
+ if (isBlobStale(BASH_OUTPUT_SUBDIR, id)) return null;
1685
+ const entry = coerceBashEntry(loadBlob(BASH_OUTPUT_SUBDIR, id));
1686
+ if (entry === null) return null;
1687
+ _byId.set(id, entry);
1688
+ return entry;
5290
1689
  }
5291
-
5292
- // src/bridges/index.ts
5293
- init_define_import_meta_env();
1690
+ registerReset(() => {
1691
+ _byId = /* @__PURE__ */ new Map();
1692
+ });
5294
1693
 
5295
1694
  // src/tool_filters/base.ts
5296
1695
  init_define_import_meta_env();
@@ -5513,7 +1912,7 @@ ${stderr.replace(/\s+$/, "")}`;
5513
1912
  notes.push(`input exceeded inspect budget (${Math.floor(MAX_INSPECT_BYTES / 1024)} KiB); fell back to truncation`);
5514
1913
  body = fallbackTruncate(normOut, normErr, maxLines);
5515
1914
  } else {
5516
- body = this.compress(normOut, normErr, exitCode, argv, { inputTruncated: soClamped !== null || seClamped !== null });
1915
+ body = this.compress(normOut, normErr, exitCode, argv, { inputTruncated: soClamped !== null || seClamped !== null, maxLines });
5517
1916
  }
5518
1917
  } catch (exc) {
5519
1918
  const kind = exc instanceof Error ? exc.constructor.name : "Error";
@@ -10999,16 +7398,16 @@ function _scoreAndCapHunks(hunkLines, maxHunks) {
10999
7398
  var DiffFilter = class extends ToolFilter {
11000
7399
  name = "diff";
11001
7400
  binaries = /* @__PURE__ */ new Set(["diff", "diff3", "sdiff", "colordiff", "wdiff"]);
11002
- compressBody(stdout, stderr, _exitCode, _argv) {
7401
+ compressBody(stdout, stderr, _exitCode, _argv, ctx = {}) {
11003
7402
  const text = this.combineOutput(stdout, stderr);
11004
7403
  const lines = text.split("\n");
11005
7404
  const nonEmpty = lines.filter((l) => l);
11006
7405
  if (nonEmpty.length <= 50) return text;
11007
7406
  const hasUnified = lines.slice(0, 20).some((l) => _DIFF_HUNK_RE.test(l));
11008
- if (hasUnified) return this._compressUnified(lines);
11009
- return compressTestOutput(lines, 300);
7407
+ if (hasUnified) return this._compressUnified(lines, ctx.maxLines);
7408
+ return compressTestOutput(lines, ctx.maxLines === void 0 ? 300 : Math.min(300, ctx.maxLines) - 1);
11010
7409
  }
11011
- _compressUnified(lines) {
7410
+ _compressUnified(lines, maxLines) {
11012
7411
  const text = lines.join("\n");
11013
7412
  const rawBlocks = _mergeDiffEchoBlocks(_splitDiffFileBlocks(text));
11014
7413
  const realFiles = rawBlocks.filter((b) => _DIFF_FILE_HEADER_RE.test(b.split("\n")[0] ?? ""));
@@ -11062,9 +7461,48 @@ var DiffFilter = class extends ToolFilter {
11062
7461
  const capped = _scoreAndCapHunks(blockLines, maxHunksPerFile);
11063
7462
  outParts.push(capped.join("\n"));
11064
7463
  }
7464
+ if (maxLines !== void 0 && outParts.join("\n").split("\n").length > maxLines) {
7465
+ return _collapsePlainDiffBlocksToCap(outParts, maxLines).join("\n");
7466
+ }
11065
7467
  return outParts.join("\n");
11066
7468
  }
11067
7469
  };
7470
+ function _collapsePlainDiffBlocksToCap(outBlocks, maxLines) {
7471
+ const isFileBlock = outBlocks.map((block) => _DIFF_FILE_HEADER_RE.test(block.split("\n")[0] ?? ""));
7472
+ const collapsedFormOf = (block) => {
7473
+ const blockLines = block.split("\n");
7474
+ const hunkIdx = blockLines.findIndex((ln) => _DIFF_HUNK_RE.test(ln));
7475
+ const headerLines = hunkIdx === -1 ? blockLines : blockLines.slice(0, hunkIdx);
7476
+ const hunkCount = blockLines.filter((ln) => _DIFF_HUNK_RE.test(ln)).length;
7477
+ const added = blockLines.filter(_isDiffAdd).length;
7478
+ const removed = blockLines.filter(_isDiffRemove).length;
7479
+ const summary = `[token-goat: ${hunkCount} hunk(s), +${added} -${removed} lines collapsed to fit the line cap]`;
7480
+ return { headerLines, summary, size: headerLines.length + 1 };
7481
+ };
7482
+ const collapsedSizes = outBlocks.map((block, i) => isFileBlock[i] ? collapsedFormOf(block).size : 0);
7483
+ const reserve = new Array(outBlocks.length).fill(0);
7484
+ for (let i = outBlocks.length - 2; i >= 0; i--) reserve[i] = reserve[i + 1] + collapsedSizes[i + 1];
7485
+ let budget = maxLines;
7486
+ const result = [];
7487
+ for (let i = 0; i < outBlocks.length; i++) {
7488
+ const block = outBlocks[i];
7489
+ if (!isFileBlock[i]) {
7490
+ result.push(block);
7491
+ budget -= block.split("\n").length;
7492
+ continue;
7493
+ }
7494
+ const lineCount = block.split("\n").length;
7495
+ if (lineCount <= budget - reserve[i]) {
7496
+ result.push(block);
7497
+ budget -= lineCount;
7498
+ } else {
7499
+ const { headerLines, summary, size } = collapsedFormOf(block);
7500
+ result.push(headerLines.join("\n") + "\n" + summary);
7501
+ budget -= size;
7502
+ }
7503
+ }
7504
+ return result;
7505
+ }
11068
7506
  var _FFMPEG_VERSION_RE = /^ff(?:mpeg|probe|play)\s+version\s/i;
11069
7507
  var _FFMPEG_BUILD_NOISE_RE = /^\s+(?:built with\b|configuration:|lib(?:av|sw|post)\w+\s+\d)/;
11070
7508
  var _FFMPEG_METADATA_SECTION_RE = /^\s{2,}Metadata:\s*$/;
@@ -14813,42 +11251,41 @@ var _GIT_LOG_ONELINE_GRAPH_RE = /^[|\\/* ]*[0-9a-f]{7,}\s/;
14813
11251
  var _GIT_LOG_MERGE_RE = /^Merge:/;
14814
11252
  var _GIT_LOG_AUTHOR_RE = /^Author:\s+(.+)/;
14815
11253
  var _GIT_LOG_DATE_RE = /^Date:\s+(.+)/;
11254
+ function _summariseCommitHeader(lines) {
11255
+ const hashLine = lines[0] ?? "";
11256
+ const merge = lines.find((ln) => _GIT_LOG_MERGE_RE.test(ln)) ?? "";
11257
+ let author = "";
11258
+ let dateStr = "";
11259
+ let subject = "";
11260
+ for (const ln of lines) {
11261
+ if (!author) {
11262
+ const m = _GIT_LOG_AUTHOR_RE.exec(ln);
11263
+ if (m) author = (m[1] ?? "").split("<")[0].trim();
11264
+ }
11265
+ if (!dateStr) {
11266
+ const m = _GIT_LOG_DATE_RE.exec(ln);
11267
+ if (m) dateStr = (m[1] ?? "").trim();
11268
+ }
11269
+ if (!subject && ln.startsWith(" ") && ln.trim()) {
11270
+ subject = ln.trim();
11271
+ }
11272
+ }
11273
+ const parts = [hashLine];
11274
+ if (merge) parts.push(merge);
11275
+ const detailParts = [];
11276
+ if (author) detailParts.push(author);
11277
+ if (dateStr) detailParts.push(dateStr);
11278
+ if (subject) detailParts.push(`"${subject}"`);
11279
+ if (detailParts.length) parts.push(" " + detailParts.join(" | "));
11280
+ return parts;
11281
+ }
14816
11282
  function _compressGitLogFull(stdout, stderr) {
14817
11283
  const blocks = splitBlocks(stdout, _GIT_LOG_COMMIT_RE);
14818
11284
  if (!blocks.length) return stdout;
14819
11285
  const prelude = !_GIT_LOG_COMMIT_RE.test(blocks[0]) ? blocks[0] : "";
14820
11286
  const commits = blocks.filter((b) => _GIT_LOG_COMMIT_RE.test(b));
14821
11287
  if (commits.length <= 10) return stdout;
14822
- const collapsed = [];
14823
- for (const block of commits) {
14824
- const lines = block.split("\n");
14825
- const hashLine = lines[0] ?? "";
14826
- const merge = lines.find((ln) => _GIT_LOG_MERGE_RE.test(ln)) ?? "";
14827
- let author = "";
14828
- let dateStr = "";
14829
- let subject = "";
14830
- for (const ln of lines) {
14831
- if (!author) {
14832
- const m = _GIT_LOG_AUTHOR_RE.exec(ln);
14833
- if (m) author = (m[1] ?? "").split("<")[0].trim();
14834
- }
14835
- if (!dateStr) {
14836
- const m = _GIT_LOG_DATE_RE.exec(ln);
14837
- if (m) dateStr = (m[1] ?? "").trim();
14838
- }
14839
- if (!subject && ln.startsWith(" ") && ln.trim()) {
14840
- subject = ln.trim();
14841
- }
14842
- }
14843
- const parts = [hashLine];
14844
- if (merge) parts.push(merge);
14845
- const detailParts = [];
14846
- if (author) detailParts.push(author);
14847
- if (dateStr) detailParts.push(dateStr);
14848
- if (subject) detailParts.push(`"${subject}"`);
14849
- if (detailParts.length) parts.push(" " + detailParts.join(" | "));
14850
- collapsed.push(parts.join("\n"));
14851
- }
11288
+ const collapsed = commits.map((block) => _summariseCommitHeader(block.split("\n")).join("\n"));
14852
11289
  let text = (prelude ? prelude + "\n" : "") + collapsed.join("\n\n");
14853
11290
  if (stderr.trim()) text += "\n---\n" + stderr.replace(/\s+$/, "");
14854
11291
  return text;
@@ -14871,19 +11308,31 @@ function _capPatchLinesInBlock(block, maxLines) {
14871
11308
  }
14872
11309
  return [...headerLines, ...diffLines].join("\n");
14873
11310
  }
14874
- function _compressGitLogCapped(stdout, stderr, capBlock) {
11311
+ var _GIT_LOG_MESSAGE_LINE_RE = /^ {4}/;
11312
+ function _collapseCommitBody(block) {
11313
+ const lines = block.split("\n");
11314
+ const header = _summariseCommitHeader(lines);
11315
+ const isHeaderLine = (ln) => _GIT_LOG_COMMIT_RE.test(ln) || _GIT_LOG_MERGE_RE.test(ln) || _GIT_LOG_AUTHOR_RE.test(ln) || _GIT_LOG_DATE_RE.test(ln);
11316
+ const rest = lines.filter((ln) => ln.trim() && !_GIT_LOG_MESSAGE_LINE_RE.test(ln) && !isHeaderLine(ln));
11317
+ return [...header, ...rest].join("\n");
11318
+ }
11319
+ function _compressGitLogCapped(stdout, stderr, capBlock, maxLines) {
14875
11320
  const blocks = splitBlocks(stdout, _GIT_LOG_COMMIT_RE);
14876
11321
  if (!blocks.length) return stdout;
14877
11322
  const prelude = !_GIT_LOG_COMMIT_RE.test(blocks[0]) ? blocks[0] : "";
14878
11323
  const commits = blocks.filter((b) => _GIT_LOG_COMMIT_RE.test(b));
14879
- const outBlocks = commits.map(capBlock);
11324
+ let outBlocks = commits.map(capBlock);
11325
+ if (maxLines !== void 0) {
11326
+ const lineCount = ((prelude ? prelude + "\n" : "") + outBlocks.join("\n")).split("\n").length;
11327
+ if (lineCount > maxLines) outBlocks = outBlocks.map(_collapseCommitBody);
11328
+ }
14880
11329
  let text = (prelude ? prelude + "\n" : "") + outBlocks.join("\n");
14881
11330
  if (stderr.trim()) text += "\n---\n" + stderr.replace(/\s+$/, "");
14882
11331
  return text;
14883
11332
  }
14884
- function _compressGitLogPatch(stdout, stderr) {
11333
+ function _compressGitLogPatch(stdout, stderr, maxLines) {
14885
11334
  const MAX_PATCH_LINES = 30;
14886
- return _compressGitLogCapped(stdout, stderr, (block) => _capPatchLinesInBlock(block, MAX_PATCH_LINES));
11335
+ return _compressGitLogCapped(stdout, stderr, (block) => _capPatchLinesInBlock(block, MAX_PATCH_LINES), maxLines);
14887
11336
  }
14888
11337
  function _capStatLinesInBlock(block, maxFiles) {
14889
11338
  const lines = block.split("\n");
@@ -14908,11 +11357,11 @@ function _capStatLinesInBlock(block, maxFiles) {
14908
11357
  }
14909
11358
  return newLines.join("\n");
14910
11359
  }
14911
- function _compressGitLogStat(stdout, stderr) {
11360
+ function _compressGitLogStat(stdout, stderr, maxLines) {
14912
11361
  const MAX_STAT_FILES = 20;
14913
- return _compressGitLogCapped(stdout, stderr, (block) => _capStatLinesInBlock(block, MAX_STAT_FILES));
11362
+ return _compressGitLogCapped(stdout, stderr, (block) => _capStatLinesInBlock(block, MAX_STAT_FILES), maxLines);
14914
11363
  }
14915
- function _compressGitLogEnhanced(stdout, stderr, argv, inputTruncated = false) {
11364
+ function _compressGitLogEnhanced(stdout, stderr, argv, inputTruncated = false, maxLines) {
14916
11365
  const flags = new Set(argv);
14917
11366
  let isOneline = flags.has("--oneline") || flags.has("--format=oneline") || flags.has("--pretty=oneline") || argv.some((a) => a.startsWith("--format=%h") || a.startsWith("--pretty=%h"));
14918
11367
  if (!isOneline) {
@@ -14922,7 +11371,7 @@ function _compressGitLogEnhanced(stdout, stderr, argv, inputTruncated = false) {
14922
11371
  }
14923
11372
  }
14924
11373
  const isPatch = flags.has("-p") || flags.has("--patch") || flags.has("-u");
14925
- const isStat = flags.has("--stat") || flags.has("--shortstat") || flags.has("--name-status");
11374
+ const isStat = flags.has("--stat") || argv.some((a) => a.startsWith("--stat=")) || flags.has("--shortstat") || flags.has("--name-status") || flags.has("--name-only") || flags.has("--numstat");
14926
11375
  if (isOneline) {
14927
11376
  const ONELINE_CAP = 50;
14928
11377
  let blocks;
@@ -14945,15 +11394,15 @@ function _compressGitLogEnhanced(stdout, stderr, argv, inputTruncated = false) {
14945
11394
  if (stderr.trim()) out += "\n---\n" + stderr.replace(/\s+$/, "");
14946
11395
  return out;
14947
11396
  }
14948
- if (isPatch) return _compressGitLogPatch(stdout, stderr);
14949
- if (isStat) return _compressGitLogStat(stdout, stderr);
11397
+ if (isPatch) return _compressGitLogPatch(stdout, stderr, maxLines);
11398
+ if (isStat) return _compressGitLogStat(stdout, stderr, maxLines);
14950
11399
  return _compressGitLogFull(stdout, stderr);
14951
11400
  }
14952
11401
  var GitLogFilter = class extends GitBaseFilter {
14953
11402
  name = "git-log";
14954
11403
  subcommands = /* @__PURE__ */ new Set(["log"]);
14955
11404
  compress(stdout, stderr, _exitCode, argv, ctx = {}) {
14956
- return _compressGitLogEnhanced(stdout, stderr, argv, ctx.inputTruncated === true);
11405
+ return _compressGitLogEnhanced(stdout, stderr, argv, ctx.inputTruncated === true, ctx.maxLines);
14957
11406
  }
14958
11407
  };
14959
11408
  var _GIT_DIFF_BINARY_RE = /^Binary files?(?: .+)? differ$/;
@@ -15116,7 +11565,43 @@ function _capHunksByDensity(hunks, maxHunksPerFile) {
15116
11565
  note: `[token-goat: ${dropped.length} more hunks, avg density ${avg.toFixed(2)} \u2014 likely whitespace/formatting]`
15117
11566
  };
15118
11567
  }
15119
- function _compressGitDiffBody(stdout, stderr, maxHunksPerFile = 10) {
11568
+ function _collapseDiffBlocksToCap(outBlocks, maxLines) {
11569
+ const isFileBlock = outBlocks.map((block) => _GIT_DIFF_FILE_RE.test(block));
11570
+ const collapsedFormOf = (block) => {
11571
+ const lines = block.split("\n");
11572
+ const hunkIdx = lines.findIndex((ln) => _GIT_DIFF_HUNK_RE.test(ln));
11573
+ const headerLines = hunkIdx === -1 ? lines : lines.slice(0, hunkIdx);
11574
+ const hunkCount = lines.filter((ln) => _GIT_DIFF_HUNK_RE.test(ln)).length;
11575
+ const added = lines.filter(_isDiffAdd2).length;
11576
+ const removed = lines.filter(_isDiffRemove2).length;
11577
+ const summary = `[token-goat: ${hunkCount} hunk(s), +${added} -${removed} lines collapsed to fit the line cap]`;
11578
+ return { headerLines, summary, size: headerLines.length + 1 };
11579
+ };
11580
+ const collapsedSizes = outBlocks.map((block, i) => isFileBlock[i] ? collapsedFormOf(block).size : 0);
11581
+ const reserve = new Array(outBlocks.length).fill(0);
11582
+ for (let i = outBlocks.length - 2; i >= 0; i--) reserve[i] = reserve[i + 1] + collapsedSizes[i + 1];
11583
+ let budget = maxLines;
11584
+ const result = [];
11585
+ for (let i = 0; i < outBlocks.length; i++) {
11586
+ const block = outBlocks[i];
11587
+ if (!isFileBlock[i]) {
11588
+ result.push(block);
11589
+ budget -= block.split("\n").length;
11590
+ continue;
11591
+ }
11592
+ const lineCount = block.split("\n").length;
11593
+ if (lineCount <= budget - reserve[i]) {
11594
+ result.push(block);
11595
+ budget -= lineCount;
11596
+ } else {
11597
+ const { headerLines, summary, size } = collapsedFormOf(block);
11598
+ result.push(headerLines.join("\n") + "\n" + summary);
11599
+ budget -= size;
11600
+ }
11601
+ }
11602
+ return result;
11603
+ }
11604
+ function _compressGitDiffBody(stdout, stderr, maxHunksPerFile = 10, maxLines) {
15120
11605
  const MAX_HUNK_CHANGED = 25;
15121
11606
  const HUNK_HEAD_KEEP = 15;
15122
11607
  const HUNK_TAIL_KEEP = 5;
@@ -15190,11 +11675,15 @@ function _compressGitDiffBody(stdout, stderr, maxHunksPerFile = 10) {
15190
11675
  if (hunkCapNote) compressedHunks.push(hunkCapNote);
15191
11676
  outBlocks.push(compressedHunks.join("\n"));
15192
11677
  }
15193
- let text = outBlocks.join("\n");
11678
+ let finalBlocks = outBlocks;
11679
+ if (maxLines !== void 0 && outBlocks.join("\n").split("\n").length > maxLines) {
11680
+ finalBlocks = _collapseDiffBlocksToCap(outBlocks, maxLines);
11681
+ }
11682
+ let text = finalBlocks.join("\n");
15194
11683
  if (stderr.trim()) text += "\n---\n" + stderr.replace(/\s+$/, "");
15195
11684
  return text;
15196
11685
  }
15197
- function _compressGitDiffEnhanced(stdout, stderr, argv) {
11686
+ function _compressGitDiffEnhanced(stdout, stderr, argv, maxLines) {
15198
11687
  const flags = new Set(argv);
15199
11688
  const isStat = flags.has("--stat") || flags.has("--shortstat") || flags.has("--name-only");
15200
11689
  if (isStat) return _compressGitDiffStat(stdout, stderr, argv);
@@ -15204,13 +11693,13 @@ function _compressGitDiffEnhanced(stdout, stderr, argv) {
15204
11693
  } catch {
15205
11694
  maxHunksPerFile = void 0;
15206
11695
  }
15207
- return maxHunksPerFile === void 0 ? _compressGitDiffBody(stdout, stderr) : _compressGitDiffBody(stdout, stderr, maxHunksPerFile);
11696
+ return maxHunksPerFile === void 0 ? _compressGitDiffBody(stdout, stderr, void 0, maxLines) : _compressGitDiffBody(stdout, stderr, maxHunksPerFile, maxLines);
15208
11697
  }
15209
11698
  var GitDiffFilter = class extends GitBaseFilter {
15210
11699
  name = "git-diff";
15211
11700
  subcommands = /* @__PURE__ */ new Set(["diff", "show"]);
15212
- compress(stdout, stderr, _exitCode, argv) {
15213
- return _compressGitDiffEnhanced(stdout, stderr, argv);
11701
+ compress(stdout, stderr, _exitCode, argv, ctx = {}) {
11702
+ return _compressGitDiffEnhanced(stdout, stderr, argv, ctx.maxLines);
15214
11703
  }
15215
11704
  };
15216
11705
  var _SHORT_STATUS_RE = /^[MADRCU?! ][MADRCU?! ] /;
@@ -15349,6 +11838,7 @@ function _compressGitBlamePorcelain(lines, stderr) {
15349
11838
  let currentAuthor = null;
15350
11839
  let runCount = 0;
15351
11840
  let blockLines = [];
11841
+ const authorByHash = /* @__PURE__ */ new Map();
15352
11842
  function flushBlock() {
15353
11843
  if (!blockLines.length) return;
15354
11844
  out.push(...blockLines);
@@ -15372,14 +11862,17 @@ function _compressGitBlamePorcelain(lines, stderr) {
15372
11862
  }
15373
11863
  flushBlock();
15374
11864
  currentHash = commitHash;
15375
- currentAuthor = null;
11865
+ currentAuthor = authorByHash.get(commitHash) ?? null;
15376
11866
  runCount = 1;
15377
11867
  blockLines = [line];
15378
11868
  i++;
15379
11869
  while (i < lines.length && !lines[i].startsWith(" ")) {
15380
11870
  const meta = lines[i];
15381
11871
  const am = _GIT_BLAME_AUTHOR_LINE_RE.exec(meta);
15382
- if (am) currentAuthor = (am[1] ?? "").trim();
11872
+ if (am) {
11873
+ currentAuthor = (am[1] ?? "").trim();
11874
+ authorByHash.set(commitHash, currentAuthor);
11875
+ }
15383
11876
  blockLines.push(meta);
15384
11877
  i++;
15385
11878
  }
@@ -15713,11 +12206,11 @@ var GitFilter = class extends GitBaseFilter {
15713
12206
  if (!super.matches(argv)) return false;
15714
12207
  return gitPositionalArgs(argv.slice(1))[0] !== "grep";
15715
12208
  }
15716
- compress(stdout, stderr, exitCode, argv) {
12209
+ compress(stdout, stderr, exitCode, argv, ctx = {}) {
15717
12210
  const positionals = gitPositionalArgs(argv.slice(1));
15718
12211
  const subcommand = positionals[0] ?? "";
15719
12212
  if (subcommand === "diff" || subcommand === "show") {
15720
- return _compressGitDiffEnhanced(stdout, stderr, argv);
12213
+ return _compressGitDiffEnhanced(stdout, stderr, argv, ctx.maxLines);
15721
12214
  }
15722
12215
  if (subcommand === "ls-files" || subcommand === "ls-tree")
15723
12216
  return _truncateListing(stdout, stderr, 100);
@@ -16111,14 +12604,14 @@ function _eslintSingleLineIssuePath(line) {
16111
12604
  var ESLintFilter = class extends ToolFilter {
16112
12605
  name = "eslint";
16113
12606
  binaries = /* @__PURE__ */ new Set(["eslint"]);
16114
- compress(stdout, stderr, exitCode, _argv) {
12607
+ compress(stdout, stderr, exitCode, _argv, ctx = {}) {
16115
12608
  const merged = this.combineOutput(stdout, stderr);
16116
12609
  const lines = merged.split("\n");
16117
12610
  if (exitCode === 0 && !lines.some((ln) => _eslintIssueSeverity(ln) !== null)) {
16118
12611
  const summary = lines.find((ln) => _ESLINT_SUMMARY_RE.test(ln.trim()));
16119
12612
  return summary ?? "ESLint: no errors";
16120
12613
  }
16121
- const out = [];
12614
+ const blocks = [];
16122
12615
  let currentFileHeader = null;
16123
12616
  let currentIssues = [];
16124
12617
  let currentHasIssues = false;
@@ -16128,29 +12621,7 @@ var ESLintFilter = class extends ToolFilter {
16128
12621
  currentHasIssues = false;
16129
12622
  return;
16130
12623
  }
16131
- if (!currentHasIssues) {
16132
- currentFileHeader = null;
16133
- currentIssues = [];
16134
- currentHasIssues = false;
16135
- return;
16136
- }
16137
- out.push(currentFileHeader);
16138
- const warnByRule = /* @__PURE__ */ new Map();
16139
- for (const issue of currentIssues) {
16140
- const severity = _eslintIssueSeverity(issue);
16141
- if (severity === "warning") {
16142
- const rule = issue.trimEnd().split(/\s+/).pop() ?? "__unknown__";
16143
- const bucket = warnByRule.get(rule) ?? [];
16144
- bucket.push(issue);
16145
- warnByRule.set(rule, bucket);
16146
- } else {
16147
- out.push(issue);
16148
- }
16149
- }
16150
- for (const [rule, entries] of [...warnByRule.entries()].sort()) {
16151
- out.push(...entries.slice(0, 3));
16152
- if (entries.length > 3) out.push(` [token-goat: +${entries.length - 3} more ${rule} warnings]`);
16153
- }
12624
+ if (currentHasIssues) blocks.push({ kind: "file", header: currentFileHeader, issues: currentIssues });
16154
12625
  currentFileHeader = null;
16155
12626
  currentIssues = [];
16156
12627
  currentHasIssues = false;
@@ -16158,7 +12629,7 @@ var ESLintFilter = class extends ToolFilter {
16158
12629
  for (const line of lines) {
16159
12630
  if (_ESLINT_SUMMARY_RE.test(line.trim())) {
16160
12631
  flushFile();
16161
- out.push(line);
12632
+ blocks.push({ kind: "line", text: line });
16162
12633
  continue;
16163
12634
  }
16164
12635
  const singleLinePath = _eslintSingleLineIssuePath(line);
@@ -16184,13 +12655,62 @@ var ESLintFilter = class extends ToolFilter {
16184
12655
  continue;
16185
12656
  }
16186
12657
  if (currentFileHeader === null) {
16187
- out.push(line);
12658
+ blocks.push({ kind: "line", text: line });
16188
12659
  } else {
16189
12660
  currentIssues.push(line);
16190
12661
  }
16191
12662
  }
16192
12663
  flushFile();
16193
- return squeezeBlankLines(out.join("\n"));
12664
+ const renderFile = (header, issues, tier) => {
12665
+ if (tier === 2) {
12666
+ const errorsByRule2 = /* @__PURE__ */ new Map();
12667
+ const warningsByRule2 = /* @__PURE__ */ new Map();
12668
+ for (const issue of issues) {
12669
+ const severity = _eslintIssueSeverity(issue);
12670
+ const bucket = severity === "error" ? errorsByRule2 : severity === "warning" ? warningsByRule2 : null;
12671
+ if (bucket !== null) {
12672
+ const rule = issue.trimEnd().split(/\s+/).pop() ?? "__unknown__";
12673
+ bucket.set(rule, (bucket.get(rule) ?? 0) + 1);
12674
+ }
12675
+ }
12676
+ const summarize = (byRule, noun) => {
12677
+ const total = [...byRule.values()].reduce((a, b) => a + b, 0);
12678
+ if (total === 0) return null;
12679
+ return `${total} ${noun}${plural(total)}: ${[...byRule.entries()].sort().map(([rule, n]) => `${rule} \xD7${n}`).join(", ")}`;
12680
+ };
12681
+ const parts = [summarize(errorsByRule2, "error"), summarize(warningsByRule2, "warning")].filter((p) => p !== null);
12682
+ return parts.length > 0 ? [`${header} [token-goat: ${parts.join("; ")}]`] : [header];
12683
+ }
12684
+ const fileOut = [header];
12685
+ const errorsByRule = /* @__PURE__ */ new Map();
12686
+ const warningsByRule = /* @__PURE__ */ new Map();
12687
+ for (const issue of issues) {
12688
+ const severity = _eslintIssueSeverity(issue);
12689
+ const byRule = severity === "warning" ? warningsByRule : tier === 1 && severity === "error" ? errorsByRule : null;
12690
+ if (byRule === null) {
12691
+ fileOut.push(issue);
12692
+ continue;
12693
+ }
12694
+ const rule = issue.trimEnd().split(/\s+/).pop() ?? "__unknown__";
12695
+ const bucket = byRule.get(rule) ?? [];
12696
+ bucket.push(issue);
12697
+ byRule.set(rule, bucket);
12698
+ }
12699
+ for (const [byRule, noun] of [[errorsByRule, "error"], [warningsByRule, "warning"]]) {
12700
+ for (const [rule, entries] of [...byRule.entries()].sort()) {
12701
+ fileOut.push(...entries.slice(0, 3));
12702
+ if (entries.length > 3) fileOut.push(` [token-goat: +${entries.length - 3} more ${rule} ${noun}s]`);
12703
+ }
12704
+ }
12705
+ return fileOut;
12706
+ };
12707
+ const render = (tier) => squeezeBlankLines(blocks.flatMap((block) => block.kind === "line" ? [block.text] : renderFile(block.header, block.issues, tier)).join("\n"));
12708
+ let text = render(0);
12709
+ if (ctx.maxLines !== void 0) {
12710
+ if (text.split("\n").length > ctx.maxLines) text = render(1);
12711
+ if (text.split("\n").length > ctx.maxLines) text = render(2);
12712
+ }
12713
+ return text;
16194
12714
  }
16195
12715
  };
16196
12716
  var _MYPY_LINE_RE = /^(?<file>.+?):(?<line>\d+):(?:\d+:)?\s+(?<level>error|note|warning):/;
@@ -17248,7 +13768,7 @@ var VCPKG_ELAPSED_RE = /^Elapsed\s+time\s+for\s+package\s+\S+:\s+\d/;
17248
13768
  var VCPKG_DONE_RE = /^(?:Total\s+install\s+time:|CMake\s+projects\s+should\s+use|All\s+requested\s+packages\s+are\s+currently\s+installed|Package\s+\S+:\S+\s+is\s+already\s+installed)/i;
17249
13769
  var VCPKG_EXTRACTING_RE = /^\s*--\s+(?:Extracting\s+source|Applying\s+patch|Using\s+cached\s+archive|Downloading\s+https?:\/\/|Fetching\s+\S+|Stored\s+binaries\s+in\s+)/i;
17250
13770
  var NPM_PROGRESS_RE = /^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s|^npm\s+(?:WARN\s+deprecated|sill|http|verb|timing)\s/;
17251
- var NPM_DEPRECATED_GENERAL_RE = /\bdeprecated\b/i;
13771
+ var NPM_DEPRECATED_GENERAL_RE = /^npm\s+warn\s+deprecated\b/i;
17252
13772
  var NPM_AUDIT_PKG_RE = /^(?:npm\s+)?(?:found|run `npm audit`|packages are looking for funding)/i;
17253
13773
  var NPM_ERR_RE = /^npm (?:ERR!|error)/i;
17254
13774
  var DEP_LIST_THRESHOLD = 30;
@@ -18233,6 +14753,7 @@ var PACKAGE_MANAGER_FILTERS = [
18233
14753
  init_define_import_meta_env();
18234
14754
  var XDIST_PREFIX_RE = /^\[gw\d+\]\s*(?:\[\s*\d+%\]\s*)?/;
18235
14755
  var DOTS_RE = /^[.FxXEsS]+\s*(\[\s*\d+%\])?\s*$/;
14756
+ var FILE_DOTS_RE = /^\S+\s+[.FxXEsS]+\s+\[\s*\d+%\]\s*$/;
18236
14757
  var BANNER_RE = /^(?:platform\s|cachedir:\s|rootdir:\s|plugins:\s|configfile:\s|bringing up\s|cacheprovider-)/;
18237
14758
  var PREAMBLE_RE = /^collecting\s/;
18238
14759
  var HEADER_RE = /^=+\s*(?:test session starts|FAILURES|ERRORS|short test summary info|warnings summary|slowest \d+ durations|\d+ failed|\d+ passed|\d+ error)\b/;
@@ -18268,7 +14789,7 @@ var PytestFilter = class extends ToolFilter {
18268
14789
  let pendingWarnHeader = null;
18269
14790
  for (let line of lines) {
18270
14791
  if (XDIST_PREFIX_RE.test(line)) line = line.replace(XDIST_PREFIX_RE, "");
18271
- if (DOTS_RE.test(line)) continue;
14792
+ if (DOTS_RE.test(line) || FILE_DOTS_RE.test(line)) continue;
18272
14793
  if (BANNER_RE.test(line)) continue;
18273
14794
  if (PREAMBLE_RE.test(line)) continue;
18274
14795
  if (HEADER_RE.test(line)) {
@@ -18588,66 +15109,8 @@ function filterByName(name) {
18588
15109
  init_define_import_meta_env();
18589
15110
 
18590
15111
  export {
18591
- getToolName,
18592
- getToolInput,
18593
- getFilePath,
18594
- getCwd,
18595
- getTranscriptPath,
18596
- extractToolResponseField,
18597
- OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
18598
- BODY_FIRST_TOOL_RESPONSE_KEYS,
18599
- isMcpErrorResponse,
18600
- extractToolResultText,
18601
- mcpContentBlocks,
18602
- isTextualContentBlock,
18603
- withPreservedBlocks,
18604
- passOutput,
18605
- denyOutput,
18606
- contextOutput,
18607
- emitRewrite,
18608
- emitRewriteIfChanged,
18609
- makeDedupHintHandlers,
18610
- MATERIALIZE_SHRUNK_IMAGE_JS,
18611
- bundledCliPath,
18612
- readServersJson,
18613
- serversOf,
18614
- setTokenGoatServer,
18615
- dropEmptyServers,
18616
- hasManagedServer,
18617
- visualStudioUserMcpPath,
18618
- visualStudioProjectMcpPath,
18619
- visualStudioOtherScopeHasManagedServer,
18620
- isVisualStudioInstalled,
18621
- visualStudioSolutionVscodeMcpPath,
18622
- visualStudioDuplicateNote,
18623
- visualStudioMcpStatus,
18624
- visualStudioManagedEntry,
18625
- installVisualStudio,
18626
- uninstallVisualStudio,
18627
- copilotCliUserRoot,
18628
- copilotCliMcpToolsDir,
18629
- copilotCliConfigPath,
18630
- copilotCliScriptPath,
18631
- HOOKS_SCRIPT_FILE,
18632
- readCopilotHooksOwners,
18633
- installCopilotCli,
18634
- uninstallCopilotCli,
18635
- isCopilotCliInstalled,
18636
15112
  FILTERS,
18637
- estimateTokensFromLength,
18638
- estimateTokens,
18639
- trimToBudget,
18640
- capJsonRows,
18641
- readCopilotMcpTools,
18642
- GEMINI_TOOL_NAME_MAP,
18643
- VSCODE_TOOL_NAME_KEY,
18644
- normalizePayload,
18645
- materializeShrunkImageFile,
18646
- sessionStateKey,
18647
- registerHook,
18648
- toolMatcherFor,
18649
- runHook,
18650
- serializeOutput,
15113
+ stripAnsiCodes,
18651
15114
  combineStreams,
18652
15115
  hasBareBackgroundOrNewline,
18653
15116
  hasUnquotedOperator,
@@ -18677,33 +15140,6 @@ export {
18677
15140
  storeBashOutputSync,
18678
15141
  storeBashOutput,
18679
15142
  getBashOutput,
18680
- anchoredMarkerPattern,
18681
- settingsPath,
18682
- installHooks,
18683
- uninstallHooks,
18684
- isInstalled,
18685
- installClaudeMd,
18686
- uninstallClaudeMd,
18687
- findStrayClaudeMdBlocks,
18688
- installSkill,
18689
- uninstallSkill,
18690
- groupHasTokenGoat2 as groupHasTokenGoat,
18691
- installCodex,
18692
- uninstallCodex,
18693
- isCodexInstalled,
18694
- installKimi,
18695
- uninstallKimi,
18696
- isKimiInstalled,
18697
- installGrok,
18698
- uninstallGrok,
18699
- isGrokInstalled,
18700
- vscodeScopeFromFlags,
18701
- vscodeHooksInstalled,
18702
- vscodeUsesClaudeHooks,
18703
- otherScopeHasManagedServer,
18704
- vscodeDecoderConfigured,
18705
- installVscode,
18706
- uninstallVscode,
18707
15143
  resolveMinNetSavingsBytes,
18708
15144
  isRewriteWorthwhile,
18709
15145
  compressedTokensSaved,