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.
- package/dist/{token-goat-chunk-ASYEPR3S.mjs → token-goat-chunk-2IEIBVVD.mjs} +7 -3
- package/dist/{token-goat-chunk-LCZBPOIN.mjs → token-goat-chunk-2PCFAQAS.mjs} +66 -51
- package/dist/{token-goat-chunk-PM76YS22.mjs → token-goat-chunk-3H6FQT62.mjs} +1 -1
- package/dist/{token-goat-chunk-C5JIO6HK.mjs → token-goat-chunk-4DC6DDIB.mjs} +5 -5
- package/dist/{token-goat-chunk-YQ7WI2CO.mjs → token-goat-chunk-5USHO7B6.mjs} +393 -3957
- package/dist/{token-goat-chunk-2X2EBBC6.mjs → token-goat-chunk-63QWJ6UM.mjs} +2 -2
- package/dist/{token-goat-chunk-NDRP4KJQ.mjs → token-goat-chunk-BFNTLGVA.mjs} +226 -50
- package/dist/token-goat-chunk-BGHSSJUA.mjs +102 -0
- package/dist/{token-goat-chunk-IVUQLQWN.mjs → token-goat-chunk-DMQ32ZSR.mjs} +5 -3
- package/dist/{token-goat-chunk-SFAS46RE.mjs → token-goat-chunk-EJCY6KHO.mjs} +2 -2
- package/dist/{token-goat-chunk-Z6UXPYJA.mjs → token-goat-chunk-F2ARFHLJ.mjs} +1 -1
- package/dist/{token-goat-chunk-YKG35VHC.mjs → token-goat-chunk-GIMGT34T.mjs} +1 -1
- package/dist/{token-goat-chunk-SZWYESBS.mjs → token-goat-chunk-GMXBQS6O.mjs} +155 -54
- package/dist/{token-goat-chunk-S4XRY446.mjs → token-goat-chunk-H3DXI6VD.mjs} +4 -3
- package/dist/{token-goat-chunk-6DLVZDB6.mjs → token-goat-chunk-LJL6FGDS.mjs} +13 -12
- package/dist/{token-goat-chunk-NEI4NC54.mjs → token-goat-chunk-LKSXAMJB.mjs} +7 -2
- package/dist/{token-goat-chunk-A4JYKD5H.mjs → token-goat-chunk-M2R4UQ3B.mjs} +2 -2
- package/dist/{token-goat-chunk-MZDIJJ3R.mjs → token-goat-chunk-MA5BZ2KI.mjs} +40 -3
- package/dist/{token-goat-chunk-GIIHUSZX.mjs → token-goat-chunk-MIL62GMN.mjs} +41 -11
- package/dist/{token-goat-chunk-NU7TLMQK.mjs → token-goat-chunk-NAMH252H.mjs} +10 -7
- package/dist/{token-goat-chunk-DQ4J5AFF.mjs → token-goat-chunk-NOBZV7PM.mjs} +69 -18
- package/dist/{token-goat-chunk-5V7DAC7V.mjs → token-goat-chunk-NQZCSVN2.mjs} +10 -3
- package/dist/{token-goat-chunk-7ZYK25AO.mjs → token-goat-chunk-OCI25AXY.mjs} +4 -4
- package/dist/{token-goat-chunk-NDPO7GAH.mjs → token-goat-chunk-QMZMHQRR.mjs} +21 -20
- package/dist/{token-goat-chunk-K7F2BFIK.mjs → token-goat-chunk-R3X5TCPB.mjs} +1 -1
- package/dist/token-goat-chunk-RRNZMM3A.mjs +3364 -0
- package/dist/{token-goat-chunk-ATIFTMRC.mjs → token-goat-chunk-ULA4WCED.mjs} +7 -7
- package/dist/{token-goat-chunk-OUGNPMDA.mjs → token-goat-chunk-UMXD2M7O.mjs} +30 -14
- package/dist/{token-goat-chunk-YZX7EFG4.mjs → token-goat-chunk-UXHDBVUR.mjs} +1 -1
- package/dist/{token-goat-chunk-LT7JRU6K.mjs → token-goat-chunk-VVIYEFBI.mjs} +4 -4
- package/dist/{token-goat-chunk-BL5LNGBG.mjs → token-goat-chunk-XOHTFRDH.mjs} +2966 -3869
- package/dist/{token-goat-chunk-XEPXYDPI.mjs → token-goat-chunk-YPS64ACE.mjs} +11 -2
- package/dist/{token-goat-chunk-GMQQA7E4.mjs → token-goat-chunk-Z5VPAXTC.mjs} +2442 -722
- package/dist/{token-goat-chunk-3NSDDTGL.mjs → token-goat-chunk-ZEHFYJ7Z.mjs} +5 -5
- package/dist/token-goat-hook.mjs +13 -12
- package/dist/token-goat.core.mjs +21 -20
- 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-
|
|
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-
|
|
18
|
+
} from "./token-goat-chunk-R3X5TCPB.mjs";
|
|
22
19
|
import {
|
|
23
|
-
detectHarness,
|
|
24
20
|
loadConfig,
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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/
|
|
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(
|
|
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 =
|
|
606
|
+
const candidate = path.join(dir, "package.json");
|
|
2697
607
|
try {
|
|
2698
|
-
const stat =
|
|
608
|
+
const stat = fs.statSync(candidate);
|
|
2699
609
|
if (stat.isFile()) return loadPackageScripts(candidate, stat);
|
|
2700
610
|
} catch {
|
|
2701
611
|
}
|
|
2702
|
-
const parent =
|
|
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
|
|
2996
|
-
import { resolve
|
|
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(
|
|
3136
|
-
return LOCK_FILE_NAMES.has(
|
|
1045
|
+
function isLockFile(basename) {
|
|
1046
|
+
return LOCK_FILE_NAMES.has(basename.toLowerCase());
|
|
3137
1047
|
}
|
|
3138
|
-
function isManifestFile(
|
|
3139
|
-
const lower =
|
|
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(
|
|
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
|
|
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 &&
|
|
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(
|
|
1409
|
+
function dirStateFingerprintSync(path2) {
|
|
3500
1410
|
try {
|
|
3501
|
-
const stat =
|
|
1411
|
+
const stat = statSync2(path2);
|
|
3502
1412
|
if (!stat.isDirectory()) return null;
|
|
3503
|
-
const entries =
|
|
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(
|
|
1423
|
+
function fileStateFingerprintSync(path2) {
|
|
3514
1424
|
try {
|
|
3515
|
-
const stat =
|
|
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(
|
|
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 =
|
|
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
|
|
5070
|
-
const
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
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
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
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
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
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
|
-
|
|
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
|
|
5150
|
-
|
|
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
|
-
|
|
5153
|
-
|
|
5154
|
-
const
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
const
|
|
5160
|
-
if (
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
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
|
|
5167
|
-
const
|
|
5168
|
-
|
|
5169
|
-
const
|
|
5170
|
-
if (
|
|
5171
|
-
|
|
5172
|
-
|
|
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
|
-
|
|
1602
|
+
return token;
|
|
5182
1603
|
}
|
|
5183
1604
|
}
|
|
5184
|
-
return
|
|
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
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
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
|
|
5212
|
-
return
|
|
1652
|
+
function storeBashOutput(command, output, exitCode, cwd = null) {
|
|
1653
|
+
return Promise.resolve(storeBashOutputSync(command, output, exitCode, cwd));
|
|
5213
1654
|
}
|
|
5214
|
-
function
|
|
5215
|
-
|
|
5216
|
-
const
|
|
5217
|
-
|
|
5218
|
-
|
|
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
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
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
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
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
|
-
|
|
5293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
12658
|
+
blocks.push({ kind: "line", text: line });
|
|
16188
12659
|
} else {
|
|
16189
12660
|
currentIssues.push(line);
|
|
16190
12661
|
}
|
|
16191
12662
|
}
|
|
16192
12663
|
flushFile();
|
|
16193
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|