token-goat 2.9.28 → 2.9.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +36 -40
  2. package/SECURITY.md +28 -10
  3. package/THIRD_PARTY_NOTICES.md +40 -3
  4. package/dist/native/linux-arm64/tg-hook +0 -0
  5. package/dist/native/linux-x64/tg-hook +0 -0
  6. package/dist/ort-wasm-simd-threaded.mjs +59 -0
  7. package/dist/token-goat-chunk-3CHUNJ2O.mjs +53 -0
  8. package/dist/token-goat-chunk-3Z5S4KGM.mjs +918 -0
  9. package/dist/token-goat-chunk-4FVIAHSI.mjs +211 -0
  10. package/dist/{token-goat-chunk-66LODAUT.mjs → token-goat-chunk-4JJS54TR.mjs} +4 -3
  11. package/dist/token-goat-chunk-4LCCNRTG.mjs +313 -0
  12. package/dist/token-goat-chunk-54K3EUV3.mjs +35 -0
  13. package/dist/token-goat-chunk-56EIMS3E.mjs +159 -0
  14. package/dist/token-goat-chunk-5BCIJXMO.mjs +609 -0
  15. package/dist/token-goat-chunk-5KDMCMF2.mjs +362 -0
  16. package/dist/token-goat-chunk-5RG5SBL6.mjs +221 -0
  17. package/dist/{token-goat-chunk-RRNZMM3A.mjs → token-goat-chunk-5Y6ZYL6U.mjs} +21 -2
  18. package/dist/{token-goat-chunk-NILVK7XM.mjs → token-goat-chunk-64FBPDTK.mjs} +12 -6
  19. package/dist/{token-goat-chunk-LEG3JNJ6.mjs → token-goat-chunk-664RTTUV.mjs} +31 -15
  20. package/dist/token-goat-chunk-67TQJ76B.mjs +57 -0
  21. package/dist/token-goat-chunk-6QMAS7AB.mjs +42 -0
  22. package/dist/token-goat-chunk-7BXSNBY5.mjs +253 -0
  23. package/dist/{token-goat-chunk-F7QZRIWM.mjs → token-goat-chunk-7F2UBPD3.mjs} +42 -35
  24. package/dist/{token-goat-chunk-CC2PYJM7.mjs → token-goat-chunk-7IN2MKDS.mjs} +718 -595
  25. package/dist/token-goat-chunk-7NADEDKJ.mjs +124 -0
  26. package/dist/{token-goat-chunk-6SGLIPQB.mjs → token-goat-chunk-A3CK6N5Z.mjs} +621 -72
  27. package/dist/{token-goat-chunk-HXNAC4ZF.mjs → token-goat-chunk-AFLYLHDH.mjs} +48 -33
  28. package/dist/token-goat-chunk-AM5V6GSX.mjs +367 -0
  29. package/dist/{token-goat-chunk-I7CB3X4K.mjs → token-goat-chunk-APGZXSPS.mjs} +44 -148
  30. package/dist/{token-goat-chunk-BJBRGPBX.mjs → token-goat-chunk-ARDNEC2U.mjs} +11 -7
  31. package/dist/token-goat-chunk-ATCPY7EQ.mjs +134 -0
  32. package/dist/token-goat-chunk-B2RZZCSS.mjs +354 -0
  33. package/dist/{token-goat-chunk-QM76S6YS.mjs → token-goat-chunk-B45VMIIH.mjs} +6 -5
  34. package/dist/token-goat-chunk-BPGCUMR7.mjs +30 -0
  35. package/dist/{token-goat-chunk-GGLEE35R.mjs → token-goat-chunk-C63JQV5K.mjs} +49 -10
  36. package/dist/{token-goat-chunk-RJH64KQH.mjs → token-goat-chunk-COBQGL5S.mjs} +2661 -2440
  37. package/dist/token-goat-chunk-COTXZHTS.mjs +39 -0
  38. package/dist/token-goat-chunk-CYCEYAEO.mjs +265 -0
  39. package/dist/token-goat-chunk-CZ3LUXYZ.mjs +4115 -0
  40. package/dist/token-goat-chunk-DZX2YDPT.mjs +15 -0
  41. package/dist/token-goat-chunk-E3OQX2XT.mjs +824 -0
  42. package/dist/token-goat-chunk-EFHMMIGP.mjs +68 -0
  43. package/dist/token-goat-chunk-ETBHVDB6.mjs +328 -0
  44. package/dist/token-goat-chunk-EVMZJ27Z.mjs +147 -0
  45. package/dist/token-goat-chunk-EXKGNHEG.mjs +2096 -0
  46. package/dist/{token-goat-chunk-6Z5ULNE5.mjs → token-goat-chunk-FBNQNSJK.mjs} +2490 -4957
  47. package/dist/{token-goat-chunk-NMTKNYGF.mjs → token-goat-chunk-FNFBKLRD.mjs} +74 -9
  48. package/dist/{token-goat-chunk-XI2VFAV6.mjs → token-goat-chunk-FZ3NEW5O.mjs} +33 -19
  49. package/dist/token-goat-chunk-G5PJ7AGM.mjs +36 -0
  50. package/dist/{token-goat-chunk-VE27HPIF.mjs → token-goat-chunk-GFBSN7BD.mjs} +30 -398
  51. package/dist/{token-goat-chunk-L45ZQRT7.mjs → token-goat-chunk-GOLEOXZP.mjs} +780 -786
  52. package/dist/token-goat-chunk-H2K3QP6H.mjs +24 -0
  53. package/dist/token-goat-chunk-H2PHHGM5.mjs +188 -0
  54. package/dist/token-goat-chunk-HAVAOB3S.mjs +87 -0
  55. package/dist/token-goat-chunk-HG45AF2B.mjs +2228 -0
  56. package/dist/{token-goat-chunk-CBMY2FGI.mjs → token-goat-chunk-HOJLCGDP.mjs} +17113 -15554
  57. package/dist/{token-goat-chunk-HUDC5GS5.mjs → token-goat-chunk-HYBFA5X3.mjs} +120 -19
  58. package/dist/token-goat-chunk-IVALLT3S.mjs +48 -0
  59. package/dist/token-goat-chunk-IVE6U7KV.mjs +53 -0
  60. package/dist/{token-goat-chunk-UBUKHVYW.mjs → token-goat-chunk-IVKIZWAB.mjs} +3 -3
  61. package/dist/{token-goat-chunk-4JMWPWCS.mjs → token-goat-chunk-JB6QN6VF.mjs} +6 -4
  62. package/dist/{token-goat-chunk-Q4S3UQGO.mjs → token-goat-chunk-JDHE26RP.mjs} +10 -3
  63. package/dist/{token-goat-chunk-7R2XYKC5.mjs → token-goat-chunk-JDU3EXSU.mjs} +14 -10
  64. package/dist/token-goat-chunk-JDWCRD6M.mjs +253 -0
  65. package/dist/{token-goat-chunk-XBESNWEX.mjs → token-goat-chunk-JN4XMLSD.mjs} +7 -4
  66. package/dist/token-goat-chunk-JTQQGXZH.mjs +733 -0
  67. package/dist/token-goat-chunk-KKUFXAPD.mjs +25 -0
  68. package/dist/token-goat-chunk-KQNZDCBD.mjs +47 -0
  69. package/dist/{token-goat-chunk-4RN64ZND.mjs → token-goat-chunk-KWPNJBDH.mjs} +28 -3
  70. package/dist/token-goat-chunk-LOBGHXPB.mjs +242 -0
  71. package/dist/token-goat-chunk-MKPF4CL5.mjs +37 -0
  72. package/dist/token-goat-chunk-MNWKPRAL.mjs +13 -0
  73. package/dist/{token-goat-chunk-YWSP6IU7.mjs → token-goat-chunk-N2NMER56.mjs} +25 -15
  74. package/dist/token-goat-chunk-NYKZLXMK.mjs +218 -0
  75. package/dist/token-goat-chunk-OJBNYTJP.mjs +3087 -0
  76. package/dist/token-goat-chunk-P4QMM4WK.mjs +295 -0
  77. package/dist/token-goat-chunk-PD5VKH46.mjs +184 -0
  78. package/dist/token-goat-chunk-PEPTHRFF.mjs +129 -0
  79. package/dist/{token-goat-chunk-VWZKZPSR.mjs → token-goat-chunk-PU7V3FD6.mjs} +8752 -11098
  80. package/dist/token-goat-chunk-PY533T7F.mjs +46 -0
  81. package/dist/token-goat-chunk-Q5VTGRH7.mjs +761 -0
  82. package/dist/{token-goat-chunk-IHKZ3QNA.mjs → token-goat-chunk-QCLDAQTS.mjs} +33 -19
  83. package/dist/{token-goat-chunk-LKSXAMJB.mjs → token-goat-chunk-QSBRTGM2.mjs} +9 -2
  84. package/dist/{token-goat-chunk-4STDIGYC.mjs → token-goat-chunk-RC35RBUL.mjs} +7 -5
  85. package/dist/{token-goat-chunk-D5ICEPSB.mjs → token-goat-chunk-RHXKCZX3.mjs} +887 -507
  86. package/dist/{token-goat-chunk-RSS3TLXU.mjs → token-goat-chunk-RZRTDST7.mjs} +17 -7
  87. package/dist/{token-goat-chunk-MAMQPC43.mjs → token-goat-chunk-SJHWVNDQ.mjs} +15 -21
  88. package/dist/token-goat-chunk-SMVRN2HU.mjs +188 -0
  89. package/dist/{token-goat-chunk-EL7M26WP.mjs → token-goat-chunk-SQSSNQPE.mjs} +2100 -1442
  90. package/dist/{token-goat-chunk-OZRSREKR.mjs → token-goat-chunk-T4FK7VOV.mjs} +3 -3
  91. package/dist/token-goat-chunk-U4N3BN2W.mjs +92 -0
  92. package/dist/token-goat-chunk-UA5V6P3P.mjs +122 -0
  93. package/dist/{token-goat-chunk-XKY4VKJ4.mjs → token-goat-chunk-UQ5UK2T7.mjs} +15 -4
  94. package/dist/token-goat-chunk-USILZ3T3.mjs +53 -0
  95. package/dist/token-goat-chunk-VES7EL7O.mjs +205 -0
  96. package/dist/{token-goat-chunk-M56BWPBN.mjs → token-goat-chunk-VL3YIFHZ.mjs} +550 -49
  97. package/dist/{token-goat-chunk-IZXKVT4A.mjs → token-goat-chunk-VPBBTSJR.mjs} +362 -128
  98. package/dist/{token-goat-chunk-F2ARFHLJ.mjs → token-goat-chunk-W3QCJS6G.mjs} +10 -2
  99. package/dist/{token-goat-chunk-FSTWAOW6.mjs → token-goat-chunk-WY3L4DVT.mjs} +11 -5
  100. package/dist/{token-goat-chunk-OSUFN2FV.mjs → token-goat-chunk-XB5KKOCG.mjs} +5 -25
  101. package/dist/token-goat-chunk-XKI4SEMX.mjs +66 -0
  102. package/dist/token-goat-chunk-XU4P6Z24.mjs +133 -0
  103. package/dist/{token-goat-chunk-6CCQDAYY.mjs → token-goat-chunk-Y776AENC.mjs} +138 -33
  104. package/dist/token-goat-chunk-YARZFCZS.mjs +359 -0
  105. package/dist/{token-goat-chunk-EEIDFMEM.mjs → token-goat-chunk-YZAAZU2S.mjs} +12 -3
  106. package/dist/{token-goat-chunk-E3K3BTEQ.mjs → token-goat-chunk-Z44YO3QZ.mjs} +12 -6
  107. package/dist/token-goat-chunk-Z57YODA2.mjs +35 -0
  108. package/dist/{token-goat-chunk-DA43OE7L.mjs → token-goat-chunk-ZCDTCVIS.mjs} +49 -30
  109. package/dist/token-goat-chunk-ZEJVMUQH.mjs +48 -0
  110. package/dist/{token-goat-chunk-BGHSSJUA.mjs → token-goat-chunk-ZFOBKUWH.mjs} +1 -1
  111. package/dist/token-goat-chunk-ZMBL6UVV.mjs +110 -0
  112. package/dist/{token-goat-chunk-SJEKA7Y2.mjs → token-goat-chunk-ZQSJMO2R.mjs} +381 -201
  113. package/dist/token-goat-hook-client.cjs +539 -0
  114. package/dist/token-goat-hook-client.mjs +26 -0
  115. package/dist/token-goat-hook.mjs +48 -28
  116. package/dist/token-goat.core.mjs +10 -45
  117. package/docs/C4_RUNTIME_ARCHITECTURE.md +3 -3
  118. package/docs/cli.md +82 -47
  119. package/docs/install.md +58 -23
  120. package/docs/security.md +7 -7
  121. package/package.json +17 -7
  122. package/scripts/verify-native-dist.mjs +386 -0
  123. package/dist/token-goat-chunk-4JJBFT7V.mjs +0 -20
  124. package/dist/token-goat-chunk-5NNYIUGV.mjs +0 -143
  125. package/dist/token-goat-chunk-6DQNNFJ6.mjs +0 -294
  126. package/dist/token-goat-chunk-7MAFDRVR.mjs +0 -47
  127. package/dist/token-goat-chunk-BCN2YQC6.mjs +0 -32
  128. package/dist/token-goat-chunk-D7GHQKQD.mjs +0 -30
  129. package/dist/token-goat-chunk-GMOUBOX4.mjs +0 -386
  130. package/dist/token-goat-chunk-IFORDBGC.mjs +0 -192
  131. package/dist/token-goat-chunk-JXK2HQXS.mjs +0 -318
  132. package/dist/token-goat-chunk-L7SDVGBT.mjs +0 -430
  133. package/dist/token-goat-chunk-MMLMALWK.mjs +0 -1381
  134. package/dist/token-goat-chunk-MVSWBNMD.mjs +0 -1985
@@ -0,0 +1,2228 @@
1
+ import { createRequire as __cjsRequire } from 'node:module';
2
+ const require = __cjsRequire(import.meta.url);
3
+ import {
4
+ HookCommandRewriteError,
5
+ LEGACY_SHIM_FILE,
6
+ MATERIALIZE_SHRUNK_IMAGE_JS,
7
+ SHIM_FILE,
8
+ SHIM_MAX_BUFFER_CONST,
9
+ SHIM_REQUIRES,
10
+ SHIM_SPAWN_LADDER,
11
+ SHIM_TRY_IN_PROCESS,
12
+ SHIM_TRY_SERVER,
13
+ SHIM_VALID_HOOK_EVENTS,
14
+ anchoredMarkerPattern,
15
+ buildGuidanceBlock,
16
+ buildGuidanceBody,
17
+ editAt,
18
+ finishHookUninstall,
19
+ groupHasTokenGoat,
20
+ hookCommandRewrittenSpan,
21
+ legacyShimForwarder,
22
+ nativeHookBinary,
23
+ nativeHookCommandLine,
24
+ skillDescriptionLine,
25
+ splitHookCommand
26
+ } from "./token-goat-chunk-OJBNYTJP.mjs";
27
+ import {
28
+ parseJsonOrJsonc,
29
+ stripBom
30
+ } from "./token-goat-chunk-XKI4SEMX.mjs";
31
+ import {
32
+ toolMatcherFor
33
+ } from "./token-goat-chunk-P4QMM4WK.mjs";
34
+ import {
35
+ GEMINI_TOOL_NAME_MAP
36
+ } from "./token-goat-chunk-VPBBTSJR.mjs";
37
+ import {
38
+ loadConfig,
39
+ parse,
40
+ stringify
41
+ } from "./token-goat-chunk-7IN2MKDS.mjs";
42
+ import {
43
+ atomicWriteText,
44
+ backupFile,
45
+ ensureDirRecordingCreation,
46
+ ensureDirSync,
47
+ extractErrorMessage,
48
+ hasCreatedConfig,
49
+ hookCommandFor,
50
+ hookPowershellCommand,
51
+ installSingleFilePlugin,
52
+ projectScopeRoot,
53
+ recordCreatedBy,
54
+ recordCreatedConfig,
55
+ removeCreatedBackups,
56
+ removeCreatedIfEmpty,
57
+ removeCreatedTree,
58
+ stripDelimitedBlock,
59
+ stripOwnHooksFromMap,
60
+ stripStaleGroupHooks,
61
+ takeCreatedConfig,
62
+ uninstallSingleFilePlugin,
63
+ upsertDelimitedBlock,
64
+ withInstallScope,
65
+ writeConfigText,
66
+ writeIfDifferent,
67
+ writeJsonSettings
68
+ } from "./token-goat-chunk-GOLEOXZP.mjs";
69
+ import {
70
+ foldPath,
71
+ powershellHookLine,
72
+ quoteShellPath
73
+ } from "./token-goat-chunk-AM5V6GSX.mjs";
74
+ import {
75
+ init_define_import_meta_env
76
+ } from "./token-goat-chunk-A37V4PBF.mjs";
77
+
78
+ // src/bridges/gemini_install.ts
79
+ init_define_import_meta_env();
80
+ import * as fs2 from "node:fs";
81
+ import * as os from "node:os";
82
+ import * as path2 from "node:path";
83
+
84
+ // src/bridges/commented_settings.ts
85
+ init_define_import_meta_env();
86
+ import * as fs from "node:fs";
87
+ import * as path from "node:path";
88
+ function isPlainObject(value) {
89
+ return typeof value === "object" && value !== null && !Array.isArray(value);
90
+ }
91
+ function applyDiff(text, prev, next, at) {
92
+ let out = text;
93
+ for (const key of Object.keys(prev)) {
94
+ if (!(key in next)) out = editAt(out, [...at, key], void 0);
95
+ }
96
+ for (const key of Object.keys(next)) {
97
+ const before = prev[key];
98
+ const after = next[key];
99
+ if (after === void 0) {
100
+ if (key in prev) out = editAt(out, [...at, key], void 0);
101
+ } else if (!(key in prev)) out = editAt(out, [...at, key], after);
102
+ else if (isPlainObject(before) && isPlainObject(after)) out = applyDiff(out, before, after, [...at, key]);
103
+ else if (JSON.stringify(before) !== JSON.stringify(after)) out = editAt(out, [...at, key], after);
104
+ }
105
+ return out;
106
+ }
107
+ function writeSettingsKeepingComments(p, next, opts = {}) {
108
+ let raw;
109
+ try {
110
+ raw = fs.readFileSync(p, "utf8");
111
+ } catch {
112
+ raw = void 0;
113
+ }
114
+ if (raw === void 0 || !isPlainObject(next)) return writeJsonSettings(p, next);
115
+ try {
116
+ JSON.parse(raw);
117
+ return writeJsonSettings(p, next);
118
+ } catch {
119
+ }
120
+ const body = stripBom(raw);
121
+ let prev;
122
+ try {
123
+ prev = parseJsonOrJsonc(body, opts);
124
+ } catch {
125
+ return writeJsonSettings(p, next);
126
+ }
127
+ if (!isPlainObject(prev)) return writeJsonSettings(p, next);
128
+ const edited = applyDiff(body, prev, next, []);
129
+ ensureDirSync(path.dirname(p));
130
+ backupFile(p);
131
+ writeConfigText(p, edited.endsWith("\n") ? edited : `${edited}
132
+ `);
133
+ }
134
+
135
+ // src/bridges/gemini_install.ts
136
+ var GEMINI_LEGACY_COMMAND_MARKER = "token-goat hook";
137
+ var GEMINI_HOOK_EVENTS = ["BeforeTool", "AfterTool", "PreCompress"];
138
+ var GEMINI_EVENT_ARG = {
139
+ BeforeTool: "pre_tool_use",
140
+ AfterTool: "post_tool_use",
141
+ PreCompress: "pre_compact"
142
+ };
143
+ function registeredInternalTools(eventName) {
144
+ const matcher = toolMatcherFor(eventName);
145
+ const names = /* @__PURE__ */ new Set();
146
+ if (matcher === null) return names;
147
+ for (const part of matcher.split("|")) {
148
+ const match = /^\^([A-Za-z]+)\$$/.exec(part);
149
+ if (match?.[1] !== void 0) names.add(match[1]);
150
+ }
151
+ return names;
152
+ }
153
+ var GeminiSettingsParseError = class extends Error {
154
+ };
155
+ function geminiSettingsPath() {
156
+ return path2.join(os.homedir(), ".gemini", "settings.json");
157
+ }
158
+ function readGeminiSettings(p, opts = {}) {
159
+ const refuse = (problem, detail) => new GeminiSettingsParseError(
160
+ (opts.command === "uninstall" ? `Gemini settings file '${p}' is unreadable: it ${problem}. Uninstall left it and its backups untouched; fix the file and run uninstall again.` : `Gemini settings file '${p}' ${problem}. Fix or back up the file before running install.`) + (detail === void 0 ? "" : ` (${detail})`)
161
+ );
162
+ let raw;
163
+ try {
164
+ raw = fs2.readFileSync(p, "utf8");
165
+ } catch (e) {
166
+ const code = e.code;
167
+ if (opts.strict === true && code !== "ENOENT" && code !== "ENOTDIR") throw refuse("exists but cannot be read", code ?? extractErrorMessage(e));
168
+ return {};
169
+ }
170
+ let parsed;
171
+ try {
172
+ parsed = parseJsonOrJsonc(raw, { allowTrailingComma: false });
173
+ } catch (e) {
174
+ if (opts.strict === true) throw refuse("exists but contains invalid JSON", extractErrorMessage(e));
175
+ return {};
176
+ }
177
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
178
+ return parsed;
179
+ }
180
+ if (opts.strict === true) throw refuse("does not contain a JSON object at the top level");
181
+ return {};
182
+ }
183
+ var GEMINI_LEGACY_MARKER_PATTERN = anchoredMarkerPattern(GEMINI_LEGACY_COMMAND_MARKER);
184
+ var GEMINI_COMMAND_PATTERN = /^"[^"]+"\s+"([^"]+)"\s+hook\s+(?:pre_tool_use|post_tool_use|pre_compact)$/;
185
+ var GEMINI_ENTRY_PATH_MARKER_PATTERN = anchoredMarkerPattern("token-goat");
186
+ function isGeminiTokenGoatCommand(command) {
187
+ if (typeof command !== "string") return false;
188
+ const match = GEMINI_COMMAND_PATTERN.exec(command);
189
+ if (match) {
190
+ const entryPath = match[1] ?? "";
191
+ return GEMINI_ENTRY_PATH_MARKER_PATTERN.test(entryPath);
192
+ }
193
+ return GEMINI_LEGACY_MARKER_PATTERN.test(command);
194
+ }
195
+ function isCurrentGeminiTokenGoatCommand(command, desiredCommand) {
196
+ if (typeof command !== "string") return false;
197
+ return command === desiredCommand;
198
+ }
199
+ function geminiHookCommand(eventArg) {
200
+ const entryPath = process.argv[1];
201
+ if (!entryPath) return `token-goat hook ${eventArg}`;
202
+ return `${quoteShellPath(process.execPath)} ${quoteShellPath(entryPath)} hook ${eventArg}`;
203
+ }
204
+ function geminiNamesByInternalTool() {
205
+ const out = /* @__PURE__ */ new Map();
206
+ for (const [geminiName, internalTool] of Object.entries(GEMINI_TOOL_NAME_MAP)) {
207
+ const list = out.get(internalTool);
208
+ if (list) {
209
+ list.push(geminiName);
210
+ } else {
211
+ out.set(internalTool, [geminiName]);
212
+ }
213
+ }
214
+ return out;
215
+ }
216
+ function desiredMatchersFor(event) {
217
+ if (event === "PreCompress") return [void 0];
218
+ const toolSet = registeredInternalTools(GEMINI_EVENT_ARG[event]);
219
+ const matchers = [];
220
+ for (const [internalTool, names] of geminiNamesByInternalTool()) {
221
+ if (!toolSet.has(internalTool)) continue;
222
+ matchers.push(`^(${names.join("|")})$`);
223
+ }
224
+ return matchers;
225
+ }
226
+ function installGemini() {
227
+ const p = geminiSettingsPath();
228
+ const settings = readGeminiSettings(p, { strict: true });
229
+ const hooks = settings.hooks ?? {};
230
+ let changed = false;
231
+ for (const event of GEMINI_HOOK_EVENTS) {
232
+ const command = geminiHookCommand(GEMINI_EVENT_ARG[event]);
233
+ const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
234
+ for (const matcher of desiredMatchersFor(event)) {
235
+ if (groupHasTokenGoat(groups, matcher, (c) => isCurrentGeminiTokenGoatCommand(c, command))) continue;
236
+ const nextGroups = stripStaleGroupHooks(groups, isGeminiTokenGoatCommand, { matcher });
237
+ groups.length = 0;
238
+ groups.push(...nextGroups);
239
+ const group = matcher === void 0 ? { hooks: [{ type: "command", command }] } : { matcher, hooks: [{ type: "command", command }] };
240
+ groups.push(group);
241
+ changed = true;
242
+ }
243
+ hooks[event] = groups;
244
+ }
245
+ if (!changed) {
246
+ return { settingsPath: p, alreadyInstalled: true };
247
+ }
248
+ settings.hooks = hooks;
249
+ recordCreatedBy([p], () => writeSettingsKeepingComments(p, settings, { allowTrailingComma: false }));
250
+ return { settingsPath: p, alreadyInstalled: false };
251
+ }
252
+ function uninstallGemini() {
253
+ const p = geminiSettingsPath();
254
+ const settings = readGeminiSettings(p, { strict: true, command: "uninstall" });
255
+ const removed = settings.hooks !== void 0 && stripOwnHooksFromMap(settings.hooks, isGeminiTokenGoatCommand);
256
+ const result = finishHookUninstall(p, settings, removed, (file, next) => writeSettingsKeepingComments(file, next, { allowTrailingComma: false }));
257
+ removeCreatedTree([p]);
258
+ return result;
259
+ }
260
+ function isGeminiInstalled() {
261
+ const settings = readGeminiSettings(geminiSettingsPath());
262
+ const hooks = settings.hooks;
263
+ if (hooks === void 0) return false;
264
+ for (const event of GEMINI_HOOK_EVENTS) {
265
+ const command = geminiHookCommand(GEMINI_EVENT_ARG[event]);
266
+ for (const matcher of desiredMatchersFor(event)) {
267
+ if (!groupHasTokenGoat(hooks[event], matcher, (c) => isCurrentGeminiTokenGoatCommand(c, command))) return false;
268
+ }
269
+ }
270
+ return true;
271
+ }
272
+
273
+ // src/bridges/qwen_install.ts
274
+ init_define_import_meta_env();
275
+ import * as fs3 from "node:fs";
276
+ import * as os2 from "node:os";
277
+ import * as path3 from "node:path";
278
+ var QWEN_HOOK_EVENTS = ["PreToolUse", "PostToolUse", "PreCompact", "UserPromptSubmit", "SubagentStop"];
279
+ var QWEN_EVENT_ARG = {
280
+ PreToolUse: "pre_tool_use",
281
+ PostToolUse: "post_tool_use",
282
+ PreCompact: "pre_compact",
283
+ UserPromptSubmit: "user_prompt_submit",
284
+ SubagentStop: "subagent_stop"
285
+ };
286
+ var QwenSettingsParseError = class extends Error {
287
+ };
288
+ function qwenSettingsPath() {
289
+ return path3.join(os2.homedir(), ".qwen", "settings.json");
290
+ }
291
+ function readQwenSettings(p, opts = {}) {
292
+ const refuse = (problem, detail) => new QwenSettingsParseError(
293
+ (opts.command === "uninstall" ? `Qwen Code settings file '${p}' is unreadable: it ${problem}. Uninstall left it and its backups untouched; fix the file and run uninstall again.` : `Qwen Code settings file '${p}' ${problem}. Fix or back up the file before running install.`) + (detail === void 0 ? "" : ` (${detail})`)
294
+ );
295
+ let raw;
296
+ try {
297
+ raw = fs3.readFileSync(p, "utf8");
298
+ } catch (e) {
299
+ const code = e.code;
300
+ if (opts.strict === true && code !== "ENOENT" && code !== "ENOTDIR") throw refuse("exists but cannot be read", code ?? extractErrorMessage(e));
301
+ return {};
302
+ }
303
+ let parsed;
304
+ try {
305
+ parsed = parseJsonOrJsonc(raw, { allowTrailingComma: false });
306
+ } catch (e) {
307
+ if (opts.strict === true) throw refuse("exists but contains invalid JSON", extractErrorMessage(e));
308
+ return {};
309
+ }
310
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
311
+ return parsed;
312
+ }
313
+ if (opts.strict === true) throw refuse("does not contain a JSON object at the top level");
314
+ return {};
315
+ }
316
+ var QWEN_LEGACY_COMMAND_MARKER = "token-goat hook";
317
+ var QWEN_LEGACY_MARKER_PATTERN = anchoredMarkerPattern(QWEN_LEGACY_COMMAND_MARKER);
318
+ var QWEN_COMMAND_PATTERN = /^"[^"]+"\s+"([^"]+)"\s+hook\s+(?:pre_tool_use|post_tool_use|pre_compact|user_prompt_submit|subagent_stop)(?:\s+--harness\s+qwen)?$/;
319
+ var QWEN_ENTRY_PATH_MARKER_PATTERN = anchoredMarkerPattern("token-goat");
320
+ function isQwenTokenGoatCommand(command) {
321
+ if (typeof command !== "string") return false;
322
+ const match = QWEN_COMMAND_PATTERN.exec(command);
323
+ if (match) {
324
+ const entryPath = match[1] ?? "";
325
+ return QWEN_ENTRY_PATH_MARKER_PATTERN.test(entryPath);
326
+ }
327
+ return QWEN_LEGACY_MARKER_PATTERN.test(command);
328
+ }
329
+ function isCurrentQwenTokenGoatCommand(command, desiredCommand) {
330
+ if (typeof command !== "string") return false;
331
+ return command === desiredCommand;
332
+ }
333
+ function qwenHookCommand(eventArg) {
334
+ const entryPath = process.argv[1];
335
+ if (!entryPath) return `token-goat hook ${eventArg} --harness qwen`;
336
+ return `${quoteShellPath(process.execPath)} ${quoteShellPath(entryPath)} hook ${eventArg} --harness qwen`;
337
+ }
338
+ function installQwen() {
339
+ const p = qwenSettingsPath();
340
+ const settings = readQwenSettings(p, { strict: true });
341
+ const hooks = settings.hooks ?? {};
342
+ let changed = false;
343
+ for (const event of QWEN_HOOK_EVENTS) {
344
+ const command = qwenHookCommand(QWEN_EVENT_ARG[event]);
345
+ const existingGroups = Array.isArray(hooks[event]) ? hooks[event] : [];
346
+ if (groupHasTokenGoat(existingGroups, "", (c) => isCurrentQwenTokenGoatCommand(c, command))) {
347
+ hooks[event] = existingGroups;
348
+ continue;
349
+ }
350
+ const groups = stripStaleGroupHooks(existingGroups, isQwenTokenGoatCommand);
351
+ groups.push({ matcher: "", hooks: [{ type: "command", command }] });
352
+ hooks[event] = groups;
353
+ changed = true;
354
+ }
355
+ if (!changed) {
356
+ return { settingsPath: p, alreadyInstalled: true };
357
+ }
358
+ settings.hooks = hooks;
359
+ recordCreatedBy([p], () => writeSettingsKeepingComments(p, settings, { allowTrailingComma: false }));
360
+ return { settingsPath: p, alreadyInstalled: false };
361
+ }
362
+ function uninstallQwen() {
363
+ const p = qwenSettingsPath();
364
+ const settings = readQwenSettings(p, { strict: true, command: "uninstall" });
365
+ const removed = settings.hooks !== void 0 && stripOwnHooksFromMap(settings.hooks, isQwenTokenGoatCommand);
366
+ const result = finishHookUninstall(p, settings, removed, (file, next) => writeSettingsKeepingComments(file, next, { allowTrailingComma: false }));
367
+ removeCreatedTree([p]);
368
+ return result;
369
+ }
370
+ function isQwenInstalled() {
371
+ const settings = readQwenSettings(qwenSettingsPath());
372
+ const hooks = settings.hooks;
373
+ if (hooks === void 0) return false;
374
+ for (const event of QWEN_HOOK_EVENTS) {
375
+ const command = qwenHookCommand(QWEN_EVENT_ARG[event]);
376
+ if (!groupHasTokenGoat(hooks[event], "", (c) => isCurrentQwenTokenGoatCommand(c, command))) return false;
377
+ }
378
+ return true;
379
+ }
380
+
381
+ // src/bridges/kimi_install.ts
382
+ init_define_import_meta_env();
383
+ import * as fs4 from "node:fs";
384
+ import * as os3 from "node:os";
385
+ import * as path4 from "node:path";
386
+
387
+ // src/bridges/kimi.ts
388
+ init_define_import_meta_env();
389
+ var KIMI_HOOK_SCRIPT = `#!/usr/bin/env node
390
+ // 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.
391
+ 'use strict'
392
+ ${SHIM_REQUIRES}
393
+
394
+ ${SHIM_VALID_HOOK_EVENTS}
395
+
396
+ // Kimi Code publishes no ambient per-session env var identifying its own hook
397
+ // subprocesses, so the harness identity has to be injected here for
398
+ // detectHarness() (src/bridges/registry.ts) to resolve 'kimi'.
399
+ process.env.TOKEN_GOAT_HARNESS_OVERRIDE = 'kimi'
400
+
401
+ // Pull the hint text out of a token-goat response: hookSpecificOutput.additionalContext
402
+ // (most events) or the top-level systemMessage (notification/pre_compact, which Claude
403
+ // Code rejects additionalContext on). Kimi reads neither, so both fold into its 'message'.
404
+ function hintText(parsed) {
405
+ if (!parsed || typeof parsed !== 'object') return ''
406
+ const hso = parsed.hookSpecificOutput
407
+ if (hso && typeof hso === 'object' && typeof hso.additionalContext === 'string') return hso.additionalContext
408
+ if (typeof parsed.systemMessage === 'string') return parsed.systemMessage
409
+ return ''
410
+ }
411
+
412
+ // Translate a token-goat (Claude Code shaped) response into Kimi's contract.
413
+ // Returns '' for anything Kimi cannot act on, which the caller writes as empty
414
+ // stdout rather than '{}' -- see this module's docstring for why that matters
415
+ // on UserPromptSubmit.
416
+ function toKimi(parsed) {
417
+ if (!parsed || typeof parsed !== 'object') return ''
418
+ if (parsed.decision === 'block' && typeof parsed.reason === 'string' && parsed.reason) {
419
+ return JSON.stringify({
420
+ message: parsed.reason,
421
+ hookSpecificOutput: { permissionDecision: 'deny', permissionDecisionReason: parsed.reason },
422
+ })
423
+ }
424
+ const hint = hintText(parsed)
425
+ if (hint) return JSON.stringify({ message: hint })
426
+ return ''
427
+ }
428
+
429
+ ${SHIM_TRY_SERVER}
430
+
431
+ ${SHIM_TRY_IN_PROCESS}
432
+
433
+ ${SHIM_MAX_BUFFER_CONST}
434
+
435
+ async function main() {
436
+ const eventName = process.argv[2] || ''
437
+ if (!VALID_HOOK_EVENTS.has(eventName)) return
438
+ let input = ''
439
+ try {
440
+ input = require('node:fs').readFileSync(0, 'utf8')
441
+ } catch {
442
+ return
443
+ }
444
+ // process.argv[3], when present, is the absolute path to the token-goat CLI entry that ran
445
+ // 'token-goat install --kimi' (baked in by hookCommandFor in kimi_install.ts). Same three-step
446
+ // ladder as the Codex shim: in-process hook lib, then the baked entry via process.execPath,
447
+ // then a PATH-based shell:true invocation for an older cached hook config. The 3000ms
448
+ // timeout keeps both spawn fallbacks well inside Kimi's own 30s default hook timeout
449
+ // (docs/en/customization/hooks.md), so token-goat degrades to its own fail-open no-op.
450
+ const entryPath = process.argv[3]
451
+ let stdout = await tryInProcess(entryPath, eventName, input)
452
+ if (stdout === undefined) {
453
+ ${SHIM_SPAWN_LADDER}
454
+ : spawnSync('token-goat hook ' + eventName, {
455
+ input,
456
+ encoding: 'utf8',
457
+ shell: true,
458
+ timeout: 3000,
459
+ killSignal: 'SIGKILL',
460
+ maxBuffer: SHIM_MAX_BUFFER_BYTES,
461
+ })
462
+ if (res.status !== 0 || !res.stdout) return
463
+ stdout = res.stdout
464
+ }
465
+ let parsed
466
+ try {
467
+ parsed = JSON.parse(stdout)
468
+ } catch {
469
+ return
470
+ }
471
+ const out = toKimi(parsed)
472
+ if (out) process.stdout.write(out)
473
+ }
474
+
475
+ main().catch(() => {
476
+ // Fail open: write nothing, exit 0. Kimi treats a silent hook as "allow".
477
+ })
478
+ `;
479
+
480
+ // src/bridges/kimi_install.ts
481
+ var KIMI_EVENT_ARG = {
482
+ PreToolUse: "pre_tool_use",
483
+ PostToolUse: "post_tool_use",
484
+ PreCompact: "pre_compact",
485
+ UserPromptSubmit: "user_prompt_submit",
486
+ SubagentStop: "subagent_stop",
487
+ SessionStart: "session_start"
488
+ };
489
+ var KIMI_HOOK_EVENTS = Object.keys(KIMI_EVENT_ARG);
490
+ var KimiConfigParseError = class extends Error {
491
+ };
492
+ function kimiHome() {
493
+ const override = process.env["KIMI_CODE_HOME"];
494
+ if (override !== void 0 && override.trim() !== "") return path4.resolve(override);
495
+ return path4.join(os3.homedir(), ".kimi-code");
496
+ }
497
+ function kimiConfigPath() {
498
+ return path4.join(kimiHome(), "config.toml");
499
+ }
500
+ function kimiAgentsPath() {
501
+ return path4.join(kimiHome(), "AGENTS.md");
502
+ }
503
+ function kimiHookScriptPath() {
504
+ return path4.join(kimiHome(), "hooks", SHIM_FILE);
505
+ }
506
+ function kimiLegacyHookScriptPath() {
507
+ return path4.join(kimiHome(), "hooks", LEGACY_SHIM_FILE);
508
+ }
509
+ function kimiSkillDir() {
510
+ return path4.join(kimiHome(), "skills", "token-goat");
511
+ }
512
+ function kimiSkillPath() {
513
+ return path4.join(kimiSkillDir(), "SKILL.md");
514
+ }
515
+ function readKimiConfig(p, opts = {}) {
516
+ const refuse = (problem, detail) => new KimiConfigParseError(
517
+ (opts.command === "uninstall" ? `Kimi Code config file '${p}' is unreadable: it ${problem}. Uninstall left it untouched, along with the hook shim it may still name, the AGENTS.md block, the skill and the file's backups; fix the file and run uninstall again.` : `Kimi Code config file '${p}' ${problem}. Fix or back up the file before running install.`) + (detail === void 0 ? "" : ` (${detail})`)
518
+ );
519
+ let raw;
520
+ try {
521
+ raw = fs4.readFileSync(p, "utf8");
522
+ } catch (e) {
523
+ const code = e.code;
524
+ if (opts.strict === true && code !== "ENOENT" && code !== "ENOTDIR") throw refuse("exists but cannot be read", code ?? extractErrorMessage(e));
525
+ return {};
526
+ }
527
+ try {
528
+ return parse(raw);
529
+ } catch (e) {
530
+ if (opts.strict === true) throw refuse("exists but contains invalid TOML", extractErrorMessage(e));
531
+ return {};
532
+ }
533
+ }
534
+ var KIMI_COMMAND_MARKER = "token-goat";
535
+ var KIMI_MARKER_PATTERN = anchoredMarkerPattern(KIMI_COMMAND_MARKER);
536
+ function isKimiTokenGoatCommand(command) {
537
+ return typeof command === "string" && KIMI_MARKER_PATTERN.test(command);
538
+ }
539
+ var AGENTS_BEGIN = "<!-- token-goat-kimi-begin -->";
540
+ var AGENTS_END = "<!-- token-goat-kimi-end -->";
541
+ function buildKimiAgentsBlock() {
542
+ return buildGuidanceBlock({
543
+ beginMarker: AGENTS_BEGIN,
544
+ endMarker: AGENTS_END,
545
+ fallbackToolClause: "Kimi Code's native `Read`, `Grep`, `Glob`, and `ReadMediaFile` tools (shell commands like `cat`/`type` run inside `Bash`)",
546
+ gdrive: loadConfig().gdrive.enabled
547
+ });
548
+ }
549
+ function writeKimiAgentsBlock(p) {
550
+ return upsertDelimitedBlock(p, AGENTS_BEGIN, AGENTS_END, buildKimiAgentsBlock());
551
+ }
552
+ function stripKimiAgentsBlock(p) {
553
+ return stripDelimitedBlock(p, AGENTS_BEGIN, AGENTS_END);
554
+ }
555
+ function kimiSkillContent() {
556
+ const gdrive = loadConfig().gdrive.enabled;
557
+ const frontmatter = [
558
+ "---",
559
+ "name: token-goat",
560
+ skillDescriptionLine(gdrive),
561
+ "---"
562
+ ].join("\n");
563
+ const body = buildGuidanceBody("Kimi Code's own Read, Grep, and Glob preference rules", { gdrive });
564
+ return `${frontmatter}
565
+
566
+ ${body}
567
+ `;
568
+ }
569
+ function writeKimiSkill() {
570
+ return writeIfDifferent(kimiSkillPath(), kimiSkillContent(), true);
571
+ }
572
+ function kimiArtifactPaths() {
573
+ return [kimiConfigPath(), kimiAgentsPath(), kimiSkillPath(), kimiHookScriptPath(), kimiLegacyHookScriptPath()];
574
+ }
575
+ function installKimi() {
576
+ return recordCreatedBy(kimiArtifactPaths(), installKimiFiles);
577
+ }
578
+ function installKimiFiles() {
579
+ const configPath = kimiConfigPath();
580
+ const agentsPath = kimiAgentsPath();
581
+ const scriptPath = kimiHookScriptPath();
582
+ const desired = KIMI_HOOK_EVENTS.map((event) => ({
583
+ event,
584
+ command: kimiHookCommandFor(scriptPath, KIMI_EVENT_ARG[event] ?? "")
585
+ }));
586
+ for (const { command } of process.platform === "win32" ? desired : []) {
587
+ const span = hookCommandRewrittenSpan(command, "cmd");
588
+ if (span !== void 0) {
589
+ throw new HookCommandRewriteError(`Kimi Code runs hook commands through cmd.exe, which replaces ${span} with the value of the environment variable of that name whenever one is set, before it reads any quotes, so the hook command token-goat would write (${command}) would not run token-goat. Nothing was written. Move the path that contains "${span}" (the Kimi Code home, token-goat's install directory, or Node's) to one without a %...% pair, then run 'token-goat install --kimi' again.`);
590
+ }
591
+ }
592
+ ensureDirSync(path4.dirname(scriptPath));
593
+ atomicWriteText(scriptPath, KIMI_HOOK_SCRIPT);
594
+ atomicWriteText(kimiLegacyHookScriptPath(), legacyShimForwarder(""));
595
+ const config = readKimiConfig(configPath, { strict: true });
596
+ const existing = Array.isArray(config.hooks) ? config.hooks : [];
597
+ const foreign = existing.filter((h) => !isKimiTokenGoatCommand(h?.command));
598
+ const ours = existing.filter((h) => isKimiTokenGoatCommand(h?.command));
599
+ const hooksChanged = JSON.stringify(ours) !== JSON.stringify(desired);
600
+ const agentsChanged = writeKimiAgentsBlock(agentsPath);
601
+ const skillChanged = writeKimiSkill();
602
+ if (hooksChanged) {
603
+ config.hooks = [...foreign, ...desired];
604
+ ensureDirSync(path4.dirname(configPath));
605
+ backupFile(configPath);
606
+ writeConfigText(configPath, stringify(config));
607
+ }
608
+ return {
609
+ configPath,
610
+ agentsPath,
611
+ hookScriptPath: scriptPath,
612
+ skillPath: kimiSkillPath(),
613
+ alreadyInstalled: !hooksChanged && !agentsChanged && !skillChanged
614
+ };
615
+ }
616
+ function uninstallKimi() {
617
+ const configPath = kimiConfigPath();
618
+ const config = readKimiConfig(configPath, { strict: true, command: "uninstall" });
619
+ let removed = false;
620
+ if (Array.isArray(config.hooks)) {
621
+ const kept = config.hooks.filter((h) => !isKimiTokenGoatCommand(h?.command));
622
+ if (kept.length !== config.hooks.length) {
623
+ if (kept.length === 0) {
624
+ delete config.hooks;
625
+ } else {
626
+ config.hooks = kept;
627
+ }
628
+ backupFile(configPath);
629
+ writeConfigText(configPath, stringify(config));
630
+ removed = true;
631
+ }
632
+ }
633
+ if (stripKimiAgentsBlock(kimiAgentsPath())) removed = true;
634
+ try {
635
+ if (fs4.existsSync(kimiSkillDir())) {
636
+ removeCreatedBackups(kimiSkillPath());
637
+ fs4.rmSync(kimiSkillDir(), { recursive: true, force: true });
638
+ removed = true;
639
+ }
640
+ } catch {
641
+ }
642
+ for (const p of [kimiHookScriptPath(), kimiLegacyHookScriptPath()]) {
643
+ try {
644
+ if (fs4.existsSync(p)) {
645
+ fs4.rmSync(p, { force: true });
646
+ removed = true;
647
+ }
648
+ } catch {
649
+ }
650
+ }
651
+ removeCreatedBackups(configPath);
652
+ removeCreatedTree(kimiArtifactPaths());
653
+ return removed;
654
+ }
655
+ function kimiHookCommandFor(scriptPath, eventArg, opts = {}) {
656
+ const bin = nativeHookBinary(process.argv[1], opts);
657
+ const native = bin === void 0 ? void 0 : nativeHookCommandLine(process.platform === "win32" ? "cmd" : "sh", bin, "kimi", scriptPath, eventArg);
658
+ return native ?? hookCommandFor(scriptPath, eventArg);
659
+ }
660
+ function wiredKimiHookWords() {
661
+ const hooks = readKimiConfig(kimiConfigPath()).hooks;
662
+ if (!Array.isArray(hooks)) return [];
663
+ const scriptPath = kimiHookScriptPath();
664
+ const expected = new Set(KIMI_HOOK_EVENTS.map((e) => kimiHookCommandFor(scriptPath, KIMI_EVENT_ARG[e] ?? "", { sync: false })));
665
+ return hooks.filter((h) => isKimiTokenGoatCommand(h?.command)).map((h) => ({ words: splitHookCommand(h.command, process.platform === "win32" ? "cmd" : "sh"), current: expected.has(h.command) }));
666
+ }
667
+ function isKimiInstalled() {
668
+ const config = readKimiConfig(kimiConfigPath());
669
+ if (!Array.isArray(config.hooks)) return false;
670
+ const scriptPath = kimiHookScriptPath();
671
+ for (const event of KIMI_HOOK_EVENTS) {
672
+ const expected = kimiHookCommandFor(scriptPath, KIMI_EVENT_ARG[event] ?? "", { sync: false });
673
+ if (!config.hooks.some((h) => h?.event === event && h?.command === expected)) return false;
674
+ }
675
+ return true;
676
+ }
677
+
678
+ // src/bridges/pi_install.ts
679
+ init_define_import_meta_env();
680
+ import * as fs5 from "node:fs";
681
+ import * as os4 from "node:os";
682
+ import * as path5 from "node:path";
683
+
684
+ // src/bridges/pi.ts
685
+ init_define_import_meta_env();
686
+ var PI_EXTENSION_SCRIPT = `// token-goat bridge extension for pi (pi-coding-agent)
687
+ // Bridges pi's extension events to token-goat's subprocess hook protocol.
688
+ // https://github.com/DFKHelper/token-goat
689
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
690
+ import { spawnSync } from "node:child_process"
691
+ import crypto from "node:crypto";
692
+ import * as fs from "node:fs";
693
+ import * as os from "node:os";
694
+ import * as path from "node:path";
695
+ import { fileURLToPath, pathToFileURL } from "node:url";
696
+
697
+ // pi built-in tool names -> token-goat internal tool names. powershell is pi's Windows shell twin of bash -- same input schema (PowerShellToolInput = BashToolInput, i.e. command/timeout) and registered under its own name (badlogic/pi-mono packages/coding-agent/src/core/tools/powershell.ts + tools/index.ts's ToolName union). Unmapped, every shell command in a powershell-tool pi session bypassed the Bash hooks entirely, the same shape as the Copilot shim's bash/powershell pairing.
698
+ const TOOL_TO_TG: Record<string, string> = {
699
+ read: "Read",
700
+ bash: "Bash",
701
+ powershell: "Bash",
702
+ edit: "Edit",
703
+ write: "Write",
704
+ grep: "Grep",
705
+ find: "Glob",
706
+ };
707
+
708
+ // pi tool args (camelCase/short) -> token-goat snake_case tool_input keys
709
+ const ARGS_TO_TG: Record<string, Record<string, string>> = {
710
+ read: { path: "file_path", offset: "offset", limit: "limit" },
711
+ bash: { command: "command", timeout: "timeout" },
712
+ powershell: { command: "command", timeout: "timeout" },
713
+ edit: { path: "file_path" },
714
+ write: { path: "file_path" },
715
+ grep: { pattern: "pattern", path: "path" },
716
+ find: { pattern: "pattern", path: "path" },
717
+ };
718
+
719
+ // Tools with a pre-hook whose output shape this extension can act on: deny ({block}), updatedInput (in-place arg rewrite), or the Read image-shrink materialization. Glob's pre handler (preGlobDedupHandler in hooks_glob.ts -- the old claim here that Glob has no pre handler was stale) emits only an advisory contextOutput hint, which pi's tool_call contract has no channel for (see the module docblock), so calling it would cost a hook call per find only to drop the answer; it stays excluded for that reason.
720
+ const PRE_HOOK_TOOLS = new Set(["Read", "Grep", "Bash", "WebFetch"]);
721
+
722
+ // resolveEntryPath reads a sidecar JSON file (token-goat-entry.json, written by
723
+ // installPi next to this extension file) containing the absolute path to the
724
+ // token-goat CLI entry that was running at install time. Unlike Codex/Copilot,
725
+ // this extension has no per-invocation command line to bake a path into (pi
726
+ // loads it once as a module, then calls into it via pi.on(...) handlers), so
727
+ // the sidecar file is the install-time channel for this value instead. Returns
728
+ // undefined (triggering the PATH-based fallback below) if the sidecar is
729
+ // missing, unreadable, or points at a path that no longer exists -- e.g. an
730
+ // older install predating this file, or a moved/removed token-goat install.
731
+ function resolveEntryPath(): string | undefined {
732
+ try {
733
+ const here = path.dirname(fileURLToPath(import.meta.url));
734
+ const sidecarPath = path.join(here, "token-goat-entry.json");
735
+ const parsed = JSON.parse(fs.readFileSync(sidecarPath, "utf8")) as { entryPath?: unknown };
736
+ if (typeof parsed.entryPath === "string" && parsed.entryPath && fs.existsSync(parsed.entryPath)) {
737
+ return parsed.entryPath;
738
+ }
739
+ } catch {
740
+ // fall through to undefined
741
+ }
742
+ return undefined;
743
+ }
744
+
745
+ // import()s dist/token-goat-hook.mjs (a sibling of the baked token-goat entry path,
746
+ // built with zero load-time side effects -- unlike the CLI entry, which runs the full
747
+ // argv-parsing CLI as a side effect of being loaded) and returns its exported
748
+ // relayInProcess() function, so callHook can call straight into the hook registry
749
+ // instead of spawnSync-ing a whole second node process for every single hook call in
750
+ // this long-lived agent process. Cached after the first successful resolution (the
751
+ // module itself is cached by Node's ESM loader on repeat import() calls of the same
752
+ // URL, so this mainly avoids repeat fs.existsSync/sidecar-read overhead). Returns
753
+ // undefined (triggering the spawnSync fallback below) when entryPath is absent, the
754
+ // sibling file doesn't exist (an install predating this file), or anything else goes
755
+ // wrong -- this must never throw.
756
+ type RelayInProcessFn = (event: string, payload: unknown, harnessWaitMs?: number, opts?: { elapsedMs?: () => number }) => Promise<string>;
757
+ let cachedRelayInProcess: RelayInProcessFn | undefined;
758
+ async function resolveRelayInProcess(): Promise<RelayInProcessFn | undefined> {
759
+ if (cachedRelayInProcess) return cachedRelayInProcess;
760
+ const entryPath = resolveEntryPath();
761
+ if (!entryPath) return undefined;
762
+ try {
763
+ const hookLibPath = path.join(path.dirname(entryPath), "token-goat-hook.mjs");
764
+ if (!fs.existsSync(hookLibPath)) return undefined;
765
+ const mod = (await import(pathToFileURL(hookLibPath).href)) as { relayInProcess?: unknown };
766
+ if (typeof mod.relayInProcess !== "function") return undefined;
767
+ cachedRelayInProcess = mod.relayInProcess as RelayInProcessFn;
768
+ return cachedRelayInProcess;
769
+ } catch {
770
+ return undefined;
771
+ }
772
+ }
773
+
774
+ function callHookViaSpawn(event: string, payload: Record<string, unknown>): Record<string, unknown> | null {
775
+ try {
776
+ // TOKEN_GOAT_HARNESS_OVERRIDE=pi guarantees detectHarness() resolves to
777
+ // 'pi' for every call this bridge makes, instead of relying on a guessed
778
+ // ambient env var pi-coding-agent may or may not set.
779
+ //
780
+ // Invoking "token-goat" as a bare command here depends on PATH resolution
781
+ // (the npm global bin being on whatever PATH pi-coding-agent's own process
782
+ // inherits) -- the same class of single-point-of-failure fixed for the
783
+ // Codex/Copilot CLI bridges' hook commands. When resolveEntryPath() finds a
784
+ // baked install-time path, invoke it directly via process.execPath instead,
785
+ // sidestepping PATH entirely; otherwise fall back to the old PATH-based
786
+ // lookup (e.g. an extension installed before this fix).
787
+ const entryPath = resolveEntryPath();
788
+ const r = entryPath
789
+ ? spawnSync(process.execPath, [entryPath, "hook", event], {
790
+ input: JSON.stringify(payload),
791
+ encoding: "utf8",
792
+ timeout: 3000,
793
+ killSignal: "SIGKILL",
794
+ windowsHide: true,
795
+ maxBuffer: 32 * 1024 * 1024,
796
+ env: { ...process.env, TOKEN_GOAT_HARNESS_OVERRIDE: "pi" },
797
+ })
798
+ : spawnSync('token-goat hook ' + event, {
799
+ input: JSON.stringify(payload),
800
+ encoding: "utf8",
801
+ timeout: 3000,
802
+ killSignal: "SIGKILL",
803
+ shell: true,
804
+ windowsHide: true,
805
+ maxBuffer: 32 * 1024 * 1024,
806
+ env: { ...process.env, TOKEN_GOAT_HARNESS_OVERRIDE: "pi" },
807
+ });
808
+ if (r.error) return null;
809
+ const out = r.stdout?.trim();
810
+ if (!out) return null;
811
+ return JSON.parse(out) as Record<string, unknown>;
812
+ } catch {
813
+ return null;
814
+ }
815
+ }
816
+
817
+ // Tries the in-process hook call first (resolveRelayInProcess above), which avoids
818
+ // spawning a second node process altogether for every single tool call in this
819
+ // long-lived agent process. Falls back to callHookViaSpawn (the original
820
+ // spawnSync-based path, now with a 3000ms timeout/killSignal so token-goat degrades to
821
+ // its own fail-open null rather than being force-killed by pi's own hook timeout
822
+ // budget, ~5000ms) when the in-process path is unavailable or throws.
823
+ async function callHook(event: string, payload: Record<string, unknown>): Promise<Record<string, unknown> | null> {
824
+ // Timed from here, not from process start: this host lives far longer than one call, so the hook library's default clock (process age) would record the host's uptime as this call's duration.
825
+ const start = performance.now();
826
+ const relay = await resolveRelayInProcess();
827
+ if (relay) {
828
+ try {
829
+ process.env.TOKEN_GOAT_HARNESS_OVERRIDE = "pi";
830
+ const out = await relay(event, payload, undefined, { elapsedMs: () => performance.now() - start });
831
+ const trimmed = out?.trim();
832
+ if (!trimmed) return null;
833
+ return JSON.parse(trimmed) as Record<string, unknown>;
834
+ } catch {
835
+ // fall through to the spawnSync fallback below
836
+ }
837
+ }
838
+ return callHookViaSpawn(event, payload);
839
+ }
840
+
841
+ function reverseArgMap(tool: string): Record<string, string> {
842
+ const fwd = ARGS_TO_TG[tool] ?? {};
843
+ return Object.fromEntries(Object.entries(fwd).map(([piKey, tgKey]) => [tgKey, piKey]));
844
+ }
845
+
846
+ const UNICODE_SPACES = new RegExp("[" + [0xa0, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200a, 0x202f, 0x205f, 0x3000].map((c) => String.fromCharCode(c)).join("") + "]", "g");
847
+
848
+ function fileExists(p: string): boolean {
849
+ try {
850
+ fs.accessSync(p);
851
+ return true;
852
+ } catch {
853
+ return false;
854
+ }
855
+ }
856
+
857
+ // The absolute path pi will open for a read/edit/write "path" argument. pi rewrites the argument before resolving it (pi-coding-agent 0.99.1, core/tools/path-utils.js resolveToCwd over utils/paths.js normalizePath), in this order: trim, unicode spaces to plain spaces, drop one leading "@", on Windows turn a Git Bash, Cygwin or WSL drive path (/c/x, /cygdrive/c/x, /mnt/c/x) into C:/x, expand "~", convert a file:// URL, then resolve against the session cwd. A read of a missing file also tries the macOS screenshot spellings (resolveReadPath). token-goat was sent the argument as written, so "@src/a.ts" or "~/notes.md" named a file that does not exist: a re-read went unrecognised and an edit never reached the index.
858
+ function resolvePiPath(raw: string, base: string, isRead: boolean): string {
859
+ let p = raw.trim().replace(UNICODE_SPACES, " ");
860
+ if (p.startsWith("@")) p = p.slice(1);
861
+ if (process.platform === "win32" && p.startsWith("/") && !p.startsWith("//") && !p.includes(String.fromCharCode(92))) {
862
+ const m = p.match(/^[/](?:mnt[/]|cygdrive[/])?([a-z])(?:[/](.*))?$/i);
863
+ if (m) p = m[1].toUpperCase() + ":/" + (m[2] ?? "");
864
+ }
865
+ if (p === "~") p = os.homedir();
866
+ else if (p.startsWith("~/") || (process.platform === "win32" && p.startsWith("~" + String.fromCharCode(92)))) p = path.join(os.homedir(), p.slice(2));
867
+ else if (/^file:[/][/]/.test(p)) {
868
+ try { p = fileURLToPath(p); } catch {}
869
+ }
870
+ const resolved = path.resolve(base, p);
871
+ if (!isRead || fileExists(resolved)) return resolved;
872
+ const amPm = resolved.replace(/ (AM|PM)[.]/gi, String.fromCharCode(0x202f) + "$1.");
873
+ const nfd = resolved.normalize("NFD");
874
+ const curly = (s: string) => s.replace(/'/g, String.fromCharCode(0x2019));
875
+ for (const variant of [amPm, nfd, curly(resolved), curly(nfd)]) {
876
+ if (variant !== resolved && fileExists(variant)) return variant;
877
+ }
878
+ return resolved;
879
+ }
880
+
881
+ function toToolInput(tool: string, input: Record<string, unknown>, base: string): Record<string, unknown> {
882
+ const argMap = ARGS_TO_TG[tool] ?? {};
883
+ const out: Record<string, unknown> = {};
884
+ for (const [piKey, tgKey] of Object.entries(argMap)) {
885
+ if (input[piKey] !== undefined) out[tgKey] = input[piKey];
886
+ }
887
+ if (typeof out["file_path"] === "string") out["file_path"] = resolvePiPath(out["file_path"] as string, base, tool === "read");
888
+ return out;
889
+ }
890
+
891
+ // Pull the additionalContext / systemMessage string out of a hook response,
892
+ // whichever shape it came back as (see the response-contract note in this
893
+ // module's header comment).
894
+ function extractContext(resp: Record<string, unknown>): string | undefined {
895
+ const hso = resp["hookSpecificOutput"] as Record<string, unknown> | undefined;
896
+ if (hso && typeof hso["additionalContext"] === "string") return hso["additionalContext"] as string;
897
+ if (typeof resp["systemMessage"] === "string") return resp["systemMessage"] as string;
898
+ return undefined;
899
+ }
900
+
901
+ function extractUpdatedInput(resp: Record<string, unknown>): Record<string, unknown> | undefined {
902
+ const hso = resp["hookSpecificOutput"] as Record<string, unknown> | undefined;
903
+ return hso ? (hso["updatedInput"] as Record<string, unknown> | undefined) : undefined;
904
+ }
905
+
906
+ // Decode a token-goat image-shrink additionalContext payload
907
+ // ("<summary>\\ndata:image/<fmt>;base64,<data>") into a real file on disk,
908
+ // since pi's tool_call handler has no context-injection channel -- only
909
+ // in-place arg mutation. Returns undefined (leaving the read path untouched)
910
+ // if the context isn't a shrink payload or anything goes wrong writing it.
911
+ // Best-effort sweep of previously materialized shrunk copies in the OS temp dir: typed twin of pruneMaterializedShrinks in shrink_block.ts (MATERIALIZE_SHRUNK_IMAGE_JS), which documents why pi keeps its own copy. The temp file only needs to outlive the single tool call whose path 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.
912
+ const MATERIALIZED_SHRINK_MAX_AGE_MS = 60 * 60 * 1000;
913
+ let lastMaterializedShrinkSweepAtMs = 0;
914
+ function pruneMaterializedShrinks(): void {
915
+ const now = Date.now();
916
+ if (now - lastMaterializedShrinkSweepAtMs < MATERIALIZED_SHRINK_MAX_AGE_MS) return;
917
+ lastMaterializedShrinkSweepAtMs = now;
918
+ try {
919
+ const dir = os.tmpdir();
920
+ for (const file of fs.readdirSync(dir)) {
921
+ if (!file.startsWith("token-goat-shrink-")) continue;
922
+ const full = path.join(dir, file);
923
+ try {
924
+ const st = fs.statSync(full);
925
+ if (st.isFile() && now - st.mtimeMs > MATERIALIZED_SHRINK_MAX_AGE_MS) fs.unlinkSync(full);
926
+ } catch {
927
+ // Best-effort per-file cleanup; one bad stat/unlink must not abort the sweep.
928
+ }
929
+ }
930
+ } catch {
931
+ // Best-effort; a readdir failure must never break the materialization below.
932
+ }
933
+ }
934
+
935
+ function materializeShrunkImage(context: string | undefined): string | undefined {
936
+ if (typeof context !== "string") return undefined;
937
+ const idx = context.indexOf("data:image/");
938
+ if (idx === -1) return undefined;
939
+ const match = /^data:image\\/([a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)$/.exec(context.slice(idx).trim());
940
+ if (!match) return undefined;
941
+ try {
942
+ pruneMaterializedShrinks();
943
+ const buf = Buffer.from(match[2], "base64");
944
+ const name = \`token-goat-shrink-\${process.pid}-\${Date.now()}-\${globalThis.crypto.randomUUID()}.\${match[1]}\`;
945
+ const file = path.join(os.tmpdir(), name);
946
+ // Owner-only: the copy is of a workspace image and sits in the shared temp dir.
947
+ fs.writeFileSync(file, buf, { mode: 0o600 });
948
+ return file;
949
+ } catch {
950
+ return undefined;
951
+ }
952
+ }
953
+
954
+ export default function (pi: ExtensionAPI) {
955
+ // Stable per-session id derived from pi's session file (filesystem-safe), or
956
+ // a process-scoped fallback for ephemeral sessions. Recomputed on every
957
+ // session_start (new / resume / fork all re-fire it).
958
+ let sessionId = \`pi-\${process.pid}\`;
959
+ let cwd = process.cwd();
960
+ // Manifest captured at session_before_compact, injected after compaction.
961
+ let pendingManifest: string | undefined;
962
+
963
+ // No forwarding callHook for session_start here (there used to be one):
964
+ // token-goat retired the session_start hook -- it only ever reached a
965
+ // permanent no-op handler (see the removal in src/hooks_session.ts and
966
+ // src/types.ts). This subscription is kept regardless, because it's pi's
967
+ // own lifecycle event and is still needed to refresh sessionId/cwd for
968
+ // every other bridged call below.
969
+ pi.on("session_start", (_event, ctx) => {
970
+ cwd = ctx.cwd ?? process.cwd();
971
+ const file = ctx.sessionManager?.getSessionFile?.();
972
+ sessionId = file ? \`pi-\${file.replace(/[^A-Za-z0-9._-]/g, "_")}\` : \`pi-\${process.pid}\`;
973
+ });
974
+
975
+ pi.on("tool_call", async (event, ctx) => {
976
+ const tg = TOOL_TO_TG[event.toolName];
977
+ if (!tg || !PRE_HOOK_TOOLS.has(tg)) return;
978
+
979
+ const input = event.input as Record<string, unknown>;
980
+ const resp = await callHook("pre_tool_use", {
981
+ session_id: sessionId,
982
+ tool_name: tg,
983
+ tool_input: toToolInput(event.toolName, input, ctx?.cwd ?? cwd),
984
+ cwd: ctx?.cwd ?? cwd,
985
+ });
986
+ if (!resp) return;
987
+
988
+ // Deny: block the tool call with a reason (e.g. confirmed re-read).
989
+ if (resp["decision"] === "block") {
990
+ return {
991
+ block: true,
992
+ reason: (resp["reason"] as string) ?? "blocked by token-goat",
993
+ };
994
+ }
995
+
996
+ // Rewrite: update tool args in place (e.g. compressed bash command).
997
+ // token-goat returns its own snake_case keys; map them back.
998
+ const updated = extractUpdatedInput(resp);
999
+ if (updated) {
1000
+ const rev = reverseArgMap(event.toolName);
1001
+ for (const [tgKey, val] of Object.entries(updated)) {
1002
+ input[rev[tgKey] ?? tgKey] = val;
1003
+ }
1004
+ return;
1005
+ }
1006
+
1007
+ // Image shrink has no context channel here -- translate it into a
1008
+ // rewritten path pointing at a materialized shrunk copy instead.
1009
+ if (tg === "Read") {
1010
+ const shrunkPath = materializeShrunkImage(extractContext(resp));
1011
+ if (shrunkPath) input["path"] = shrunkPath;
1012
+ }
1013
+ });
1014
+
1015
+ pi.on("tool_result", async (event, ctx) => {
1016
+ const tg = TOOL_TO_TG[event.toolName];
1017
+ if (!tg) return;
1018
+ // event.content is tool_result's real output field (ToolResultEventBase.content:
1019
+ // (TextContent | ImageContent)[] -- verified against pi's own
1020
+ // core/extensions/types.ts). Join the text blocks into a single string so
1021
+ // extractReadOutput's truncation-marker detection (which only recognizes a
1022
+ // string tool_response.output) can see it, mirroring opencode.ts's
1023
+ // tool_response: { output } shape.
1024
+ const contentBlocks = Array.isArray(event.content) ? event.content : [];
1025
+ const output = contentBlocks
1026
+ .filter((c: { type?: string }) => c && c.type === "text")
1027
+ .map((c: { text?: string }) => c.text ?? "")
1028
+ .join("\\n");
1029
+ const resp = await callHook("post_tool_use", {
1030
+ session_id: sessionId,
1031
+ tool_name: tg,
1032
+ tool_input: toToolInput(event.toolName, (event.input ?? {}) as Record<string, unknown>, ctx?.cwd ?? cwd),
1033
+ cwd: ctx?.cwd ?? cwd,
1034
+ tool_response: { output },
1035
+ });
1036
+ if (!resp) return;
1037
+
1038
+ // rewriteOutput: replace the tool result the model sees. pi's tool_result handler CAN modify the result: its own types declare "Fired after a tool executes. Can modify result." over ToolResultEvent and a ToolResultEventResult of { content, details, isError, usage }, and the shipped runtime applies it (dist/core/extensions/runner.js emitToolResult assigns handlerResult.content onto the event, and dist/core/agent-session.js afterToolCall returns hookResult.content as the tool result content). Without this, injection fencing, secret redaction and output compression were computed and thrown away on every pi session, the same whitelist-shaped drop fixed for opencode.
1039
+ const hso = resp["hookSpecificOutput"] as Record<string, unknown> | undefined;
1040
+ const updatedToolOutput = hso && typeof hso["updatedToolOutput"] === "string" ? (hso["updatedToolOutput"] as string) : undefined;
1041
+ if (updatedToolOutput === undefined) return;
1042
+
1043
+ // Replace only the text blocks, in place, and keep every non-text block (images) untouched: emitToolResult overwrites content wholesale, so anything not carried here is dropped from what the model sees. details/isError/usage are deliberately not returned, since runner.js only overwrites the fields a handler actually provides.
1044
+ const rewritten: typeof contentBlocks = [];
1045
+ let replaced = false;
1046
+ for (const block of contentBlocks) {
1047
+ const isText = Boolean(block) && (block as { type?: string }).type === "text";
1048
+ if (!isText) {
1049
+ rewritten.push(block);
1050
+ continue;
1051
+ }
1052
+ if (replaced) continue;
1053
+ replaced = true;
1054
+ rewritten.push({ type: "text", text: updatedToolOutput });
1055
+ }
1056
+ if (!replaced) rewritten.push({ type: "text", text: updatedToolOutput });
1057
+ return { content: rewritten };
1058
+ });
1059
+
1060
+ // Compaction: pi's session_before_compact REPLACES the summary rather than
1061
+ // appending to it (unlike opencode's additive output.context). So capture the
1062
+ // token-goat manifest here, let pi build its own summary, then inject the
1063
+ // manifest as a post-compaction message that survives into the new context
1064
+ // window. This preserves the edited-file / symbol manifest within pi's
1065
+ // replace-only compaction model.
1066
+ pi.on("session_before_compact", async (_event, _ctx) => {
1067
+ const resp = await callHook("pre_compact", { session_id: sessionId, trigger: "auto" });
1068
+ pendingManifest = resp?.["systemMessage"] as string | undefined;
1069
+ });
1070
+
1071
+ pi.on("session_compact", async (_event, _ctx) => {
1072
+ if (pendingManifest) {
1073
+ pi.sendMessage(
1074
+ { customType: "token-goat-manifest", content: pendingManifest, display: false },
1075
+ { deliverAs: "nextTurn" },
1076
+ );
1077
+ pendingManifest = undefined;
1078
+ }
1079
+ });
1080
+ }
1081
+ `;
1082
+
1083
+ // src/bridges/pi_install.ts
1084
+ function piGlobalExtensionPath() {
1085
+ return path5.join(os4.homedir(), ".pi", "agent", "extensions", "token-goat.ts");
1086
+ }
1087
+ function piLocalExtensionPath() {
1088
+ return path5.join(process.cwd(), ".pi", "extensions", "token-goat.ts");
1089
+ }
1090
+ function piExtensionPath(opts = {}) {
1091
+ return opts.local === true ? piLocalExtensionPath() : piGlobalExtensionPath();
1092
+ }
1093
+ function piEntrySidecarPath(opts = {}) {
1094
+ return path5.join(path5.dirname(piExtensionPath(opts)), "token-goat-entry.json");
1095
+ }
1096
+ function installPi(opts = {}) {
1097
+ return withInstallScope(projectScopeRoot(opts), () => {
1098
+ const extensionPath = piExtensionPath(opts);
1099
+ const { alreadyInstalled } = recordCreatedBy([extensionPath, piEntrySidecarPath(opts)], () => installSingleFilePlugin(extensionPath, piEntrySidecarPath(opts), PI_EXTENSION_SCRIPT));
1100
+ return { extensionPath, alreadyInstalled };
1101
+ });
1102
+ }
1103
+ function uninstallPiScope(opts) {
1104
+ return withInstallScope(projectScopeRoot(opts), () => {
1105
+ const removed = uninstallSingleFilePlugin(piExtensionPath(opts), piEntrySidecarPath(opts));
1106
+ removeCreatedTree([piExtensionPath(opts), piEntrySidecarPath(opts)]);
1107
+ return removed;
1108
+ });
1109
+ }
1110
+ function uninstallPi(opts = {}) {
1111
+ if (opts.local === true) {
1112
+ return uninstallPiScope({ local: true });
1113
+ }
1114
+ const globalRemoved = uninstallPiScope({ local: false });
1115
+ const localRemoved = uninstallPiScope({ local: true });
1116
+ return globalRemoved || localRemoved;
1117
+ }
1118
+ function isPiInstalled(opts = {}) {
1119
+ return fs5.existsSync(piExtensionPath(opts));
1120
+ }
1121
+
1122
+ // src/bridges/opencode_install.ts
1123
+ init_define_import_meta_env();
1124
+ import * as fs6 from "node:fs";
1125
+ import * as os5 from "node:os";
1126
+ import * as path6 from "node:path";
1127
+
1128
+ // src/bridges/opencode.ts
1129
+ init_define_import_meta_env();
1130
+
1131
+ // src/bridges/relay_block.ts
1132
+ init_define_import_meta_env();
1133
+ var BRIDGE_RELAY_JS = `// resolveEntryPath reads a sidecar JSON file (token-goat-entry.json, written by
1134
+ // the install step next to this plugin file) containing the absolute path to
1135
+ // the token-goat CLI entry that was running at install time. Unlike
1136
+ // Codex/Copilot's generated hook commands, this plugin has no per-invocation
1137
+ // command line to bake a path into (the host loads it once as a module), so
1138
+ // callHook reads this sidecar at runtime instead, to invoke that entry
1139
+ // directly via process.execPath rather than depending on PATH resolution for
1140
+ // a bare "token-goat" lookup -- the same single-point-of-failure class fixed
1141
+ // for the Codex/Copilot CLI bridges' hook commands and pi's extension
1142
+ // (resolveEntryPath). Returns undefined (triggering the PATH-based
1143
+ // shell:true fallback below) if the sidecar is missing, unreadable, or
1144
+ // points at a path that no longer exists.
1145
+ function resolveEntryPath() {
1146
+ try {
1147
+ const here = path.dirname(fileURLToPath(import.meta.url));
1148
+ const sidecarPath = path.join(here, "token-goat-entry.json");
1149
+ const parsed = JSON.parse(fs.readFileSync(sidecarPath, "utf8"));
1150
+ if (typeof parsed.entryPath === "string" && parsed.entryPath && fs.existsSync(parsed.entryPath)) {
1151
+ return parsed.entryPath;
1152
+ }
1153
+ } catch {
1154
+ // fall through to undefined
1155
+ }
1156
+ return undefined;
1157
+ }
1158
+
1159
+ // import()s dist/token-goat-hook.mjs (a sibling of the baked token-goat entry path,
1160
+ // built with zero load-time side effects -- unlike the CLI entry, which runs the full
1161
+ // argv-parsing CLI as a side effect of being loaded) and returns its exported
1162
+ // relayInProcess() function, so callHook can call straight into the hook registry
1163
+ // instead of spawnSync-ing a whole second node process for every single hook call in
1164
+ // this long-lived host process. Cached after the first successful resolution (the
1165
+ // module itself is cached by Node's ESM loader on repeat import() calls of the same
1166
+ // URL, so this mainly avoids repeat fs.existsSync/sidecar-read overhead). Returns
1167
+ // undefined (triggering the spawnSync fallback below) when entryPath is absent, the
1168
+ // sibling file doesn't exist (an install predating this file), or anything else goes
1169
+ // wrong -- this must never throw.
1170
+ let cachedRelayInProcess;
1171
+ async function resolveRelayInProcess() {
1172
+ if (cachedRelayInProcess) return cachedRelayInProcess;
1173
+ const entryPath = resolveEntryPath();
1174
+ if (!entryPath) return undefined;
1175
+ try {
1176
+ const hookLibPath = path.join(path.dirname(entryPath), "token-goat-hook.mjs");
1177
+ if (!fs.existsSync(hookLibPath)) return undefined;
1178
+ const mod = await import(pathToFileURL(hookLibPath).href);
1179
+ if (typeof mod.relayInProcess !== "function") return undefined;
1180
+ cachedRelayInProcess = mod.relayInProcess;
1181
+ return cachedRelayInProcess;
1182
+ } catch {
1183
+ return undefined;
1184
+ }
1185
+ }
1186
+
1187
+ function callHookViaSpawn(event, payload) {
1188
+ try {
1189
+ // Invoking "token-goat" as a bare command here depends on PATH resolution
1190
+ // (the npm global bin being on whatever PATH the host process inherits) --
1191
+ // on Windows a global npm install resolves "token-goat" to a .cmd/.ps1
1192
+ // shim, which spawnSync cannot exec without shell: true. When
1193
+ // resolveEntryPath() finds a baked install-time path, invoke it directly
1194
+ // via process.execPath instead, sidestepping PATH entirely; otherwise
1195
+ // fall back to the old PATH-based lookup with shell: true so a .cmd/.ps1
1196
+ // shim still resolves (e.g. an install predating this fix).
1197
+ const entryPath = resolveEntryPath();
1198
+ const r = entryPath
1199
+ ? spawnSync(process.execPath, [entryPath, "hook", event], {
1200
+ input: JSON.stringify(payload),
1201
+ encoding: "utf8",
1202
+ timeout: 3000,
1203
+ killSignal: "SIGKILL",
1204
+ windowsHide: true,
1205
+ maxBuffer: 32 * 1024 * 1024,
1206
+ })
1207
+ : spawnSync("token-goat hook " + event, {
1208
+ input: JSON.stringify(payload),
1209
+ encoding: "utf8",
1210
+ timeout: 3000,
1211
+ killSignal: "SIGKILL",
1212
+ shell: true,
1213
+ windowsHide: true,
1214
+ maxBuffer: 32 * 1024 * 1024,
1215
+ });
1216
+ if (r.error) return null;
1217
+ const out = r.stdout ? r.stdout.trim() : "";
1218
+ if (!out) return null;
1219
+ return JSON.parse(out);
1220
+ } catch {
1221
+ return null;
1222
+ }
1223
+ }
1224
+
1225
+ // Tries the in-process hook call first (resolveRelayInProcess above), which avoids
1226
+ // spawning a second node process altogether for every single tool call in this
1227
+ // long-lived host process. Falls back to callHookViaSpawn (the original
1228
+ // spawnSync-based path, now with a 3000ms timeout/killSignal so token-goat degrades to
1229
+ // its own fail-open null rather than being force-killed by the host's own hook timeout
1230
+ // budget, ~5000ms) when the in-process path is unavailable or throws.
1231
+ async function callHook(event, payload) {
1232
+ // Timed from here, not from process start: this host lives far longer than one call, so the hook library's default clock (process age) would record the host's uptime as this call's duration.
1233
+ const start = performance.now();
1234
+ const relay = await resolveRelayInProcess();
1235
+ if (relay) {
1236
+ try {
1237
+ const out = await relay(event, payload, undefined, { elapsedMs: () => performance.now() - start });
1238
+ const trimmed = out ? out.trim() : "";
1239
+ if (!trimmed) return null;
1240
+ return JSON.parse(trimmed);
1241
+ } catch {
1242
+ // fall through to the spawnSync fallback below
1243
+ }
1244
+ }
1245
+ return callHookViaSpawn(event, payload);
1246
+ }`;
1247
+
1248
+ // src/bridges/opencode.ts
1249
+ var OPENCODE_PLUGIN_SCRIPT = `// token-goat bridge plugin for opencode
1250
+ // Bridges opencode's plugin hooks to token-goat's subprocess hook protocol.
1251
+ // https://github.com/DFKHelper/token-goat
1252
+ import { spawnSync } from "node:child_process"
1253
+ import crypto from "node:crypto"
1254
+ import fs from "node:fs"
1255
+ import os from "node:os"
1256
+ import path from "node:path"
1257
+ import { fileURLToPath, pathToFileURL } from "node:url"
1258
+
1259
+ // opencode built-in tool id -> token-goat canonical tool name. websearch/skill/task were re-verified against opencode's own source at the tag matching the installed release (anomalyco/opencode v1.18.16 packages/opencode/src/tool/): WebSearchTool registers as "websearch" (websearch.ts), SkillTool as "skill" (skill.ts), TaskTool as "task" (task.ts, params prompt/subagent_type/description -- the exact keys token-goat's hooks_agent_spawn.ts reads, which registers a lowercase 'task' handler). Unmapped, all three were dead mechanisms here: no WebSearch repeat-search deny/compression, no repeat-skill-load deny, no agent-spawn briefing or report compaction. apply_patch is mapped to Edit (see patchTextPaths below). lsp/plan/question/todo have no token-goat equivalent and stay unmapped.
1260
+ const TOOL_TO_TG = {
1261
+ read: "Read",
1262
+ bash: "Bash",
1263
+ edit: "Edit",
1264
+ write: "Write",
1265
+ grep: "Grep",
1266
+ glob: "Glob",
1267
+ webfetch: "WebFetch",
1268
+ websearch: "WebSearch",
1269
+ skill: "Skill",
1270
+ task: "task",
1271
+ apply_patch: "Edit",
1272
+ }
1273
+
1274
+ // Tools with a pre_tool_use handler server-side whose OUTPUT SHAPE this hook can act on: deny (throw), updatedInput (args rewrite), or the Read image-shrink materialization. websearch (repeat-search deny within the dedup TTL, hooks_websearch.ts), skill (repeat-load deny, hooks_skill.ts) and task (briefing updatedInput rewrite, hooks_agent_spawn.ts) all qualify. Edit/Write have no pre-hook at all. Glob DOES have one (preGlobDedupHandler, hooks_glob.ts) -- the old claim here that it has none was stale -- but its only output is an advisory contextOutput hint, and tool.execute.before has no context channel (see the module docblock), so calling it would spawn a subprocess per glob only to drop the answer; it stays excluded for that reason, not the old one.
1275
+ const PRE_HOOK_TOOLS = new Set(["read", "bash", "grep", "webfetch", "websearch", "skill", "task"])
1276
+
1277
+ // opencode tool args (camelCase) -> token-goat snake_case tool_input keys. websearch's query (websearch.ts Parameters), skill's name (skill.ts Parameters; hooks_skill.ts reads tool_input['skill']) and task's prompt/subagent_type/description (task.ts BaseParameterFields) are all from opencode's own schemas at v1.18.16.
1278
+ const ARGS_TO_TG = {
1279
+ read: { filePath: "file_path", offset: "offset", limit: "limit" },
1280
+ bash: { command: "command", timeout: "timeout" },
1281
+ edit: { filePath: "file_path" },
1282
+ write: { filePath: "file_path" },
1283
+ grep: { pattern: "pattern", path: "path" },
1284
+ glob: { pattern: "pattern", path: "path" },
1285
+ webfetch: { url: "url" },
1286
+ websearch: { query: "query" },
1287
+ skill: { name: "skill" },
1288
+ task: { prompt: "prompt", subagent_type: "subagent_type", description: "description" },
1289
+ }
1290
+
1291
+ function reverseArgMap(tool) {
1292
+ const fwd = ARGS_TO_TG[tool] ?? {}
1293
+ const rev = {}
1294
+ for (const [opencodeKey, tgKey] of Object.entries(fwd)) rev[tgKey] = opencodeKey
1295
+ return rev
1296
+ }
1297
+
1298
+ // opencode's apply_patch tool (tool/apply_patch.ts, v1.18.33) takes one patchText argument and no per-file path; it is the tool opencode offers GPT models in place of edit and write. The file paths are the header lines of the patch (patch/index.ts parsePatchHeader: "*** Add File: ", "*** Delete File: ", "*** Update File: ", plus "*** Move to: " for a rename), each trimmed and resolved against the instance directory, which is the directory this plugin sends as cwd. A trailing carriage return is dropped by the trim. Left unmapped, an edit a GPT model made was never queued for reindexing.
1299
+ const PATCH_PATH_HEADERS = ["*** Add File: ", "*** Delete File: ", "*** Update File: ", "*** Move to: "]
1300
+
1301
+ function patchTextPaths(patchText) {
1302
+ const paths = []
1303
+ if (typeof patchText !== "string") return paths
1304
+ for (const line of patchText.split(String.fromCharCode(10))) {
1305
+ for (const header of PATCH_PATH_HEADERS) {
1306
+ if (!line.startsWith(header)) continue
1307
+ const p = line.slice(header.length).trim()
1308
+ if (p && !paths.includes(p)) paths.push(p)
1309
+ }
1310
+ }
1311
+ return paths
1312
+ }
1313
+
1314
+ function toToolInput(tool, args) {
1315
+ if (tool === "apply_patch") {
1316
+ const paths = patchTextPaths(args && args.patchText)
1317
+ return paths.length === 1 ? { file_path: paths[0], file_paths: paths } : { file_paths: paths }
1318
+ }
1319
+ const map = ARGS_TO_TG[tool] ?? {}
1320
+ const out = {}
1321
+ for (const [opencodeKey, tgKey] of Object.entries(map)) {
1322
+ if (args && args[opencodeKey] !== undefined) out[tgKey] = args[opencodeKey]
1323
+ }
1324
+ return out
1325
+ }
1326
+
1327
+ ${BRIDGE_RELAY_JS}
1328
+
1329
+ // Pull the additionalContext / systemMessage string out of a hook response,
1330
+ // whichever shape it came back as (see the response-contract note above).
1331
+ function extractContext(resp) {
1332
+ if (!resp) return undefined
1333
+ const hso = resp.hookSpecificOutput
1334
+ if (hso && typeof hso.additionalContext === "string") return hso.additionalContext
1335
+ if (typeof resp.systemMessage === "string") return resp.systemMessage
1336
+ return undefined
1337
+ }
1338
+
1339
+ function extractUpdatedInput(resp) {
1340
+ const hso = resp && resp.hookSpecificOutput
1341
+ return hso && typeof hso === "object" ? hso.updatedInput : undefined
1342
+ }
1343
+
1344
+ ${MATERIALIZE_SHRUNK_IMAGE_JS}
1345
+
1346
+ export const TokenGoatPlugin = async ({ directory }) => {
1347
+ return {
1348
+ "tool.execute.before": async (input, output) => {
1349
+ const tg = TOOL_TO_TG[input.tool]
1350
+ if (!tg || !PRE_HOOK_TOOLS.has(input.tool)) return
1351
+
1352
+ const resp = await callHook("pre_tool_use", {
1353
+ session_id: input.sessionID,
1354
+ tool_name: tg,
1355
+ tool_input: toToolInput(input.tool, output.args),
1356
+ cwd: directory,
1357
+ })
1358
+ if (!resp) return
1359
+
1360
+ if (resp.decision === "block") {
1361
+ throw new Error(resp.reason || "blocked by token-goat")
1362
+ }
1363
+
1364
+ const updated = extractUpdatedInput(resp)
1365
+ if (updated) {
1366
+ const rev = reverseArgMap(input.tool)
1367
+ for (const [tgKey, val] of Object.entries(updated)) {
1368
+ output.args[rev[tgKey] ?? tgKey] = val
1369
+ }
1370
+ return
1371
+ }
1372
+
1373
+ // Image shrink has no context channel here -- translate it into a
1374
+ // rewritten filePath pointing at a materialized shrunk copy instead.
1375
+ if (tg === "Read") {
1376
+ const shrunkPath = materializeShrunkImage(extractContext(resp))
1377
+ if (shrunkPath) output.args.filePath = shrunkPath
1378
+ }
1379
+ },
1380
+
1381
+ "tool.execute.after": async (input, output) => {
1382
+ const tg = TOOL_TO_TG[input.tool]
1383
+ if (!tg) return
1384
+
1385
+ const resp = await callHook("post_tool_use", {
1386
+ session_id: input.sessionID,
1387
+ tool_name: tg,
1388
+ tool_input: toToolInput(input.tool, input.args),
1389
+ tool_response: { output: output.output, exit_code: output.metadata ? output.metadata.exit : undefined },
1390
+ cwd: directory,
1391
+ })
1392
+ if (!resp) return
1393
+
1394
+ // rewriteOutput: replace the tool result wholesale. This is the shape every post_tool_use rewrite producer emits (WebFetch injection fencing and secret redaction, websearch redaction, bash/grep output compression, agent-report compaction), and it went unapplied here for the same whitelist reason the response-contract docblock above used to omit it -- the plugin only handled the shapes its author listed, so opencode sessions got the appended hint channel but never a fenced, redacted or compressed result. Mutating output.output is the exact mechanism the context append below already relies on, so this claims no capability the append did not.
1395
+ const hso = resp.hookSpecificOutput
1396
+ const updatedToolOutput = hso && typeof hso.updatedToolOutput === "string" ? hso.updatedToolOutput : undefined
1397
+ if (updatedToolOutput !== undefined) {
1398
+ output.output = updatedToolOutput
1399
+ return
1400
+ }
1401
+
1402
+ const context = extractContext(resp)
1403
+ if (context && typeof output.output === "string") {
1404
+ output.output += \`\\n\\n[token-goat] \${context}\`
1405
+ }
1406
+ },
1407
+
1408
+ "experimental.session.compacting": async (input, output) => {
1409
+ const resp = await callHook("pre_compact", { session_id: input.sessionID })
1410
+ const manifest = resp && typeof resp.systemMessage === "string" ? resp.systemMessage : undefined
1411
+ if (manifest) output.context.push(manifest)
1412
+ },
1413
+ }
1414
+ }
1415
+ `;
1416
+
1417
+ // src/bridges/opencode_install.ts
1418
+ function opencodeGlobalConfigDir() {
1419
+ const xdgConfigHome = process.env["XDG_CONFIG_HOME"];
1420
+ if (xdgConfigHome !== void 0 && xdgConfigHome.trim() !== "") return xdgConfigHome;
1421
+ return path6.join(os5.homedir(), ".config");
1422
+ }
1423
+ function opencodePluginPath() {
1424
+ return path6.join(opencodeGlobalConfigDir(), "opencode", "plugins", "token-goat.ts");
1425
+ }
1426
+ function opencodeEntrySidecarPath() {
1427
+ return path6.join(path6.dirname(opencodePluginPath()), "token-goat-entry.json");
1428
+ }
1429
+ function legacyWindowsPluginPath() {
1430
+ if (process.platform !== "win32") return null;
1431
+ const appData = process.env["APPDATA"];
1432
+ const root = appData !== void 0 && appData.trim() !== "" ? appData : path6.join(os5.homedir(), "AppData", "Roaming");
1433
+ return path6.join(root, "opencode", "plugins", "token-goat.ts");
1434
+ }
1435
+ function removeLegacyWindowsPlugin() {
1436
+ const legacy = legacyWindowsPluginPath();
1437
+ if (legacy === null) return false;
1438
+ return uninstallSingleFilePlugin(legacy, path6.join(path6.dirname(legacy), "token-goat-entry.json"));
1439
+ }
1440
+ function installOpencode() {
1441
+ const pluginPath = opencodePluginPath();
1442
+ const { alreadyInstalled } = recordCreatedBy([pluginPath, opencodeEntrySidecarPath()], () => installSingleFilePlugin(pluginPath, opencodeEntrySidecarPath(), OPENCODE_PLUGIN_SCRIPT));
1443
+ removeLegacyWindowsPlugin();
1444
+ return { pluginPath, alreadyInstalled };
1445
+ }
1446
+ function uninstallOpencode() {
1447
+ const removedLegacy = removeLegacyWindowsPlugin();
1448
+ const removed = uninstallSingleFilePlugin(opencodePluginPath(), opencodeEntrySidecarPath());
1449
+ removeCreatedTree([opencodePluginPath(), opencodeEntrySidecarPath()]);
1450
+ return removed || removedLegacy;
1451
+ }
1452
+ function isOpencodeInstalled() {
1453
+ return fs6.existsSync(opencodePluginPath());
1454
+ }
1455
+
1456
+ // src/bridges/openclaw_install.ts
1457
+ init_define_import_meta_env();
1458
+ import * as fs7 from "node:fs";
1459
+ import * as os6 from "node:os";
1460
+ import * as path7 from "node:path";
1461
+
1462
+ // src/bridges/openclaw.ts
1463
+ init_define_import_meta_env();
1464
+ var OPENCLAW_PLUGIN_SCRIPT = `// token-goat bridge plugin for OpenClaw
1465
+ // Bridges OpenClaw's tool-call and session hooks to token-goat's subprocess
1466
+ // hook protocol. https://github.com/DFKHelper/token-goat
1467
+ //
1468
+ // NOTE ON PARAMETER SHAPES: OpenClaw's bash tool really does use token-goat's own key names (command/timeout), but the path-carrying tools (read/edit/write) send the file path under "path", not "file_path" -- read out of OpenClaw's own tool input schemas (src/agents/sessions/tools/read-tool-contract.ts, edit.ts, bash.ts in openclaw/openclaw). toToolInput below ADDS the canonical file_path alongside the original path so token-goat's getFilePath()-based handlers can see it, without renaming anything. The built-in tool NAME list below is broader than any single source confirms, since an unmatched name is a harmless no-op here, not a wrong rewrite.
1469
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
1470
+ import { spawnSync } from "node:child_process";
1471
+ import * as fs from "node:fs";
1472
+ import * as os from "node:os";
1473
+ import * as path from "node:path";
1474
+ import { fileURLToPath, pathToFileURL } from "node:url";
1475
+
1476
+ // OpenClaw built-in tool names -> token-goat internal tool names.
1477
+ const TOOL_TO_TG = {
1478
+ read: "Read",
1479
+ write: "Write",
1480
+ edit: "Edit",
1481
+ apply_patch: "Edit",
1482
+ exec: "Bash",
1483
+ bash: "Bash",
1484
+ grep: "Grep",
1485
+ glob: "Glob",
1486
+ find: "Glob",
1487
+ webfetch: "WebFetch",
1488
+ web_search: "WebFetch",
1489
+ web_fetch: "WebFetch",
1490
+ };
1491
+
1492
+ // Tools with a pre_tool_use handler whose output shape this hook can act on: deny (block/blockReason), updatedInput (params rewrite), or the Read image-shrink materialization. Edit/Write have no pre handler at all. Glob (OpenClaw's find) DOES have one -- preGlobDedupHandler in hooks_glob.ts; the old claim here that it has none was stale -- but its only output is an advisory contextOutput hint and before_tool_call has no context channel, so calling it would spawn a hook subprocess per find only to drop the answer; it stays excluded for that reason (matching pi.ts/opencode.ts's corrected note).
1493
+ const PRE_HOOK_TOOLS = new Set(["Read", "Grep", "Bash", "WebFetch"]);
1494
+
1495
+ // OpenClaw tools whose file path travels under "path" (see the parameter-shapes note above). apply_patch is handled separately: its input is a patch document naming any number of files.
1496
+ const PATH_ARG_TOOLS = new Set(["read", "write", "edit"]);
1497
+
1498
+ // The directory OpenClaw resolves a relative tool path against. Checked against OpenClaw 2026.9.7: its coding tools resolve against the agent workspace (agents.list[].workspace, else agents.defaults.workspace, else the state dir's workspace folder), not the gateway process's own directory, and the tool-call hook context carries only agentId/sessionId/sessionKey/runId/toolCallId, no directory. So the workspace is looked up through the plugin runtime's own resolver, api.runtime.agent.resolveAgentWorkspaceDir(api.runtime.config.current(), agentId). The workspace differs from the resolve base only when a run asked for a separate cwd or runs sandboxed, and neither is visible to a plugin. process.cwd() is the last resort for a runtime without that resolver.
1499
+ function workspaceFor(api, ctx) {
1500
+ if (ctx && typeof ctx.workspaceDir === "string" && ctx.workspaceDir) return ctx.workspaceDir;
1501
+ try {
1502
+ const rt = api && api.runtime;
1503
+ const dir = rt.agent.resolveAgentWorkspaceDir(rt.config.current(), ctx && ctx.agentId);
1504
+ if (typeof dir === "string" && dir) return dir;
1505
+ } catch {}
1506
+ return process.cwd();
1507
+ }
1508
+
1509
+ // Turn a tool's raw path argument into the absolute path OpenClaw will open, applying OpenClaw's own rewrites in its order: a leading "@" (a file reference) is dropped unless a file or folder literally named that way exists, a file:// URL becomes a path, and "~", "~/" and, on Windows, "~" plus a backslash expand to the home folder. Without this, a relative path was resolved against whatever directory token-goat happened to run in, so a re-read was never recognised and an edit was never queued for reindexing.
1510
+ function resolveToolPathArg(raw, base) {
1511
+ let p = raw.trim();
1512
+ if (p.startsWith("@") && p.length > 1) {
1513
+ const first = p.split("/")[0].split(path.sep)[0];
1514
+ if (!fs.existsSync(path.resolve(base, p)) && !fs.existsSync(path.resolve(base, first))) p = p.slice(1);
1515
+ }
1516
+ if (/^file:[/][/]/i.test(p)) {
1517
+ try { p = fileURLToPath(p); } catch {}
1518
+ }
1519
+ if (p === "~") p = os.homedir();
1520
+ else if (p.startsWith("~/") || p.startsWith("~" + path.sep)) p = path.join(os.homedir(), p.slice(2));
1521
+ return path.resolve(base, p);
1522
+ }
1523
+
1524
+ // apply_patch (params.input, OpenClaw's apply-patch-paths extractApplyPatchTargets) names its files on header lines, leading whitespace allowed, with a rename's "*** Move to: " following its "*** Update File: ".
1525
+ const PATCH_PATH_HEADERS = ["*** Add File: ", "*** Delete File: ", "*** Update File: ", "*** Move to: "];
1526
+
1527
+ function patchTextPaths(patchText) {
1528
+ const paths = [];
1529
+ if (typeof patchText !== "string") return paths;
1530
+ for (const rawLine of patchText.split(String.fromCharCode(10))) {
1531
+ const line = rawLine.trimStart();
1532
+ for (const header of PATCH_PATH_HEADERS) {
1533
+ if (!line.startsWith(header)) continue;
1534
+ const p = line.slice(header.length).trim();
1535
+ if (p && !paths.includes(p)) paths.push(p);
1536
+ }
1537
+ }
1538
+ return paths;
1539
+ }
1540
+
1541
+ // Build token-goat's tool_input from OpenClaw's params: same object, plus the canonical file_path (absolute, see resolveToolPathArg) added alongside the original path for the tools that carry one, or file_paths for apply_patch. derived is OpenClaw's own resolution of the patch's files (event.derivedPaths), preferred over re-parsing the patch. Never renames or drops a key.
1542
+ function toToolInput(toolName, params, base, derived) {
1543
+ const p = params || {};
1544
+ if (PATH_ARG_TOOLS.has(toolName) && typeof p.path === "string" && p.file_path === undefined) {
1545
+ return { ...p, file_path: resolveToolPathArg(p.path, base) };
1546
+ }
1547
+ if (toolName === "apply_patch" && p.file_path === undefined) {
1548
+ const files = derived && derived.length ? derived : patchTextPaths(p.input).map((f) => resolveToolPathArg(f, base));
1549
+ if (files.length === 1) return { ...p, file_path: files[0], file_paths: files };
1550
+ if (files.length > 1) return { ...p, file_paths: files };
1551
+ }
1552
+ return p;
1553
+ }
1554
+
1555
+ // Pull the additionalContext / systemMessage string out of a hook response, whichever shape it came back as (see the response-contract note in this module's header comment).
1556
+ function extractContext(resp) {
1557
+ if (!resp) return undefined;
1558
+ const hso = resp["hookSpecificOutput"];
1559
+ if (hso && typeof hso["additionalContext"] === "string") return hso["additionalContext"];
1560
+ if (typeof resp["systemMessage"] === "string") return resp["systemMessage"];
1561
+ return undefined;
1562
+ }
1563
+
1564
+ ${BRIDGE_RELAY_JS}
1565
+
1566
+ ${MATERIALIZE_SHRUNK_IMAGE_JS}
1567
+
1568
+ export default definePluginEntry({
1569
+ id: "token-goat",
1570
+ name: "token-goat",
1571
+ description:
1572
+ "Bridges OpenClaw's tool-call and session hooks to token-goat's read-hinting, bash compression, and index-refresh pipeline.",
1573
+ register(api) {
1574
+ // No confirmed stable per-session id on tool-call context (see module
1575
+ // header) -- fall back to a per-process pseudo-id if ctx carries none.
1576
+ let sessionId = \`openclaw-\${process.pid}-\${Date.now()}\`;
1577
+
1578
+ api.on("session_start", (event, ctx) => {
1579
+ sessionId = (ctx && (ctx.sessionId || ctx.sessionKey)) || sessionId;
1580
+ });
1581
+
1582
+ // No forwarding callHook for session_start here (there used to be one):
1583
+ // token-goat retired the session_start hook -- it only ever reached a
1584
+ // permanent no-op handler (see the removal in src/hooks_session.ts and
1585
+ // src/types.ts). The subscription above is kept regardless, because it's
1586
+ // OpenClaw's own lifecycle event and is still needed to refresh
1587
+ // sessionId for every other bridged call below.
1588
+
1589
+ // No api.on("session_end", ...) here: token-goat's own HOOK_EVENTS
1590
+ // (src/types.ts) has no session_end member, so calling it would be the
1591
+ // same invented-event-name no-op already found and fixed once for pi's
1592
+ // bridge (commit 2f0a15e4). OpenClaw's session_end is a real lifecycle
1593
+ // event, but token-goat has no corresponding hook to bridge it to.
1594
+
1595
+ // apply_patch's files as OpenClaw itself resolved them (event.derivedPaths, absolute), keyed by toolCallId: only before_tool_call carries them, and after_tool_call is where the edit is recorded. Capped so a call that never completes cannot grow it without bound.
1596
+ const patchPaths = new Map();
1597
+
1598
+ api.on("before_tool_call", async (event, ctx) => {
1599
+ const callId = event.toolCallId || (ctx && ctx.toolCallId);
1600
+ if (event.toolName === "apply_patch" && callId && Array.isArray(event.derivedPaths)) {
1601
+ patchPaths.set(callId, [...event.derivedPaths]);
1602
+ if (patchPaths.size > 256) patchPaths.delete(patchPaths.keys().next().value);
1603
+ }
1604
+ const tg = TOOL_TO_TG[event.toolName];
1605
+ if (!tg || !PRE_HOOK_TOOLS.has(tg)) return {};
1606
+
1607
+ const sid = (ctx && (ctx.sessionId || ctx.sessionKey)) || sessionId;
1608
+ const base = workspaceFor(api, ctx);
1609
+ const resp = await callHook("pre_tool_use", {
1610
+ session_id: sid,
1611
+ tool_name: tg,
1612
+ tool_input: toToolInput(event.toolName, event.params, base),
1613
+ cwd: base,
1614
+ });
1615
+ if (!resp) return {};
1616
+
1617
+ // Deny: block the tool call with a reason (e.g. confirmed re-read).
1618
+ if (resp["decision"] === "block") {
1619
+ return { block: true, blockReason: resp["reason"] || "blocked by token-goat" };
1620
+ }
1621
+
1622
+ // Rewrite: token-goat returns its own snake_case tool_input keys, which
1623
+ // are already OpenClaw's native param shape (see module header) -- no
1624
+ // reverse-mapping needed, just merge over the original params.
1625
+ const hso = resp["hookSpecificOutput"];
1626
+ const updated = hso ? hso["updatedInput"] : undefined;
1627
+ if (updated) {
1628
+ return { params: { ...(event.params || {}), ...updated } };
1629
+ }
1630
+
1631
+ // Image shrink has no context channel here -- translate it into a rewritten path pointing at a materialized shrunk copy instead, via the same params-rewrite channel the updatedInput merge above already relies on. OpenClaw's read tool takes the path under "path" (see the parameter-shapes note above), and the rest of the original params are preserved.
1632
+ if (tg === "Read") {
1633
+ const shrunkPath = materializeShrunkImage(extractContext(resp));
1634
+ if (shrunkPath) return { params: { ...(event.params || {}), path: shrunkPath } };
1635
+ }
1636
+
1637
+ return {};
1638
+ });
1639
+
1640
+ api.on("after_tool_call", async (event, ctx) => {
1641
+ const tg = TOOL_TO_TG[event.toolName];
1642
+ if (!tg) return;
1643
+ const sid = (ctx && (ctx.sessionId || ctx.sessionKey)) || sessionId;
1644
+ // event.result's shape is unverified against a live OpenClaw instance
1645
+ // (see module header) -- token-goat's post_tool_use handlers already
1646
+ // accept tool_response as either a raw string or an object keyed by
1647
+ // output/content/text/body, so forward it as-is when it's already an
1648
+ // object, or wrap a bare string/other value as { output } so those
1649
+ // handlers can find it either way, mirroring opencode.ts's and
1650
+ // pi.ts's tool_response: { output } shape.
1651
+ const toolResponse =
1652
+ event.result && typeof event.result === "object" ? event.result : { output: event.result };
1653
+ const callId = event.toolCallId || (ctx && ctx.toolCallId);
1654
+ const derived = callId ? patchPaths.get(callId) : undefined;
1655
+ if (callId) patchPaths.delete(callId);
1656
+ const base = workspaceFor(api, ctx);
1657
+ await callHook("post_tool_use", {
1658
+ session_id: sid,
1659
+ tool_name: tg,
1660
+ tool_input: toToolInput(event.toolName, event.params, base, derived),
1661
+ cwd: base,
1662
+ tool_response: toolResponse,
1663
+ });
1664
+ });
1665
+
1666
+ // Observation-only: no manifest-reinjection channel exists here (see
1667
+ // module header). This keeps token-goat's own session bookkeeping in
1668
+ // sync with real compaction events.
1669
+ api.on("before_compaction", async (event, ctx) => {
1670
+ const sid = (ctx && (ctx.sessionId || ctx.sessionKey)) || sessionId;
1671
+ await callHook("pre_compact", { session_id: sid, trigger: "auto" });
1672
+ });
1673
+ },
1674
+ });
1675
+ `;
1676
+
1677
+ // src/bridges/openclaw_install.ts
1678
+ var OpenclawConfigParseError = class extends Error {
1679
+ };
1680
+ var OPENCLAW_PLUGIN_ID = "token-goat";
1681
+ function openclawHomeDir() {
1682
+ return path7.join(os6.homedir(), ".openclaw");
1683
+ }
1684
+ function openclawConfigPath() {
1685
+ return path7.join(openclawHomeDir(), "openclaw.json");
1686
+ }
1687
+ function openclawPluginPath() {
1688
+ return path7.join(openclawHomeDir(), "plugins", "token-goat.ts");
1689
+ }
1690
+ function openclawEntrySidecarPath() {
1691
+ return path7.join(path7.dirname(openclawPluginPath()), "token-goat-entry.json");
1692
+ }
1693
+ function readOpenclawConfig(p, opts = {}) {
1694
+ const refuse = (problem, detail) => new OpenclawConfigParseError(
1695
+ (opts.command === "uninstall" ? `OpenClaw config file '${p}' is unreadable: it ${problem}. Uninstall left it untouched, along with the plugin it may still load, the plugin's entry sidecar and the file's backups; fix the file and run uninstall again.` : `OpenClaw config file '${p}' ${problem}. Fix or back up the file before running install.`) + (detail === void 0 ? "" : ` (${detail})`)
1696
+ );
1697
+ let raw;
1698
+ try {
1699
+ raw = fs7.readFileSync(p, "utf8");
1700
+ } catch (e) {
1701
+ const code = e.code;
1702
+ if (opts.strict === true && code !== "ENOENT" && code !== "ENOTDIR") throw refuse("exists but cannot be read", code ?? extractErrorMessage(e));
1703
+ return {};
1704
+ }
1705
+ let parsed;
1706
+ try {
1707
+ parsed = parseJsonOrJsonc(raw);
1708
+ } catch (e) {
1709
+ if (opts.strict === true) throw refuse("exists but contains invalid JSON", extractErrorMessage(e));
1710
+ return {};
1711
+ }
1712
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1713
+ const settings = parsed;
1714
+ const paths = settings.plugins?.load?.paths;
1715
+ if (paths !== void 0 && !Array.isArray(paths)) {
1716
+ if (opts.strict === true) throw refuse("has a 'plugins.load.paths' field that isn't a JSON array");
1717
+ delete settings.plugins?.load?.paths;
1718
+ }
1719
+ return settings;
1720
+ }
1721
+ if (opts.strict === true) throw refuse("does not contain a JSON object at the top level");
1722
+ return {};
1723
+ }
1724
+ function openclawArtifactPaths() {
1725
+ return [openclawConfigPath(), openclawPluginPath(), openclawEntrySidecarPath()];
1726
+ }
1727
+ function installOpenclaw() {
1728
+ return recordCreatedBy(openclawArtifactPaths(), installOpenclawFiles);
1729
+ }
1730
+ function installOpenclawFiles() {
1731
+ const configPath = openclawConfigPath();
1732
+ const pluginPath = openclawPluginPath();
1733
+ let existingPlugin;
1734
+ try {
1735
+ existingPlugin = fs7.readFileSync(pluginPath, "utf8");
1736
+ } catch {
1737
+ existingPlugin = void 0;
1738
+ }
1739
+ const pluginChanged = existingPlugin !== OPENCLAW_PLUGIN_SCRIPT;
1740
+ const settings = readOpenclawConfig(configPath, { strict: true });
1741
+ const entryPath = process.argv[1];
1742
+ if (entryPath) {
1743
+ ensureDirSync(path7.dirname(pluginPath));
1744
+ atomicWriteText(openclawEntrySidecarPath(), JSON.stringify({ entryPath }));
1745
+ }
1746
+ const plugins = settings.plugins ?? {};
1747
+ const loadPaths = [...plugins.load?.paths ?? []];
1748
+ const entries = { ...plugins.entries ?? {} };
1749
+ let configChanged = false;
1750
+ if (!loadPaths.some((p) => foldPath(p) === foldPath(pluginPath))) {
1751
+ loadPaths.push(pluginPath);
1752
+ configChanged = true;
1753
+ }
1754
+ if (entries[OPENCLAW_PLUGIN_ID] === void 0) {
1755
+ entries[OPENCLAW_PLUGIN_ID] = { enabled: true };
1756
+ configChanged = true;
1757
+ }
1758
+ if (!pluginChanged && !configChanged) {
1759
+ return { configPath, pluginPath, alreadyInstalled: true };
1760
+ }
1761
+ if (pluginChanged) {
1762
+ ensureDirSync(path7.dirname(pluginPath));
1763
+ atomicWriteText(pluginPath, OPENCLAW_PLUGIN_SCRIPT);
1764
+ }
1765
+ if (configChanged) {
1766
+ settings.plugins = { ...plugins, load: { ...plugins.load, paths: loadPaths }, entries };
1767
+ writeSettingsKeepingComments(configPath, settings);
1768
+ }
1769
+ return { configPath, pluginPath, alreadyInstalled: false };
1770
+ }
1771
+ function uninstallOpenclaw() {
1772
+ const configPath = openclawConfigPath();
1773
+ const pluginPath = openclawPluginPath();
1774
+ const settings = readOpenclawConfig(configPath, { strict: true, command: "uninstall" });
1775
+ let removed = false;
1776
+ try {
1777
+ fs7.unlinkSync(pluginPath);
1778
+ removed = true;
1779
+ } catch {
1780
+ }
1781
+ try {
1782
+ fs7.unlinkSync(openclawEntrySidecarPath());
1783
+ } catch {
1784
+ }
1785
+ const plugins = settings.plugins;
1786
+ if (plugins !== void 0) {
1787
+ const loadPaths = plugins.load?.paths;
1788
+ const entries = plugins.entries;
1789
+ let configChanged = false;
1790
+ if (loadPaths !== void 0 && loadPaths.some((p) => foldPath(p) === foldPath(pluginPath))) {
1791
+ const kept = loadPaths.filter((p) => foldPath(p) !== foldPath(pluginPath));
1792
+ if (kept.length > 0) {
1793
+ plugins.load = { ...plugins.load, paths: kept };
1794
+ } else if (plugins.load !== void 0) {
1795
+ delete plugins.load.paths;
1796
+ if (Object.keys(plugins.load).length === 0) delete plugins.load;
1797
+ }
1798
+ configChanged = true;
1799
+ removed = true;
1800
+ }
1801
+ if (entries !== void 0 && entries[OPENCLAW_PLUGIN_ID] !== void 0) {
1802
+ delete entries[OPENCLAW_PLUGIN_ID];
1803
+ if (Object.keys(entries).length === 0) delete plugins.entries;
1804
+ configChanged = true;
1805
+ removed = true;
1806
+ }
1807
+ if (configChanged) {
1808
+ if (Object.keys(plugins).length === 0) {
1809
+ delete settings.plugins;
1810
+ } else {
1811
+ settings.plugins = plugins;
1812
+ }
1813
+ writeSettingsKeepingComments(configPath, settings);
1814
+ }
1815
+ }
1816
+ removeCreatedBackups(configPath);
1817
+ removeCreatedTree(openclawArtifactPaths());
1818
+ return removed;
1819
+ }
1820
+ function isOpenclawInstalled() {
1821
+ if (!fs7.existsSync(openclawPluginPath())) return false;
1822
+ const settings = readOpenclawConfig(openclawConfigPath());
1823
+ const plugins = settings.plugins;
1824
+ if (plugins === void 0) return false;
1825
+ const targetFolded = foldPath(openclawPluginPath());
1826
+ const hasPath = (plugins.load?.paths ?? []).some((p) => foldPath(p) === targetFolded);
1827
+ const hasEntry = plugins.entries?.[OPENCLAW_PLUGIN_ID] !== void 0;
1828
+ return hasPath && hasEntry;
1829
+ }
1830
+
1831
+ // src/bridges/grok.ts
1832
+ init_define_import_meta_env();
1833
+ var GROK_HOOK_SCRIPT = `#!/usr/bin/env node
1834
+ // 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.
1835
+ 'use strict'
1836
+ ${SHIM_REQUIRES}
1837
+
1838
+ // Keep in sync with HOOK_EVENTS in src/types.ts.
1839
+ const VALID_HOOK_EVENTS = new Set([
1840
+ 'pre_tool_use',
1841
+ 'post_tool_use',
1842
+ 'notification',
1843
+ 'stop',
1844
+ 'pre_compact',
1845
+ 'user_prompt_submit',
1846
+ 'subagent_stop',
1847
+ ])
1848
+
1849
+ function allowResponse(eventName) {
1850
+ return eventName === 'pre_tool_use' ? '{"decision":"allow"}' : '{}'
1851
+ }
1852
+
1853
+ ${SHIM_TRY_SERVER}
1854
+
1855
+ ${SHIM_TRY_IN_PROCESS}
1856
+
1857
+ ${SHIM_MAX_BUFFER_CONST}
1858
+
1859
+ async function main() {
1860
+ const eventName = process.argv[2] || ''
1861
+ if (!VALID_HOOK_EVENTS.has(eventName)) {
1862
+ process.stdout.write('{}')
1863
+ return
1864
+ }
1865
+ let input = ''
1866
+ try {
1867
+ input = require('node:fs').readFileSync(0, 'utf8')
1868
+ } catch {
1869
+ process.stdout.write(allowResponse(eventName))
1870
+ return
1871
+ }
1872
+ // process.argv[3], when present, is the absolute path to the token-goat CLI entry
1873
+ // that ran 'token-goat install --grok' (baked in by hookCommandFor in
1874
+ // grok_install.ts). Try the in-process hook lib first (tryInProcess above) -- this
1875
+ // avoids spawning a second node process altogether. If that's unavailable, invoking
1876
+ // the entry directly via process.execPath sidesteps PATH resolution for this inner
1877
+ // call, the same reasoning as every other bridge shim's inner call. Falls back
1878
+ // further to the old PATH-based shell:true invocation when argv[3] is absent. A
1879
+ // 3000ms timeout/killSignal on both spawnSync fallbacks bounds them well under
1880
+ // Grok's own hook timeout (default 5s, per the hooks doc), so token-goat degrades to
1881
+ // its own fail-open response rather than being force-killed by Grok first.
1882
+ const entryPath = process.argv[3]
1883
+ let stdout = await tryInProcess(entryPath, eventName, input)
1884
+ if (stdout === undefined) {
1885
+ ${SHIM_SPAWN_LADDER}
1886
+ : spawnSync('token-goat hook ' + eventName, {
1887
+ input,
1888
+ encoding: 'utf8',
1889
+ shell: true,
1890
+ timeout: 3000,
1891
+ killSignal: 'SIGKILL',
1892
+ maxBuffer: SHIM_MAX_BUFFER_BYTES,
1893
+ })
1894
+ if (res.status !== 0 || !res.stdout) {
1895
+ process.stdout.write(allowResponse(eventName))
1896
+ return
1897
+ }
1898
+ stdout = res.stdout
1899
+ }
1900
+ let parsed
1901
+ try {
1902
+ parsed = JSON.parse(stdout)
1903
+ } catch {
1904
+ process.stdout.write(allowResponse(eventName))
1905
+ return
1906
+ }
1907
+
1908
+ if (eventName !== 'pre_tool_use') {
1909
+ // Passive events: Grok ignores stdout entirely (per the hooks doc), so the exact
1910
+ // shape doesn't matter -- forward token-goat's response verbatim.
1911
+ process.stdout.write(JSON.stringify(parsed))
1912
+ return
1913
+ }
1914
+
1915
+ if (parsed && parsed.decision === 'block') {
1916
+ process.stdout.write(JSON.stringify({ decision: 'deny', reason: parsed.reason || 'blocked by token-goat' }))
1917
+ process.exitCode = 2
1918
+ return
1919
+ }
1920
+ process.stdout.write('{"decision":"allow"}')
1921
+ }
1922
+
1923
+ main().catch((err) => {
1924
+ try {
1925
+ process.stdout.write(allowResponse(process.argv[2] || ''))
1926
+ } catch {
1927
+ // stdout itself is broken; nothing more can be done here.
1928
+ }
1929
+ })
1930
+ `;
1931
+
1932
+ // src/bridges/grok_install.ts
1933
+ init_define_import_meta_env();
1934
+ import * as fs8 from "node:fs";
1935
+ import * as os7 from "node:os";
1936
+ import * as path8 from "node:path";
1937
+ var GROK_HOOK_EVENTS = ["PreToolUse", "PostToolUse", "PreCompact", "UserPromptSubmit", "SubagentStop"];
1938
+ var GROK_EVENT_ARG = {
1939
+ PreToolUse: "pre_tool_use",
1940
+ PostToolUse: "post_tool_use",
1941
+ PreCompact: "pre_compact",
1942
+ UserPromptSubmit: "user_prompt_submit",
1943
+ SubagentStop: "subagent_stop"
1944
+ };
1945
+ function grokHooksDir() {
1946
+ return path8.join(os7.homedir(), ".grok", "hooks");
1947
+ }
1948
+ function grokConfigPath() {
1949
+ return path8.join(grokHooksDir(), "token-goat.json");
1950
+ }
1951
+ function grokHookScriptPath() {
1952
+ return path8.join(grokHooksDir(), SHIM_FILE);
1953
+ }
1954
+ function grokLegacyHookScriptPath() {
1955
+ return path8.join(grokHooksDir(), LEGACY_SHIM_FILE);
1956
+ }
1957
+ function grokHookCommandFor(scriptPath, eventArg, opts = {}) {
1958
+ const bin = nativeHookBinary(process.argv[1], opts);
1959
+ const native = bin === void 0 ? void 0 : nativeHookCommandLine(process.platform === "win32" ? "powershell" : "sh", bin, "grok", scriptPath, eventArg);
1960
+ return native ?? (process.platform === "win32" ? powershellHookLine(hookPowershellCommand(scriptPath, eventArg)) : hookCommandFor(scriptPath, eventArg));
1961
+ }
1962
+ function wiredGrokHookWords() {
1963
+ let config;
1964
+ try {
1965
+ config = JSON.parse(stripBom(fs8.readFileSync(grokConfigPath(), "utf8")));
1966
+ } catch {
1967
+ return [];
1968
+ }
1969
+ const scriptPath = grokHookScriptPath();
1970
+ const expected = new Set(GROK_HOOK_EVENTS.map((e) => grokHookCommandFor(scriptPath, GROK_EVENT_ARG[e], { sync: false })));
1971
+ const out = [];
1972
+ for (const groups of Object.values(config?.hooks ?? {})) {
1973
+ for (const g of Array.isArray(groups) ? groups : []) {
1974
+ for (const h of Array.isArray(g?.hooks) ? g.hooks : []) {
1975
+ if (typeof h?.command === "string") out.push({ words: splitHookCommand(h.command, process.platform === "win32" ? "powershell" : "sh"), current: expected.has(h.command) });
1976
+ }
1977
+ }
1978
+ }
1979
+ return out;
1980
+ }
1981
+ function buildConfig(scriptPath) {
1982
+ const hooks = {};
1983
+ for (const event of GROK_HOOK_EVENTS) {
1984
+ hooks[event] = [
1985
+ { matcher: "", hooks: [{ type: "command", command: grokHookCommandFor(scriptPath, GROK_EVENT_ARG[event]) }] }
1986
+ ];
1987
+ }
1988
+ return { hooks };
1989
+ }
1990
+ function installGrok() {
1991
+ return recordCreatedBy(grokArtifactPaths(), installGrokFiles);
1992
+ }
1993
+ function grokArtifactPaths() {
1994
+ return [grokConfigPath(), grokHookScriptPath(), grokLegacyHookScriptPath()];
1995
+ }
1996
+ function installGrokFiles() {
1997
+ const configPath = grokConfigPath();
1998
+ const scriptPath = grokHookScriptPath();
1999
+ const config = buildConfig(scriptPath);
2000
+ for (const command of Object.values(config.hooks).flatMap((groups) => (groups ?? []).flatMap((g) => g.hooks.map((h) => h.command)))) {
2001
+ const span = hookCommandRewrittenSpan(command, "grok");
2002
+ if (span !== void 0) {
2003
+ throw new HookCommandRewriteError(`Grok CLI substitutes ${span} in a hook command with the value of that environment variable when it loads the hook, whatever the quoting, and refuses to run the hook when it is unset, so the hook command token-goat would write (${command}) would not run token-goat. Nothing was written. Move the path that contains "${span}" (the home directory, token-goat's install directory, or Node's) to one without a $ name, then run 'token-goat install --grok' again.`);
2004
+ }
2005
+ }
2006
+ const forwarderChanged = writeIfDifferent(grokLegacyHookScriptPath(), legacyShimForwarder("{}"));
2007
+ const scriptChanged = writeIfDifferent(scriptPath, GROK_HOOK_SCRIPT);
2008
+ const desiredText = `${JSON.stringify(config, null, 2)}
2009
+ `;
2010
+ const configChanged = writeIfDifferent(configPath, desiredText, true);
2011
+ return { configPath, hookScriptPath: scriptPath, alreadyInstalled: !scriptChanged && !forwarderChanged && !configChanged };
2012
+ }
2013
+ function uninstallGrok() {
2014
+ const configPath = grokConfigPath();
2015
+ const scriptPath = grokHookScriptPath();
2016
+ let removedAny = false;
2017
+ try {
2018
+ fs8.unlinkSync(configPath);
2019
+ removedAny = true;
2020
+ } catch {
2021
+ }
2022
+ for (const p of [scriptPath, grokLegacyHookScriptPath()]) {
2023
+ try {
2024
+ fs8.unlinkSync(p);
2025
+ removedAny = true;
2026
+ } catch {
2027
+ }
2028
+ }
2029
+ removeCreatedBackups(configPath);
2030
+ removeCreatedTree(grokArtifactPaths());
2031
+ return removedAny;
2032
+ }
2033
+ function isGrokInstalled() {
2034
+ return fs8.existsSync(grokConfigPath()) && fs8.existsSync(grokHookScriptPath());
2035
+ }
2036
+
2037
+ // src/bridges/antigravity_install.ts
2038
+ init_define_import_meta_env();
2039
+ import * as fs9 from "node:fs";
2040
+ import * as os8 from "node:os";
2041
+ import * as path9 from "node:path";
2042
+ var HOOK_NAME = "token-goat";
2043
+ var PLUGIN_DIR_NAME = "token-goat";
2044
+ var SHIM_FILE2 = "token-goat-hook.cmd";
2045
+ var HOOK_TIMEOUT_SECONDS = 30;
2046
+ var SHIM_MARKER = "--harness antigravity";
2047
+ var ANTIGRAVITY_HOOK_EVENTS = ["PreToolUse", "PostToolUse"];
2048
+ var ANTIGRAVITY_EVENT_ARG = {
2049
+ PreToolUse: "pre_tool_use",
2050
+ PostToolUse: "post_tool_use"
2051
+ };
2052
+ var AntigravitySettingsParseError = class extends Error {
2053
+ };
2054
+ function geminiDir() {
2055
+ return path9.join(os8.homedir(), ".gemini");
2056
+ }
2057
+ function geminiConfigDir() {
2058
+ return path9.join(geminiDir(), "config");
2059
+ }
2060
+ function antigravityPluginsDir() {
2061
+ return path9.join(geminiConfigDir(), "plugins");
2062
+ }
2063
+ function antigravityPluginDir() {
2064
+ return path9.join(antigravityPluginsDir(), PLUGIN_DIR_NAME);
2065
+ }
2066
+ function antigravityHooksPath() {
2067
+ return path9.join(antigravityPluginDir(), "hooks.json");
2068
+ }
2069
+ function pluginManifestPath() {
2070
+ return path9.join(antigravityPluginDir(), "plugin.json");
2071
+ }
2072
+ function shimPath() {
2073
+ return path9.join(antigravityPluginDir(), SHIM_FILE2);
2074
+ }
2075
+ function readJsonObject(p, label, opts = {}) {
2076
+ const refuse = (problem, detail) => new AntigravitySettingsParseError(
2077
+ (opts.command === "uninstall" ? `Antigravity ${label} '${p}' is unreadable: it ${problem}. Uninstall left it and its backups untouched; fix the file and run uninstall again.` : `Antigravity ${label} '${p}' ${problem}. Fix or back up the file before running install.`) + (detail === void 0 ? "" : ` (${detail})`)
2078
+ );
2079
+ let raw;
2080
+ try {
2081
+ raw = fs9.readFileSync(p, "utf8");
2082
+ } catch (e) {
2083
+ const code = e.code;
2084
+ if (opts.strict === true && code !== "ENOENT" && code !== "ENOTDIR") throw refuse("exists but cannot be read", code ?? extractErrorMessage(e));
2085
+ return void 0;
2086
+ }
2087
+ let parsed;
2088
+ try {
2089
+ parsed = JSON.parse(stripBom(raw));
2090
+ } catch (e) {
2091
+ if (opts.strict === true) throw refuse("exists but contains invalid JSON", extractErrorMessage(e));
2092
+ return void 0;
2093
+ }
2094
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
2095
+ if (opts.strict === true) throw refuse("does not contain a JSON object at the top level");
2096
+ return void 0;
2097
+ }
2098
+ function shimContent() {
2099
+ const entryPath = process.argv[1];
2100
+ const escape = (value) => quoteShellPath(value).replace(/%/g, "%%");
2101
+ const invocation = entryPath ? `${escape(process.execPath)} ${escape(entryPath)}` : "token-goat";
2102
+ return `@${invocation} hook %1 ${SHIM_MARKER}\r
2103
+ @exit /b %ERRORLEVEL%\r
2104
+ `;
2105
+ }
2106
+ function antigravityHookCommand(eventArg, platform = process.platform) {
2107
+ if (platform === "win32") return `.\\${SHIM_FILE2} ${eventArg}`;
2108
+ const entryPath = process.argv[1];
2109
+ if (!entryPath) return `token-goat hook ${eventArg} ${SHIM_MARKER}`;
2110
+ return `${quoteShellPath(process.execPath)} ${quoteShellPath(entryPath)} hook ${eventArg} ${SHIM_MARKER}`;
2111
+ }
2112
+ function desiredHookEntry() {
2113
+ const entry = {};
2114
+ for (const event of ANTIGRAVITY_HOOK_EVENTS) {
2115
+ entry[event] = [{ matcher: "*", hooks: [{ type: "command", command: antigravityHookCommand(ANTIGRAVITY_EVENT_ARG[event]), timeout: HOOK_TIMEOUT_SECONDS }] }];
2116
+ }
2117
+ return entry;
2118
+ }
2119
+ function readShim() {
2120
+ try {
2121
+ return fs9.readFileSync(shimPath(), "utf8");
2122
+ } catch {
2123
+ return void 0;
2124
+ }
2125
+ }
2126
+ function installAntigravity() {
2127
+ const pluginDir = antigravityPluginDir();
2128
+ const hooksPath = antigravityHooksPath();
2129
+ const manifestPath = pluginManifestPath();
2130
+ const hooksFile = readJsonObject(hooksPath, "plugin hooks file", { strict: true, command: "install" });
2131
+ const manifest = readJsonObject(manifestPath, "plugin manifest", { strict: true, command: "install" });
2132
+ let changed = false;
2133
+ for (const dir of [geminiDir(), geminiConfigDir(), antigravityPluginsDir(), pluginDir]) ensureDirRecordingCreation(dir);
2134
+ if (manifest === void 0) {
2135
+ writeJsonSettings(manifestPath, { name: PLUGIN_DIR_NAME });
2136
+ recordCreatedConfig(manifestPath);
2137
+ changed = true;
2138
+ }
2139
+ if (process.platform === "win32") {
2140
+ const content = shimContent();
2141
+ const existing = readShim();
2142
+ if (existing !== content) {
2143
+ atomicWriteText(shimPath(), content);
2144
+ if (existing === void 0) recordCreatedConfig(shimPath());
2145
+ changed = true;
2146
+ }
2147
+ }
2148
+ const desired = desiredHookEntry();
2149
+ const current = hooksFile?.[HOOK_NAME];
2150
+ if (JSON.stringify(current) !== JSON.stringify(desired)) {
2151
+ writeJsonSettings(hooksPath, { ...hooksFile ?? {}, [HOOK_NAME]: desired });
2152
+ if (hooksFile === void 0) recordCreatedConfig(hooksPath);
2153
+ changed = true;
2154
+ }
2155
+ return { pluginDir, alreadyInstalled: !changed };
2156
+ }
2157
+ function uninstallAntigravity() {
2158
+ const pluginDir = antigravityPluginDir();
2159
+ const hooksPath = antigravityHooksPath();
2160
+ const manifestPath = pluginManifestPath();
2161
+ const hooksFile = readJsonObject(hooksPath, "plugin hooks file", { strict: true, command: "uninstall" });
2162
+ let removed = false;
2163
+ if (hooksFile !== void 0 && Object.prototype.hasOwnProperty.call(hooksFile, HOOK_NAME)) {
2164
+ const rest = { ...hooksFile };
2165
+ delete rest[HOOK_NAME];
2166
+ removed = true;
2167
+ if (Object.keys(rest).length === 0 && takeCreatedConfig(hooksPath)) {
2168
+ fs9.unlinkSync(hooksPath);
2169
+ } else {
2170
+ writeJsonSettings(hooksPath, rest);
2171
+ }
2172
+ }
2173
+ removeCreatedBackups(hooksPath);
2174
+ const shim = readShim();
2175
+ if (shim?.includes(SHIM_MARKER) === true) {
2176
+ fs9.unlinkSync(shimPath());
2177
+ takeCreatedConfig(shimPath());
2178
+ removed = true;
2179
+ }
2180
+ const manifest = readJsonObject(manifestPath, "plugin manifest");
2181
+ if (manifest !== void 0 && hasCreatedConfig(manifestPath) && JSON.stringify(manifest) === JSON.stringify({ name: PLUGIN_DIR_NAME }) && !fs9.existsSync(hooksPath)) {
2182
+ fs9.unlinkSync(manifestPath);
2183
+ takeCreatedConfig(manifestPath);
2184
+ removed = true;
2185
+ }
2186
+ removeCreatedBackups(manifestPath);
2187
+ for (const dir of [pluginDir, antigravityPluginsDir(), geminiConfigDir(), geminiDir()]) removeCreatedIfEmpty(dir);
2188
+ return removed;
2189
+ }
2190
+ function isAntigravityInstalled() {
2191
+ const hooksFile = readJsonObject(antigravityHooksPath(), "plugin hooks file");
2192
+ if (hooksFile !== void 0 && Object.prototype.hasOwnProperty.call(hooksFile, HOOK_NAME)) return true;
2193
+ return readShim()?.includes(SHIM_MARKER) === true;
2194
+ }
2195
+
2196
+ export {
2197
+ installGemini,
2198
+ uninstallGemini,
2199
+ isGeminiInstalled,
2200
+ installQwen,
2201
+ uninstallQwen,
2202
+ isQwenInstalled,
2203
+ kimiConfigPath,
2204
+ kimiHookScriptPath,
2205
+ installKimi,
2206
+ uninstallKimi,
2207
+ wiredKimiHookWords,
2208
+ isKimiInstalled,
2209
+ installPi,
2210
+ uninstallPi,
2211
+ isPiInstalled,
2212
+ installOpencode,
2213
+ uninstallOpencode,
2214
+ isOpencodeInstalled,
2215
+ installOpenclaw,
2216
+ uninstallOpenclaw,
2217
+ isOpenclawInstalled,
2218
+ GROK_HOOK_SCRIPT,
2219
+ grokConfigPath,
2220
+ grokHookScriptPath,
2221
+ wiredGrokHookWords,
2222
+ installGrok,
2223
+ uninstallGrok,
2224
+ isGrokInstalled,
2225
+ installAntigravity,
2226
+ uninstallAntigravity,
2227
+ isAntigravityInstalled
2228
+ };