token-goat 2.9.29 → 2.9.31

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 (197) hide show
  1. package/README.md +53 -42
  2. package/SECURITY.md +37 -11
  3. package/THIRD_PARTY_NOTICES.md +41 -4
  4. package/dist/native/linux-arm64/tg-hook +0 -0
  5. package/dist/native/linux-x64/tg-hook +0 -0
  6. package/dist/native/win32-arm64/tg-hook.exe +0 -0
  7. package/dist/native/win32-x64/tg-hook.exe +0 -0
  8. package/dist/ort-wasm-simd-threaded.mjs +59 -0
  9. package/dist/{token-goat-chunk-SE6E4BKJ.mjs → token-goat-chunk-25TRAWET.mjs} +23 -6
  10. package/dist/token-goat-chunk-2KXO6V2B.mjs +203 -0
  11. package/dist/{token-goat-chunk-UXSXQQIY.mjs → token-goat-chunk-2M7E4PF7.mjs} +43 -14
  12. package/dist/token-goat-chunk-2UPZGKKY.mjs +188 -0
  13. package/dist/token-goat-chunk-2WLPWKHV.mjs +135 -0
  14. package/dist/token-goat-chunk-2XBMMDBD.mjs +126 -0
  15. package/dist/token-goat-chunk-32ZOY4N5.mjs +23 -0
  16. package/dist/token-goat-chunk-3EZRNKTK.mjs +188 -0
  17. package/dist/token-goat-chunk-4ADZHI7J.mjs +63 -0
  18. package/dist/token-goat-chunk-4GBVPH3D.mjs +242 -0
  19. package/dist/{token-goat-chunk-2WOUYWLM.mjs → token-goat-chunk-4GILLLWY.mjs} +34 -7
  20. package/dist/token-goat-chunk-4NAPX7BA.mjs +90 -0
  21. package/dist/{token-goat-chunk-NMBWLRHW.mjs → token-goat-chunk-4SC7UJYV.mjs} +9981 -11563
  22. package/dist/token-goat-chunk-4SMRO4TO.mjs +366 -0
  23. package/dist/{token-goat-chunk-O6I2XU4P.mjs → token-goat-chunk-4ZBFWLXF.mjs} +310 -441
  24. package/dist/token-goat-chunk-57BIAXJL.mjs +286 -0
  25. package/dist/token-goat-chunk-5AC66HNP.mjs +115 -0
  26. package/dist/token-goat-chunk-5IMLKSOP.mjs +145 -0
  27. package/dist/token-goat-chunk-5RG5SBL6.mjs +221 -0
  28. package/dist/{token-goat-chunk-RRNZMM3A.mjs → token-goat-chunk-5Y6ZYL6U.mjs} +21 -2
  29. package/dist/token-goat-chunk-646E6U44.mjs +398 -0
  30. package/dist/token-goat-chunk-65N4RHP4.mjs +33 -0
  31. package/dist/{token-goat-chunk-6AK46ZT7.mjs → token-goat-chunk-6DG6HWP3.mjs} +13 -3
  32. package/dist/token-goat-chunk-6ENQSNRZ.mjs +573 -0
  33. package/dist/token-goat-chunk-6ZY4GNF2.mjs +91 -0
  34. package/dist/{token-goat-chunk-E5TK5DNO.mjs → token-goat-chunk-76FGDLRO.mjs} +574 -340
  35. package/dist/{token-goat-chunk-TGOXNAHW.mjs → token-goat-chunk-7EZTZOMC.mjs} +780 -767
  36. package/dist/token-goat-chunk-7MAOA6X4.mjs +3647 -0
  37. package/dist/token-goat-chunk-7R2ZVVAX.mjs +218 -0
  38. package/dist/{token-goat-chunk-TCQANXYC.mjs → token-goat-chunk-AM5V6GSX.mjs} +18 -2
  39. package/dist/token-goat-chunk-AUNGYYQX.mjs +184 -0
  40. package/dist/token-goat-chunk-AUQYRMMH.mjs +2985 -0
  41. package/dist/token-goat-chunk-B3PEKBYG.mjs +128 -0
  42. package/dist/{token-goat-chunk-KOPIR3KZ.mjs → token-goat-chunk-BA4HI6BW.mjs} +1910 -165
  43. package/dist/{token-goat-chunk-52NSDWYY.mjs → token-goat-chunk-BFCWY73P.mjs} +58 -20
  44. package/dist/token-goat-chunk-BPGCUMR7.mjs +30 -0
  45. package/dist/token-goat-chunk-BWGFUZZE.mjs +25 -0
  46. package/dist/token-goat-chunk-C75NA7PL.mjs +322 -0
  47. package/dist/token-goat-chunk-CBIQ7WTD.mjs +87 -0
  48. package/dist/token-goat-chunk-CETM74KC.mjs +307 -0
  49. package/dist/token-goat-chunk-CIMQ2BB3.mjs +233 -0
  50. package/dist/{token-goat-chunk-DNSPXQXQ.mjs → token-goat-chunk-CJLS3CDL.mjs} +70 -18
  51. package/dist/{token-goat-chunk-MELOKFDJ.mjs → token-goat-chunk-CO3UYX6G.mjs} +7 -1
  52. package/dist/token-goat-chunk-COPNXRJM.mjs +154 -0
  53. package/dist/token-goat-chunk-CSJ5L3NL.mjs +479 -0
  54. package/dist/token-goat-chunk-CVCEVUGK.mjs +315 -0
  55. package/dist/token-goat-chunk-CXHOFN7R.mjs +371 -0
  56. package/dist/token-goat-chunk-D735C4QM.mjs +603 -0
  57. package/dist/{token-goat-chunk-E4ZOJIZK.mjs → token-goat-chunk-DGS2LVNA.mjs} +70 -160
  58. package/dist/{token-goat-chunk-L7H3U27L.mjs → token-goat-chunk-DOWRSMWO.mjs} +1 -1
  59. package/dist/token-goat-chunk-E45INMY3.mjs +71 -0
  60. package/dist/token-goat-chunk-EC5F37F4.mjs +244 -0
  61. package/dist/{token-goat-chunk-XTQAOTSO.mjs → token-goat-chunk-EDBDRVH5.mjs} +4 -1
  62. package/dist/token-goat-chunk-EELTBKDM.mjs +613 -0
  63. package/dist/token-goat-chunk-EVMZJ27Z.mjs +147 -0
  64. package/dist/token-goat-chunk-EXKGNHEG.mjs +2096 -0
  65. package/dist/{token-goat-chunk-YOOVFPTP.mjs → token-goat-chunk-F4HYL64R.mjs} +12 -2
  66. package/dist/token-goat-chunk-F6PAUIEK.mjs +624 -0
  67. package/dist/token-goat-chunk-FDSREWLX.mjs +692 -0
  68. package/dist/{token-goat-chunk-NMTKNYGF.mjs → token-goat-chunk-FNFBKLRD.mjs} +74 -9
  69. package/dist/token-goat-chunk-G5PJ7AGM.mjs +36 -0
  70. package/dist/token-goat-chunk-GGGG7RWB.mjs +236 -0
  71. package/dist/{token-goat-chunk-TDUT2CL6.mjs → token-goat-chunk-GL3ETKSC.mjs} +276 -19
  72. package/dist/{token-goat-chunk-7CPMDFGB.mjs → token-goat-chunk-GVB2MUST.mjs} +265 -1567
  73. package/dist/token-goat-chunk-GWFLAUMQ.mjs +307 -0
  74. package/dist/token-goat-chunk-HAVAOB3S.mjs +87 -0
  75. package/dist/token-goat-chunk-HLPW63A7.mjs +48 -0
  76. package/dist/token-goat-chunk-HU4TORC5.mjs +1377 -0
  77. package/dist/token-goat-chunk-I7VKZZAW.mjs +35 -0
  78. package/dist/token-goat-chunk-IA6XSPE3.mjs +218 -0
  79. package/dist/token-goat-chunk-IAGXMHIK.mjs +863 -0
  80. package/dist/{token-goat-chunk-435UQTKD.mjs → token-goat-chunk-IK2DRMFX.mjs} +129 -74
  81. package/dist/{token-goat-chunk-ZER6EAXZ.mjs → token-goat-chunk-INNPTRUX.mjs} +2954 -2783
  82. package/dist/token-goat-chunk-JJ5BX4F4.mjs +129 -0
  83. package/dist/token-goat-chunk-JVFSHVFJ.mjs +129 -0
  84. package/dist/token-goat-chunk-KGEFFSHD.mjs +50 -0
  85. package/dist/token-goat-chunk-KKUFXAPD.mjs +25 -0
  86. package/dist/token-goat-chunk-KQBLRPBN.mjs +433 -0
  87. package/dist/token-goat-chunk-LCNQ2CNN.mjs +497 -0
  88. package/dist/token-goat-chunk-M3LFJDPU.mjs +58 -0
  89. package/dist/token-goat-chunk-M52KPT3C.mjs +87 -0
  90. package/dist/token-goat-chunk-MBEG42H7.mjs +150 -0
  91. package/dist/{token-goat-chunk-EVC4TOLE.mjs → token-goat-chunk-METECDOH.mjs} +3 -12
  92. package/dist/{token-goat-chunk-2AWQ2R5X.mjs → token-goat-chunk-MHX7XCC3.mjs} +3 -2
  93. package/dist/token-goat-chunk-MNWKPRAL.mjs +13 -0
  94. package/dist/token-goat-chunk-N5LSOVSU.mjs +137 -0
  95. package/dist/{token-goat-chunk-DPNLUJWB.mjs → token-goat-chunk-N5MMR2IW.mjs} +30 -8
  96. package/dist/token-goat-chunk-N7B3JQ2C.mjs +160 -0
  97. package/dist/{token-goat-chunk-VMFWNHGR.mjs → token-goat-chunk-NISNMD7T.mjs} +2545 -2953
  98. package/dist/token-goat-chunk-NMD42SFQ.mjs +434 -0
  99. package/dist/token-goat-chunk-NOSS5VWY.mjs +174 -0
  100. package/dist/token-goat-chunk-NSNSPPBG.mjs +69 -0
  101. package/dist/token-goat-chunk-OBOFIHKN.mjs +38 -0
  102. package/dist/token-goat-chunk-OBRFJ5O5.mjs +76 -0
  103. package/dist/token-goat-chunk-ODBT2USM.mjs +800 -0
  104. package/dist/token-goat-chunk-OS5EM5PP.mjs +107 -0
  105. package/dist/token-goat-chunk-OUKDTVNU.mjs +1178 -0
  106. package/dist/token-goat-chunk-OXFMP75N.mjs +566 -0
  107. package/dist/{token-goat-chunk-ASLWOQ3I.mjs → token-goat-chunk-OYIYKDDT.mjs} +2 -1
  108. package/dist/{token-goat-chunk-67BNIKKO.mjs → token-goat-chunk-P6HQSWQ3.mjs} +55 -47
  109. package/dist/token-goat-chunk-P6VGNTQT.mjs +71 -0
  110. package/dist/token-goat-chunk-PFWIMH2E.mjs +261 -0
  111. package/dist/token-goat-chunk-PJZXIS3G.mjs +39 -0
  112. package/dist/token-goat-chunk-PUO5INXP.mjs +606 -0
  113. package/dist/token-goat-chunk-PX5PQWTY.mjs +462 -0
  114. package/dist/token-goat-chunk-Q5QDBC6O.mjs +111 -0
  115. package/dist/token-goat-chunk-QFNQXSDT.mjs +40 -0
  116. package/dist/token-goat-chunk-QH7YVYOY.mjs +3957 -0
  117. package/dist/token-goat-chunk-QLMUHEPB.mjs +43 -0
  118. package/dist/{token-goat-chunk-PYSUADX4.mjs → token-goat-chunk-QQOJIHL3.mjs} +50 -14
  119. package/dist/token-goat-chunk-QQTWJY2O.mjs +85 -0
  120. package/dist/token-goat-chunk-QYHO5CPH.mjs +93 -0
  121. package/dist/token-goat-chunk-RELAVWAD.mjs +27 -0
  122. package/dist/token-goat-chunk-RSLPOBKQ.mjs +339 -0
  123. package/dist/token-goat-chunk-SAMFDIS2.mjs +712 -0
  124. package/dist/token-goat-chunk-SDELXLDM.mjs +48 -0
  125. package/dist/{token-goat-chunk-PGTH2U4C.mjs → token-goat-chunk-SOAFZNNZ.mjs} +164 -40
  126. package/dist/{token-goat-chunk-G23DWHV4.mjs → token-goat-chunk-SRTHT7KV.mjs} +33 -15
  127. package/dist/token-goat-chunk-T5P5ACNQ.mjs +212 -0
  128. package/dist/token-goat-chunk-T5YGG4V3.mjs +45 -0
  129. package/dist/token-goat-chunk-TAUZ4OFS.mjs +110 -0
  130. package/dist/token-goat-chunk-TM6MC754.mjs +40 -0
  131. package/dist/token-goat-chunk-TWUKBOMP.mjs +72 -0
  132. package/dist/token-goat-chunk-U2KSB4SG.mjs +56 -0
  133. package/dist/token-goat-chunk-U4WHMG6Y.mjs +898 -0
  134. package/dist/token-goat-chunk-UA5V6P3P.mjs +122 -0
  135. package/dist/token-goat-chunk-UGJHGR5A.mjs +52 -0
  136. package/dist/{token-goat-chunk-RQGGMPCR.mjs → token-goat-chunk-UOUDVA5U.mjs} +76 -16
  137. package/dist/{token-goat-chunk-HNFZZHFW.mjs → token-goat-chunk-URRJ33GR.mjs} +22 -4
  138. package/dist/token-goat-chunk-UZOROKIS.mjs +165 -0
  139. package/dist/token-goat-chunk-VIHGGGEI.mjs +4234 -0
  140. package/dist/token-goat-chunk-VTJLAA7P.mjs +21 -0
  141. package/dist/token-goat-chunk-VWDXLT4F.mjs +678 -0
  142. package/dist/token-goat-chunk-VWZA2H4P.mjs +368 -0
  143. package/dist/{token-goat-chunk-QO3LXVVR.mjs → token-goat-chunk-VXEXMJDL.mjs} +2964 -3153
  144. package/dist/token-goat-chunk-WBA5SKT4.mjs +547 -0
  145. package/dist/{token-goat-chunk-ONXH6NZ5.mjs → token-goat-chunk-WDH44VAU.mjs} +19 -22
  146. package/dist/token-goat-chunk-X2SUP3EI.mjs +107 -0
  147. package/dist/token-goat-chunk-X3BELZSJ.mjs +125 -0
  148. package/dist/{token-goat-chunk-RLP6J7WR.mjs → token-goat-chunk-XLRBNZCC.mjs} +40 -21
  149. package/dist/{token-goat-chunk-S4VSXURL.mjs → token-goat-chunk-XRKUX6CM.mjs} +4 -0
  150. package/dist/token-goat-chunk-XX6O25RN.mjs +82 -0
  151. package/dist/token-goat-chunk-XZSUHBVS.mjs +65 -0
  152. package/dist/token-goat-chunk-Y2ZTQOCL.mjs +35 -0
  153. package/dist/{token-goat-chunk-UA5BKWWQ.mjs → token-goat-chunk-YB4VMGK2.mjs} +15 -4
  154. package/dist/token-goat-chunk-YBIIY3HR.mjs +51 -0
  155. package/dist/{token-goat-chunk-PKRWMHVP.mjs → token-goat-chunk-YHVDKQBR.mjs} +389 -221
  156. package/dist/token-goat-chunk-YJB5XLWI.mjs +26 -0
  157. package/dist/{token-goat-chunk-PQ53BSCF.mjs → token-goat-chunk-YKB6AQG3.mjs} +53 -31
  158. package/dist/token-goat-chunk-YMTG2YA5.mjs +35 -0
  159. package/dist/token-goat-chunk-YPBZYQRV.mjs +40 -0
  160. package/dist/{token-goat-chunk-HUYUHODZ.mjs → token-goat-chunk-YY6AVUFL.mjs} +13246 -12247
  161. package/dist/token-goat-chunk-Z7N5OMMP.mjs +98 -0
  162. package/dist/{token-goat-chunk-BGHSSJUA.mjs → token-goat-chunk-ZFOBKUWH.mjs} +1 -1
  163. package/dist/token-goat-chunk-ZMBL6UVV.mjs +110 -0
  164. package/dist/token-goat-chunk-ZNMNUPAZ.mjs +938 -0
  165. package/dist/token-goat-hook-client.cjs +87 -23
  166. package/dist/token-goat-hook-client.mjs +3 -3
  167. package/dist/token-goat-hook.mjs +76 -31
  168. package/dist/token-goat.core.mjs +6 -6
  169. package/docs/C4_RUNTIME_ARCHITECTURE.md +2 -2
  170. package/docs/cli.md +113 -60
  171. package/docs/install.md +63 -20
  172. package/docs/security.md +10 -8
  173. package/package.json +18 -7
  174. package/scripts/verify-native-dist.mjs +386 -0
  175. package/dist/token-goat-chunk-2NNUA2LR.mjs +0 -143
  176. package/dist/token-goat-chunk-44BM7DDQ.mjs +0 -294
  177. package/dist/token-goat-chunk-BGYOZ6DO.mjs +0 -24
  178. package/dist/token-goat-chunk-C5YPNZKQ.mjs +0 -318
  179. package/dist/token-goat-chunk-CIQ2FTY3.mjs +0 -1990
  180. package/dist/token-goat-chunk-D4ETHJD6.mjs +0 -246
  181. package/dist/token-goat-chunk-DNRPXHYH.mjs +0 -1062
  182. package/dist/token-goat-chunk-EUSDXRXU.mjs +0 -1429
  183. package/dist/token-goat-chunk-FTP4U4QA.mjs +0 -64
  184. package/dist/token-goat-chunk-G7IWXCFF.mjs +0 -456
  185. package/dist/token-goat-chunk-GLXRTTLE.mjs +0 -36
  186. package/dist/token-goat-chunk-H5J3C56Q.mjs +0 -1511
  187. package/dist/token-goat-chunk-IVWI2OCK.mjs +0 -368
  188. package/dist/token-goat-chunk-JTKCQMQA.mjs +0 -418
  189. package/dist/token-goat-chunk-LZNBQTDK.mjs +0 -26
  190. package/dist/token-goat-chunk-ORNFMIER.mjs +0 -295
  191. package/dist/token-goat-chunk-RTMVLHWX.mjs +0 -7113
  192. package/dist/token-goat-chunk-VQ6F6T7X.mjs +0 -36
  193. package/dist/token-goat-chunk-VZQEWRVH.mjs +0 -312
  194. package/dist/token-goat-chunk-XYWPOSCN.mjs +0 -807
  195. package/dist/token-goat-chunk-YAGLESS3.mjs +0 -23
  196. package/dist/token-goat-chunk-YKSRAWUU.mjs +0 -33
  197. package/dist/token-goat-chunk-Z6KBMOBI.mjs +0 -53
@@ -0,0 +1,3647 @@
1
+ import { createRequire as __cjsRequire } from 'node:module';
2
+ const require = __cjsRequire(import.meta.url);
3
+ import {
4
+ compareSemver
5
+ } from "./token-goat-chunk-CXHOFN7R.mjs";
6
+ import {
7
+ emitErr,
8
+ jsonc,
9
+ stripBom
10
+ } from "./token-goat-chunk-QQTWJY2O.mjs";
11
+ import {
12
+ HOOKS_CONFIG_FILE,
13
+ copilotHooksOwnersPath,
14
+ readCopilotHooksOwners
15
+ } from "./token-goat-chunk-G5PJ7AGM.mjs";
16
+ import {
17
+ claudeConfigDir
18
+ } from "./token-goat-chunk-BPGCUMR7.mjs";
19
+ import {
20
+ toolMatcherFor
21
+ } from "./token-goat-chunk-C75NA7PL.mjs";
22
+ import {
23
+ COPILOT_CLI_TOOL_NAME_MAP,
24
+ copilotCliUserRoot,
25
+ foldToolName
26
+ } from "./token-goat-chunk-76FGDLRO.mjs";
27
+ import {
28
+ loadConfig,
29
+ nativeHooksEnabled,
30
+ parse,
31
+ stringify
32
+ } from "./token-goat-chunk-7EZTZOMC.mjs";
33
+ import {
34
+ registerReset
35
+ } from "./token-goat-chunk-YZAAZU2S.mjs";
36
+ import {
37
+ assertWriteInScope,
38
+ atomicWriteText,
39
+ backupFile,
40
+ commandHookFields,
41
+ ensureDirRecordingCreation,
42
+ ensureDirSync,
43
+ escapeRegExp,
44
+ extractErrorMessage,
45
+ hasCreatedConfig,
46
+ hookCommandFor,
47
+ hookExecPartsFor,
48
+ hookPowershellCommand,
49
+ isOwnHookEntry,
50
+ projectScopeRoot,
51
+ recordCreatedBy,
52
+ recordCreatedConfig,
53
+ recordCreatedRootKey,
54
+ removeCreatedBackups,
55
+ removeCreatedIfEmpty,
56
+ removeCreatedTree,
57
+ removeFileInScope,
58
+ stripDelimitedBlock,
59
+ stripOwnHooksFromMap,
60
+ stripStaleGroupHooks,
61
+ takeCreatedConfig,
62
+ takeCreatedRootKey,
63
+ upsertDelimitedBlock,
64
+ withInstallScope,
65
+ withRetryOnLock,
66
+ writeConfigText,
67
+ writeIfDifferent,
68
+ writeJsonSettings
69
+ } from "./token-goat-chunk-HU4TORC5.mjs";
70
+ import {
71
+ echoedValue,
72
+ fencedCommand
73
+ } from "./token-goat-chunk-GGGG7RWB.mjs";
74
+ import {
75
+ powershellHookLine,
76
+ quotePosixShellWord,
77
+ quotePowershellPath,
78
+ resolveOnPath,
79
+ spawnResolvedSync
80
+ } from "./token-goat-chunk-AM5V6GSX.mjs";
81
+ import {
82
+ normalizeDarwinSystemAlias
83
+ } from "./token-goat-chunk-FNFBKLRD.mjs";
84
+ import {
85
+ dataDir
86
+ } from "./token-goat-chunk-CO3UYX6G.mjs";
87
+ import {
88
+ init_define_import_meta_env
89
+ } from "./token-goat-chunk-A37V4PBF.mjs";
90
+
91
+ // src/bridges/claudecode.ts
92
+ init_define_import_meta_env();
93
+
94
+ // src/bridges/shim_common.ts
95
+ init_define_import_meta_env();
96
+
97
+ // src/bridges/shim_try_server.ts
98
+ init_define_import_meta_env();
99
+ var SHIM_TRY_SERVER = `// Attempts the resident hook server (src/hook_server.ts): the sibling hook client sends this
100
+ // call to an already-running token-goat process, so neither Node's module loading nor the hook
101
+ // graph is paid for again. The CommonJS client comes first: this shim is CommonJS, and loading
102
+ // the ES module one starts Node's ESM loader, about 5ms of a call the server answers. The ES
103
+ // module one is for an install built before the CommonJS file existed. It returns undefined
104
+ // whenever nothing was dispatched -- no server yet (it starts one in the background), all busy,
105
+ // replaced by a newer build, turned off, or an install predating the client -- and the caller
106
+ // then runs the call itself.
107
+ async function tryServer(entryPath, eventName, input, harnessWaitMs) {
108
+ if (!entryPath) return undefined
109
+ try {
110
+ const fs = require('node:fs')
111
+ const cjsPath = path.join(path.dirname(entryPath), 'token-goat-hook-client.cjs')
112
+ const mjsPath = path.join(path.dirname(entryPath), 'token-goat-hook-client.mjs')
113
+ const client = fs.existsSync(cjsPath) ? require(cjsPath) : fs.existsSync(mjsPath) ? await import(pathToFileURL(mjsPath).href) : undefined
114
+ if (client === undefined) return undefined
115
+ return await client.relayViaServer(eventName, input, harnessWaitMs)
116
+ } catch {
117
+ return undefined
118
+ }
119
+ }`;
120
+
121
+ // src/bridges/shim_common.ts
122
+ var SHIM_FILE = "token-goat-shim.cjs";
123
+ var LEGACY_SHIM_FILE = "token-goat-shim.js";
124
+ function legacyShimForwarder(noOp) {
125
+ const answer = noOp === "" ? "" : `
126
+ try { process.stdout.write('${noOp}') } catch {}`;
127
+ return `#!/usr/bin/env node
128
+ // Forwards to token-goat-shim.cjs for hook commands wired before the shim was renamed.
129
+ import('./token-goat-shim.cjs').catch(() => {${answer}
130
+ process.exitCode = 0
131
+ });
132
+ `;
133
+ }
134
+ var SHIM_REQUIRES = `const { spawnSync } = require('node:child_process')
135
+ const path = require('node:path')
136
+ const { pathToFileURL } = require('node:url')`;
137
+ var SHIM_VALID_HOOK_EVENTS = `// Keep in sync with HOOK_EVENTS in src/types.ts. eventName is validated against this closed
138
+ // set before being concatenated into a shell command string, so a hostile argv (e.g.
139
+ // 'pre_tool_use & calc.exe') can never reach the shell parser.
140
+ const VALID_HOOK_EVENTS = new Set([
141
+ 'pre_tool_use',
142
+ 'post_tool_use',
143
+ 'notification',
144
+ 'stop',
145
+ 'pre_compact',
146
+ 'post_compact',
147
+ 'user_prompt_submit',
148
+ 'subagent_stop',
149
+ 'subagent_start',
150
+ 'session_start',
151
+ 'post_tool_use_failure',
152
+ ])`;
153
+ var SHIM_TRY_IN_PROCESS = `// Attempts the in-process hook call: import()s dist/token-goat-hook.mjs (a sibling of
154
+ // the baked token-goat entry path, built with zero load-time side effects -- unlike
155
+ // the CLI entry, which runs the full argv-parsing CLI as a side effect of being
156
+ // loaded) and calls its exported relayInProcess() directly, avoiding a second node
157
+ // process spawn entirely. Returns undefined (triggering the spawnSync fallback below)
158
+ // when entryPath is absent, the sibling file doesn't exist (an older install predating
159
+ // this file), or anything else goes wrong -- this must never throw. harnessWaitMs, when
160
+ // the caller has one (Claude Code's async-detach marker time -- see SHIM_ASYNC_DETACH),
161
+ // is forwarded straight through: this runs in the same process as the caller, so its
162
+ // performance.now() reading shares the same origin and needs no conversion.
163
+ async function tryInProcess(entryPath, eventName, input, harnessWaitMs) {
164
+ if (!entryPath) return undefined
165
+ try {
166
+ const payload = JSON.parse(input)
167
+ const served = await tryServer(entryPath, eventName, input, harnessWaitMs)
168
+ if (served !== undefined) return served
169
+ const hookLibPath = path.join(path.dirname(entryPath), 'token-goat-hook.mjs')
170
+ if (!require('node:fs').existsSync(hookLibPath)) return undefined
171
+ const mod = await import(pathToFileURL(hookLibPath).href)
172
+ return await mod.relayInProcess(eventName, payload, harnessWaitMs)
173
+ } catch {
174
+ return undefined
175
+ }
176
+ }`;
177
+ var SHIM_SPAWN_LADDER = ` const res = entryPath
178
+ ? spawnSync(process.execPath, [entryPath, 'hook', eventName], {
179
+ input,
180
+ encoding: 'utf8',
181
+ timeout: SHIM_FALLBACK_TIMEOUT_MS,
182
+ killSignal: 'SIGKILL',
183
+ maxBuffer: SHIM_MAX_BUFFER_BYTES,
184
+ })`;
185
+ var SHIM_FALLBACK_TIMEOUT_MS = { claudecode: 2e4, codex: 45e3, copilot: 45e3, grok: 3e3, kimi: 2e4, pi: 2e4, relay: 1e4 };
186
+ var SHIM_SESSION_START_TIMEOUT_MS = 1e4;
187
+ var SESSION_START_TEST_BUDGET_ENV = "TG_TEST_SESSION_START_BUDGET_MS";
188
+ var SESSION_START_TEST_BUDGET_MIN_MS = 1e3;
189
+ var SESSION_START_TEST_BUDGET_MAX_MS = 6e5;
190
+ function shimFallbackTimeoutConst(harness) {
191
+ return `const SHIM_FALLBACK_TIMEOUT_MS = ${SHIM_FALLBACK_TIMEOUT_MS[harness]}`;
192
+ }
193
+ var SHIM_MAX_BUFFER_CONST = `const SHIM_MAX_BUFFER_BYTES = 32 * 1024 * 1024`;
194
+ var SHIM_ASYNC_DETACH = `const ASYNC_DETACH_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
195
+ const ASYNC_DETACH_SKIP_EXT_RE = /\\.(md|mdx|markdown|rst)$/i
196
+ const ASYNC_DETACH_BASH_MAX_BYTES = 200
197
+ // Same key order as OUTPUT_FIRST_TOOL_RESPONSE_KEYS in hooks_common.ts.
198
+ const ASYNC_DETACH_BASH_TEXT_KEYS = ['output', 'content', 'text', 'body', 'stdout', 'stderr']
199
+ function bashResultByteLength(resp) {
200
+ if (typeof resp === 'string') return Buffer.byteLength(resp, 'utf8')
201
+ if (resp && typeof resp === 'object') {
202
+ if (typeof resp['persistedOutputSize'] === 'number') return resp['persistedOutputSize']
203
+ for (const key of ASYNC_DETACH_BASH_TEXT_KEYS) {
204
+ if (typeof resp[key] === 'string' && resp[key] !== '') return Buffer.byteLength(resp[key], 'utf8')
205
+ }
206
+ return Buffer.byteLength(JSON.stringify(resp), 'utf8')
207
+ }
208
+ return 0
209
+ }
210
+ // A token-goat read|section|symbol result is never detached, however short: see isAsyncDetachEligible in src/hook_adapters.ts.
211
+ const TG_LEDGER_PREFIX_RE = /^(?:(?:cd\\s+(?:"[^"]*"|'[^']*'|\\S+)|export[ \\t]+[A-Za-z_]\\w*=(?:"[^"]*"|'[^']*'|[^\\s"'])*(?:[ \\t]+[A-Za-z_]\\w*=(?:"[^"]*"|'[^']*'|[^\\s"'])*)*)[ \\t]*(?:&&|;|\\r?\\n)\\s*)+/
212
+ const TG_LEDGER_READ_RE = /^(?:[A-Za-z_][A-Za-z0-9_]*=\\S+\\s+)*(?:env\\s+(?:(?:(?:-u|--unset)\\s+\\S+|-(?![A-Za-z]*C|-c)\\S*|[A-Za-z_][A-Za-z0-9_]*=\\S+)\\s+)*)?(?:token-goat|tg|node(?:\\.exe)?\\s+["']?(?:\\S*[\\\\/])?token-goat(?:\\.mjs)?["']?)\\s+(?:read|section|symbol)\\b/i
213
+ function isLedgerReadCommand(cmd) {
214
+ if (typeof cmd !== 'string') return false
215
+ const body = cmd.trim().replace(/^\\(\\s*/, '')
216
+ return TG_LEDGER_READ_RE.test((body.replace(TG_LEDGER_PREFIX_RE, '') || body).trim())
217
+ }
218
+ function isAsyncDetachEligible(eventName, input) {
219
+ if (eventName === 'subagent_stop') return true
220
+ if (eventName !== 'post_tool_use') return false
221
+ try {
222
+ const payload = JSON.parse(input)
223
+ const toolName = payload['tool_name']
224
+ if (ASYNC_DETACH_TOOLS.has(toolName)) {
225
+ const toolInput = payload['tool_input'] || {}
226
+ const filePath =
227
+ typeof toolInput['file_path'] === 'string'
228
+ ? toolInput['file_path']
229
+ : typeof toolInput['notebook_path'] === 'string'
230
+ ? toolInput['notebook_path']
231
+ : ''
232
+ return !ASYNC_DETACH_SKIP_EXT_RE.test(filePath)
233
+ }
234
+ if (toolName === 'Bash') return !isLedgerReadCommand((payload['tool_input'] || {})['command']) && bashResultByteLength(payload['tool_response']) < ASYNC_DETACH_BASH_MAX_BYTES
235
+ return false
236
+ } catch {
237
+ return false
238
+ }
239
+ }`;
240
+ var SHIM_OWN_COMMAND_BYPASS = `const TG_CD_PREFIX_RE = /^(?:cd\\s+(?:"[^"]*"|'[^']*'|\\S+)[ \\t]*(?:&&|;|\\r?\\n)\\s*)+/
241
+ const TG_OWN_COMMAND_RE = /^(?:[A-Za-z_][A-Za-z0-9_]*=\\S+\\s+)*(?:token-goat\\b|node(?:\\.exe)?\\s+["']?(?:\\S*[\\\\/])?token-goat(?:\\.mjs)?["']?\\b)/i
242
+ function isOwnTokenGoatCommand(cmd) {
243
+ if (typeof cmd !== 'string' || cmd.trim() === '') return false
244
+ const body = (cmd.replace(TG_CD_PREFIX_RE, '') || cmd).trim()
245
+ if (/[;&|\`]|\\$\\(/.test(body)) return false
246
+ return TG_OWN_COMMAND_RE.test(body)
247
+ }`;
248
+
249
+ // src/bridges/claudecode.ts
250
+ var CLAUDECODE_HOOK_SCRIPT = `#!/usr/bin/env node
251
+ // token-goat Claude Code hook shim. Reads the hook payload on stdin, forwards it to \`token-goat hook <event>\`, and relays the response on stdout.
252
+ 'use strict'
253
+ // Cache V8's compiled bytecode for the 3.4 MB hook bundle this shim import()s below. Without it
254
+ // every hook invocation recompiles that bundle from source, which profiled at ~40ms of the ~148ms
255
+ // a single hook takes -- and a hook fires on every tool call. Worth ~23ms per invocation, measured
256
+ // twice on the installed shim by interleaving both variants: 148ms to 125ms, and later 186ms to
257
+ // 163ms on a busier machine. The floor moves with load; the 23ms it removes does not.
258
+ //
259
+ // It has to be here rather than inside the bundle: a module is compiled before any of its own code
260
+ // runs, so a bundle cannot enable the cache for itself. This shim is small enough to compile in
261
+ // well under a millisecond, and the dynamic import() happens after this line, so the bundle is
262
+ // compiled with the cache already active. Same reason the MCP SDK could not be deferred from
263
+ // inside mcp_server.ts -- what a module can affect starts after it is already compiled.
264
+ //
265
+ // enableCompileCache landed in Node 22.1 and package.json allows >=22.0.0, so a miss is possible;
266
+ // it is also best-effort by nature (read-only cache dir, full disk). Swallow everything: this shim
267
+ // must never fail a tool call to save itself 23ms.
268
+ try { require('node:module').enableCompileCache() } catch { /* older Node, or an unwritable cache dir: run uncached */ }
269
+ ${SHIM_REQUIRES}
270
+
271
+ ${SHIM_VALID_HOOK_EVENTS}
272
+
273
+ ${SHIM_TRY_SERVER}
274
+
275
+ ${SHIM_TRY_IN_PROCESS}
276
+
277
+ ${SHIM_MAX_BUFFER_CONST}
278
+ ${shimFallbackTimeoutConst("claudecode")}
279
+
280
+ ${SHIM_ASYNC_DETACH}
281
+
282
+ ${SHIM_OWN_COMMAND_BYPASS}
283
+
284
+ async function main() {
285
+ const eventName = process.argv[2] || ''
286
+ if (!VALID_HOOK_EVENTS.has(eventName)) {
287
+ process.stdout.write('{}')
288
+ return
289
+ }
290
+ let input = ''
291
+ try {
292
+ input = require('node:fs').readFileSync(0, 'utf8')
293
+ } catch {
294
+ process.stdout.write('{}')
295
+ return
296
+ }
297
+ // A pre_tool_use Bash call that is token-goat's own CLI has nothing for the pipeline below to
298
+ // say, so this skips the round trip entirely rather than backgrounding it -- see
299
+ // isOwnTokenGoatCommand's docstring in shim_common.ts for the measured emission rate this
300
+ // forfeits.
301
+ if (eventName === 'pre_tool_use') {
302
+ try {
303
+ const payload = JSON.parse(input)
304
+ if (payload['tool_name'] === 'Bash' && isOwnTokenGoatCommand(payload['tool_input'] && payload['tool_input']['command'])) {
305
+ process.stdout.write('{}')
306
+ return
307
+ }
308
+ } catch {
309
+ // Fall through to normal handling on unparseable stdin.
310
+ }
311
+ }
312
+ // Print this before the in-process/spawn round trip below, not after: the harness detaches on
313
+ // the first stdout line it sees, and the whole point is not waiting out that round trip for a
314
+ // call that was always going to answer '{}'. asyncDetachAtMs, taken at this exact print, is the
315
+ // last instant this process's elapsed time and the harness's own wait are the same number --
316
+ // record it now so the finally block in relayInProcess (relay.ts) can report what Claude Code
317
+ // actually waited on instead of this call's full lifetime.
318
+ let asyncDetachAtMs
319
+ if (isAsyncDetachEligible(eventName, input)) {
320
+ asyncDetachAtMs = performance.now()
321
+ process.stdout.write('{"async":true}\\n')
322
+ }
323
+ // process.argv[3], when present, is the absolute path to the running token-goat CLI
324
+ // entry (mirrors the same argv[3]-baked-entry convention used by the Codex/Copilot
325
+ // CLI shims). Try the in-process hook lib first (tryInProcess above), which avoids
326
+ // spawning a second node process altogether; fall back to the entry path directly via
327
+ // process.execPath, then finally to the old PATH-based shell:true invocation (an
328
+ // older cached hook config with no argv[3], or a config wired directly to
329
+ // \`token-goat hook <event>\` rather than through this shim). A
330
+ // SHIM_FALLBACK_TIMEOUT_MS timeout/killSignal on both spawnSync fallbacks means token-goat degrades to its own
331
+ // fail-open '{}' rather than being force-killed by Claude Code's own hook timeout (30 s on UserPromptSubmit).
332
+ const entryPath = process.argv[3]
333
+ let stdout = await tryInProcess(entryPath, eventName, input, asyncDetachAtMs)
334
+ if (stdout === undefined) {
335
+ // Reaching here with asyncDetachAtMs set means tryInProcess failed after
336
+ // isAsyncDetachEligible had already parsed this same input successfully, so the cause is an
337
+ // install missing its dist/token-goat-hook.mjs sibling or a broken import -- never a bad
338
+ // payload. asyncDetachAtMs is this process's own elapsed time and has no meaning in the
339
+ // spawned child's clock, so it is deliberately not threaded across that boundary; the child
340
+ // falls back to timing its own lifetime, the same already-approximate number every hook got
341
+ // on this fallback path before async-detach existed.
342
+ ${SHIM_SPAWN_LADDER}
343
+ : spawnSync('token-goat hook ' + eventName, {
344
+ input,
345
+ encoding: 'utf8',
346
+ shell: true,
347
+ timeout: SHIM_FALLBACK_TIMEOUT_MS,
348
+ killSignal: 'SIGKILL',
349
+ maxBuffer: SHIM_MAX_BUFFER_BYTES,
350
+ })
351
+ if (res.status !== 0 || !res.stdout) {
352
+ process.stdout.write('{}')
353
+ return
354
+ }
355
+ stdout = res.stdout
356
+ }
357
+ process.stdout.write(stdout)
358
+ }
359
+
360
+ main().catch(() => {
361
+ process.stdout.write('{}')
362
+ })
363
+ `;
364
+
365
+ // src/bridges/guidance_block.ts
366
+ init_define_import_meta_env();
367
+ function buildGuidanceBody(fallbackToolClause, opts = {}) {
368
+ const gdrive = opts.gdrive !== false;
369
+ return [
370
+ "## token-goat",
371
+ "",
372
+ "**Gate \u2014 before every file read, answer one question first: is there a token-goat command that returns just what I need?** If yes, run it. A read tool invoked without answering the gate is a violation, not an oversight. The gate is per file: batched or parallel reads do not exempt it.",
373
+ "",
374
+ `This gate decides *whether* to reach for a read tool at all. ${fallbackToolClause} only pick the *fallback* once token-goat has been ruled out for this read \u2014 they never authorize skipping the gate.`,
375
+ "",
376
+ "Shell binaries and editor programs are commands run through the shell tool, never tool identifiers: keep them out of an agent's tools and allowed-tools lists, where loaders harvest the names and warn that each is unknown.",
377
+ "",
378
+ "Exemptions (gate passes, read directly): the file is under ~200 lines and you need all of it; it was never indexed (new, untracked, or generated this turn); it is a genuinely opaque binary (not an image); the target has no symbol handle (e.g. a literal mid-function).",
379
+ "",
380
+ "Failure shapes to catch yourself in, and the command that replaces each:",
381
+ '- not knowing which command answers a question you can already state in words \u2192 `answer "<question>"`, which runs the right command and refuses rather than guess',
382
+ '- a shell text search with context flags to find a function body \u2192 `read "file::symbol"`',
383
+ '- paging one function with view/view_range \u2192 `read "file::symbol"`',
384
+ '- issuing sequential or overlapping line-range reads across a file (paging) \u2192 `read "file::symbol"` / `outline file` / `skeleton file` / rg <pattern> <file>',
385
+ "- a read refused by a `[tg]` deny, retried with a shifted range, different lines, or another read tool \u2192 never retry reading the file at different lines; run the command the deny names or use bash-output / token-goat read",
386
+ '- reading a symbol plus chasing its callers and containing doc section as separate reads \u2192 `brief "file::symbol"`',
387
+ '- reading one heading of a large doc \u2192 `section "file::Heading"`',
388
+ "- searching for a symbol's callers \u2192 `refs file::symbol --callers`",
389
+ '- searching across symbols, headings, text, and concepts concurrently \u2192 `search "<query>"`',
390
+ '- searching for a *concept* rather than a literal string \u2192 `semantic "description"`',
391
+ "- re-reading output you already captured \u2192 `bash-output`/`web-output`/`mcp-output` by ID",
392
+ "- a directory listing or recursive wildcard walk to orient in an unfamiliar repo \u2192 `map --compact`",
393
+ "- pulling one value or subtree out of a JSON/YAML/XML file (manifest, lockfile, spec, config) \u2192 `json-query file 'a.b.c'` / `yaml-query file 'a.b.c'` / `xml-query file 'a.b.c'`",
394
+ "- opening an image to check its dimensions, format, or size \u2192 `image-meta file`",
395
+ "- opening a screenshot, diagram, or scan to read the text in it \u2192 `image-text file`",
396
+ "- running tests immediately on edited code without fast static validation \u2192 run language validator first (e.g. `ruff check --select F821 <file>`, `node --check`, `tsc --noEmit`) to catch missing imports and syntax errors before test execution",
397
+ "- passing concatenated or unquoted paths to shell tools \u2192 verify whitespace and argument separation in multi-path commands (e.g. `git add path1 path2`, never `git add path1path2`)",
398
+ "",
399
+ "Commands: `symbol NAME`, `changed --symbol`, `config-get file KEY`, `json-outline`/`yaml-outline`/`xml-outline`, " + (gdrive ? "`gdrive-sections <file-id>`, " : "") + "and `token-goat --help` for the rest.",
400
+ "",
401
+ "Sub-agent briefs must carry this gate verbatim: a sub-agent inherits none of this context and its reads spend the same token budget.",
402
+ "",
403
+ "`token-goat stats` \u2014 self-check. Flat counts during code work mean the gate is being skipped."
404
+ ].join("\n");
405
+ }
406
+ function skillDescriptionLine(gdrive) {
407
+ return `description: Use before reading whole files or grepping wide. token-goat commands (symbol, read, section, semantic, outline, skeleton, map, refs, changed, config-get, bash-output, web-output${gdrive ? ", gdrive-sections" : ""}) return narrow slices of code and docs at a fraction of the token cost.`;
408
+ }
409
+ function buildGuidanceBlock(h) {
410
+ return [h.beginMarker, buildGuidanceBody(h.fallbackToolClause, h.gdrive === void 0 ? {} : { gdrive: h.gdrive }), h.endMarker].join("\n");
411
+ }
412
+
413
+ // src/native_hook.ts
414
+ init_define_import_meta_env();
415
+ import { spawnSync } from "node:child_process";
416
+ import * as crypto from "node:crypto";
417
+ import * as fs from "node:fs";
418
+ import * as path from "node:path";
419
+ var NATIVE_TARGETS = /* @__PURE__ */ new Set(["win32-x64", "win32-arm64", "linux-x64", "linux-arm64"]);
420
+ var SELFTEST_TIMEOUT_MS = 5e3;
421
+ function exeName(platform = process.platform) {
422
+ return platform === "win32" ? "tg-hook.exe" : "tg-hook";
423
+ }
424
+ function packagedNativeBinary(entryPath = process.argv[1]) {
425
+ if (entryPath === void 0 || entryPath === "") return void 0;
426
+ const target = `${process.platform}-${process.arch}`;
427
+ if (!NATIVE_TARGETS.has(target)) return void 0;
428
+ const bin = path.join(path.dirname(entryPath), "native", target, exeName());
429
+ return fs.existsSync(bin) ? bin : void 0;
430
+ }
431
+ function nativeCopyPath(entryPath, home = dataDir()) {
432
+ let bundleDir = path.dirname(entryPath);
433
+ try {
434
+ bundleDir = fs.realpathSync.native(bundleDir);
435
+ } catch {
436
+ }
437
+ const id = crypto.createHash("sha256").update(bundleDir.toLowerCase()).digest("hex").slice(0, 16);
438
+ return path.join(home, "native", id, exeName("win32"));
439
+ }
440
+ var LEFTOVER = /^tg-hook\.exe\.[0-9a-f]+\.(?:old|new)$/;
441
+ function reclaimLeftovers(dir) {
442
+ let names;
443
+ try {
444
+ names = fs.readdirSync(dir);
445
+ } catch {
446
+ return;
447
+ }
448
+ for (const name of names) {
449
+ if (!LEFTOVER.test(name)) continue;
450
+ try {
451
+ fs.unlinkSync(path.join(dir, name));
452
+ } catch {
453
+ }
454
+ }
455
+ }
456
+ function syncNativeCopy(src, dest) {
457
+ let want;
458
+ try {
459
+ want = fs.readFileSync(src);
460
+ } catch {
461
+ return false;
462
+ }
463
+ try {
464
+ if (fs.readFileSync(dest).equals(want)) return true;
465
+ } catch {
466
+ }
467
+ const dir = path.dirname(dest);
468
+ const tag = crypto.randomBytes(6).toString("hex");
469
+ const staged = `${dest}.${tag}.new`;
470
+ try {
471
+ fs.mkdirSync(dir, { recursive: true });
472
+ reclaimLeftovers(dir);
473
+ fs.writeFileSync(staged, want, { mode: 493 });
474
+ } catch {
475
+ discardStaged(staged);
476
+ return false;
477
+ }
478
+ try {
479
+ fs.renameSync(staged, dest);
480
+ return true;
481
+ } catch {
482
+ }
483
+ const ready = `${dest}.${crypto.randomBytes(6).toString("hex")}.new`;
484
+ try {
485
+ withRetryOnLock(() => fs.renameSync(staged, ready));
486
+ } catch {
487
+ discardStaged(staged);
488
+ return false;
489
+ }
490
+ const aside = `${dest}.${tag}.old`;
491
+ let movedAside = false;
492
+ try {
493
+ if (fs.existsSync(dest)) {
494
+ withRetryOnLock(() => fs.renameSync(dest, aside));
495
+ movedAside = true;
496
+ }
497
+ withRetryOnLock(() => fs.renameSync(ready, dest));
498
+ return true;
499
+ } catch {
500
+ if (movedAside) {
501
+ try {
502
+ withRetryOnLock(() => fs.renameSync(aside, dest));
503
+ } catch {
504
+ }
505
+ }
506
+ discardStaged(ready);
507
+ return false;
508
+ }
509
+ }
510
+ function discardStaged(staged) {
511
+ try {
512
+ fs.rmSync(staged, { force: true });
513
+ } catch {
514
+ }
515
+ }
516
+ function nativeCopyCurrent(src, dest) {
517
+ try {
518
+ return fs.readFileSync(dest).equals(fs.readFileSync(src));
519
+ } catch {
520
+ return false;
521
+ }
522
+ }
523
+ var _selftests = /* @__PURE__ */ new Map();
524
+ function nativeSelftest(bin) {
525
+ const cached = _selftests.get(bin);
526
+ if (cached !== void 0) return cached;
527
+ let result;
528
+ const res = spawnSync(bin, ["--selftest"], { encoding: "utf8", timeout: SELFTEST_TIMEOUT_MS, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
529
+ if (res.error !== void 0) result = { ok: false, reason: res.error.code === "ETIMEDOUT" ? `no answer within ${SELFTEST_TIMEOUT_MS} ms` : res.error.message };
530
+ else if (res.status !== 0) result = { ok: false, reason: res.signal !== null ? `killed by ${res.signal}` : `exit code ${String(res.status)}` };
531
+ else result = { ok: true };
532
+ _selftests.set(bin, result);
533
+ return result;
534
+ }
535
+ var _decisions = /* @__PURE__ */ new Map();
536
+ var COPY_FAILED = "its copy could not be put in place in the data directory (a scanner or another process held the file)";
537
+ var COPY_STALE = "the data-directory copy is missing or differs from the packaged binary";
538
+ var TRANSIENT_WHY = /could not be put in place|no answer within/;
539
+ registerReset(() => {
540
+ _selftests.clear();
541
+ _decisions.clear();
542
+ });
543
+ function nativeHookBinary(entryPath = process.argv[1], opts = {}) {
544
+ if (entryPath === void 0 || entryPath === "" || !nativeHooksEnabled()) return void 0;
545
+ const sync = opts.sync !== false;
546
+ const key = `${String(sync)}\0${entryPath}`;
547
+ const cached = _decisions.get(key);
548
+ if (cached !== void 0) return cached ?? void 0;
549
+ const packaged = packagedNativeBinary(entryPath);
550
+ let bin;
551
+ let why;
552
+ if (packaged !== void 0) {
553
+ for (let attempt = 1; attempt <= 2 && bin === void 0; attempt++) {
554
+ if (attempt === 2) {
555
+ if (why === void 0 || !TRANSIENT_WHY.test(why)) break;
556
+ _selftests.clear();
557
+ }
558
+ why = void 0;
559
+ let candidate = packaged;
560
+ if (process.platform === "win32") {
561
+ const copy = nativeCopyPath(entryPath);
562
+ const current = sync ? syncNativeCopy(packaged, copy) : nativeCopyCurrent(packaged, copy);
563
+ if (current) candidate = copy;
564
+ else {
565
+ candidate = void 0;
566
+ why = sync ? COPY_FAILED : COPY_STALE;
567
+ }
568
+ }
569
+ if (candidate !== void 0) {
570
+ const test = nativeSelftest(candidate);
571
+ if (test.ok) bin = candidate;
572
+ else why = `its self-test failed: ${test.reason ?? "no reason given"}`;
573
+ }
574
+ }
575
+ }
576
+ if (bin === void 0 && sync && why !== void 0 && why !== COPY_STALE) emitErr(`token-goat: wrote the Node form of the hooks, not the native hook client, because ${why}. Run \`token-goat doctor --repair\` (or install again) to retry.`);
577
+ _decisions.set(key, bin ?? null);
578
+ if (sync) _decisions.set(`false\0${entryPath}`, bin ?? null);
579
+ return bin;
580
+ }
581
+ function nativeHookFlags(harness, event, entry, scriptPath) {
582
+ const flags = ["--harness", harness, "--event", event, "--entry", entry];
583
+ if (harness === "copilot_cli") flags.push("--script-dir", path.dirname(scriptPath));
584
+ return flags;
585
+ }
586
+ function nodeHookArgv(scriptPath, event, entry) {
587
+ if (entry === void 0 || entry === "") return void 0;
588
+ return [process.execPath, scriptPath, event, entry];
589
+ }
590
+ var PATH_FLAGS = /* @__PURE__ */ new Set(["--entry", "--script-dir"]);
591
+ function nativeWords(bin, harness, scriptPath, event, entry) {
592
+ const flags = nativeHookFlags(harness, event, entry, scriptPath);
593
+ const flagWords = flags.map((text, i) => ({ text, quote: i > 0 && PATH_FLAGS.has(flags[i - 1]) }));
594
+ return [{ text: bin, quote: true }, ...flagWords, { text: "--", quote: false }, { text: process.execPath, quote: true }, { text: scriptPath, quote: true }, { text: event, quote: false }, { text: entry, quote: true }];
595
+ }
596
+ function quoteFor(shell, text) {
597
+ if (shell === "sh") return quotePosixShellWord(text);
598
+ if (shell === "powershell") return quotePowershellPath(text);
599
+ return `"${text}"`;
600
+ }
601
+ function nativeHookCommandLine(shell, bin, harness, scriptPath, event, entry = process.argv[1]) {
602
+ if (nodeHookArgv(scriptPath, event, entry) === void 0) return void 0;
603
+ const line = nativeWords(bin, harness, scriptPath, event, entry).map((w) => w.quote ? quoteFor(shell, w.text) : w.text).join(" ");
604
+ return shell === "powershell" ? powershellHookLine(line) : line;
605
+ }
606
+ function nativeHookExecParts(bin, harness, scriptPath, event, entry = process.argv[1]) {
607
+ if (nodeHookArgv(scriptPath, event, entry) === void 0) return void 0;
608
+ return { command: bin, args: nativeWords(bin, harness, scriptPath, event, entry).slice(1).map((w) => w.text) };
609
+ }
610
+ function splitHookCommand(command, shell) {
611
+ const words = [];
612
+ let text = command.trim();
613
+ if (shell === "powershell" && text.startsWith("& ")) text = text.slice(2);
614
+ const posix = shell === "sh";
615
+ const dqEscapes = posix && process.platform !== "win32";
616
+ let cur = "";
617
+ let inWord = false;
618
+ for (let i = 0; i < text.length; i++) {
619
+ const ch = text[i];
620
+ if (ch === ";" && shell === "powershell") break;
621
+ if (/\s/.test(ch)) {
622
+ if (inWord) words.push(cur);
623
+ cur = "";
624
+ inWord = false;
625
+ continue;
626
+ }
627
+ inWord = true;
628
+ if (ch === "'" && shell !== "cmd") {
629
+ for (i++; i < text.length; i++) {
630
+ if (text[i] !== "'") cur += text[i];
631
+ else if (shell === "powershell" && text[i + 1] === "'") cur += text[++i];
632
+ else break;
633
+ }
634
+ continue;
635
+ }
636
+ if (ch === '"') {
637
+ for (i++; i < text.length && text[i] !== '"'; i++) {
638
+ if (dqEscapes && text[i] === "\\" && i + 1 < text.length) cur += text[++i];
639
+ else cur += text[i];
640
+ }
641
+ continue;
642
+ }
643
+ if (posix && ch === "\\" && i + 1 < text.length && (process.platform !== "win32" || text[i + 1] === "'")) {
644
+ cur += text[++i];
645
+ continue;
646
+ }
647
+ cur += ch;
648
+ }
649
+ if (inWord) words.push(cur);
650
+ return words;
651
+ }
652
+ var HookCommandRewriteError = class extends Error {
653
+ };
654
+ function hookCommandRewrittenSpan(command, reader) {
655
+ return (reader === "cmd" ? /%[^%\r\n]+%/ : /\$(?:[A-Za-z_][A-Za-z0-9_]*|\{[^}]*\})/).exec(command)?.[0];
656
+ }
657
+ function parseNativeInvocation(argv) {
658
+ const bin = argv[0];
659
+ if (bin === void 0 || !/^tg-hook(?:\.exe)?$/i.test(path.basename(bin.replace(/\\/g, "/")))) return void 0;
660
+ const split = argv.indexOf("--");
661
+ if (split < 0) return void 0;
662
+ const flags = argv.slice(1, split);
663
+ const h = flags.indexOf("--harness");
664
+ return { bin, harness: h >= 0 ? flags[h + 1] : void 0, wrapped: argv.slice(split + 1) };
665
+ }
666
+
667
+ // src/install.ts
668
+ init_define_import_meta_env();
669
+ import * as fs2 from "node:fs";
670
+ import * as path2 from "node:path";
671
+
672
+ // src/canonical_skill.ts
673
+ init_define_import_meta_env();
674
+ import { gunzipSync } from "node:zlib";
675
+ var CANONICAL_SKILL_GZIP_BASE64 = "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";
676
+ var CANONICAL_SKILL_MD = gunzipSync(Buffer.from(CANONICAL_SKILL_GZIP_BASE64, "base64")).toString("utf8");
677
+
678
+ // src/install.ts
679
+ var HOOK_EVENT_MAP = [
680
+ ["PreToolUse", "pre_tool_use"],
681
+ ["PostToolUse", "post_tool_use"],
682
+ // A failed call never reaches PostToolUse: Claude Code 2.1.281 sends a missing-file Read and a non-zero Bash exit here instead, carrying `error`, `is_interrupt` and `duration_ms` (captured, 2026-09-24).
683
+ ["PostToolUseFailure", "post_tool_use_failure"],
684
+ ["PreCompact", "pre_compact"],
685
+ ["PostCompact", "post_compact"],
686
+ ["UserPromptSubmit", "user_prompt_submit"],
687
+ ["Stop", "stop"],
688
+ ["SubagentStop", "subagent_stop"],
689
+ ["SessionStart", "session_start"]
690
+ ];
691
+ var SHIM_COMMAND_MARKER = "token-goat-shim";
692
+ var COMMAND_MARKER = "token-goat hook";
693
+ var LEGACY_COMMAND_MARKERS = ["tokenwise", "token_goat", "tg-hook", "token-goat-hook"];
694
+ function anchoredMarkerPattern(marker) {
695
+ const escaped = escapeRegExp(marker);
696
+ return new RegExp(`(?<![a-zA-Z0-9_-])${escaped}(?![a-zA-Z0-9_-])`);
697
+ }
698
+ var HOOK_MARKER_PATTERNS = [SHIM_COMMAND_MARKER, COMMAND_MARKER, ...LEGACY_COMMAND_MARKERS].map(anchoredMarkerPattern);
699
+ function isTokenGoatHookCommand(command, args) {
700
+ const haystack = args !== void 0 && args.length > 0 ? `${command} ${args.join(" ")}` : command;
701
+ return HOOK_MARKER_PATTERNS.some((pattern) => pattern.test(haystack));
702
+ }
703
+ var CLAUDE_EXEC_FORM_MIN_VERSION = "2.1.139";
704
+ var _claudeExecFormHooksSupported;
705
+ function claudeExecFormHooksSupported() {
706
+ const override = process.env["TOKEN_GOAT_CLAUDE_EXEC_FORM_HOOKS"];
707
+ if (override === "1") return true;
708
+ if (override === "0") return false;
709
+ if (_claudeExecFormHooksSupported !== void 0) return _claudeExecFormHooksSupported;
710
+ _claudeExecFormHooksSupported = probeClaudeExecFormHooksSupported();
711
+ return _claudeExecFormHooksSupported;
712
+ }
713
+ function probeClaudeExecFormHooksSupported() {
714
+ const resolved = resolveOnPath("claude");
715
+ if (resolved === null) return false;
716
+ try {
717
+ const result = spawnResolvedSync(resolved, ["--version"], { encoding: "utf-8", timeout: 5e3, windowsHide: true });
718
+ if (result.status !== 0) return false;
719
+ return execFormSupportedForVersionOutput(result.stdout ?? "");
720
+ } catch {
721
+ return false;
722
+ }
723
+ }
724
+ function execFormSupportedForVersionOutput(stdout) {
725
+ const m = /(\d+\.\d+\.\d+)/.exec(stdout);
726
+ if (!m) return false;
727
+ return compareSemver(m[1], CLAUDE_EXEC_FORM_MIN_VERSION) >= 0;
728
+ }
729
+ function expectedHookEntryFor(scriptPath, event, opts = {}) {
730
+ const bin = nativeHookBinary(process.argv[1], opts);
731
+ if (claudeExecFormHooksSupported()) return (bin === void 0 ? void 0 : nativeHookExecParts(bin, "claudecode", scriptPath, event)) ?? hookExecPartsFor(scriptPath, event);
732
+ return { command: (bin === void 0 ? void 0 : nativeHookCommandLine("sh", bin, "claudecode", scriptPath, event)) ?? hookCommandFor(scriptPath, event) };
733
+ }
734
+ function realPathOrNull(p) {
735
+ if (!path2.isAbsolute(p)) return null;
736
+ try {
737
+ return fs2.realpathSync.native(p);
738
+ } catch {
739
+ return null;
740
+ }
741
+ }
742
+ function sameHookWord(a, b) {
743
+ if (a === b) return true;
744
+ const realA = realPathOrNull(a);
745
+ return realA !== null && realA === realPathOrNull(b);
746
+ }
747
+ function stringFormHookWords(command) {
748
+ return splitHookCommand(command, "sh");
749
+ }
750
+ function hookEntryMatches(command, args, expected) {
751
+ const execForm = expected.args !== void 0;
752
+ if (!execForm && args !== void 0 && args.length > 0) return false;
753
+ const a = execForm ? [command, ...args ?? []] : stringFormHookWords(command);
754
+ const b = execForm ? [expected.command, ...expected.args] : stringFormHookWords(expected.command);
755
+ return a.length === b.length && a.every((v, i) => sameHookWord(v, b[i]));
756
+ }
757
+ function claudeHookScriptPath() {
758
+ return path2.join(claudeConfigDir(), "hooks", SHIM_FILE);
759
+ }
760
+ function claudeLegacyHookScriptPath() {
761
+ return path2.join(claudeConfigDir(), "hooks", LEGACY_SHIM_FILE);
762
+ }
763
+ function anyScopeReferencesShim(scriptPaths, currentScope, alreadyStripped) {
764
+ const referencesIn = (map) => {
765
+ for (const groups of Object.values(map ?? {})) {
766
+ for (const group of Array.isArray(groups) ? groups : []) {
767
+ for (const h of hooksOf(group)) {
768
+ const fields = commandHookFields(h);
769
+ if (fields !== void 0 && scriptPaths.some((p) => fields.command.includes(p) || (fields.args ?? []).some((a) => a.includes(p)))) return true;
770
+ }
771
+ }
772
+ }
773
+ return false;
774
+ };
775
+ if (referencesIn(alreadyStripped)) return true;
776
+ for (const scope of ["user", "project"]) {
777
+ if (scope === currentScope) continue;
778
+ let other;
779
+ try {
780
+ other = readSettings(settingsPath(scope), { strict: true });
781
+ } catch (e) {
782
+ if (e instanceof SettingsParseError) return true;
783
+ throw e;
784
+ }
785
+ if (referencesIn(other.hooks)) return true;
786
+ }
787
+ return false;
788
+ }
789
+ function settingsPath(scope) {
790
+ const base = scope === "user" ? claudeConfigDir() : path2.join(normalizeDarwinSystemAlias(process.cwd()), ".claude");
791
+ return path2.join(base, "settings.json");
792
+ }
793
+ var SettingsParseError = class extends Error {
794
+ };
795
+ function readSettings(p, opts = {}) {
796
+ const refuse = (problem, detail) => new SettingsParseError(
797
+ (opts.command === "uninstall" ? `settings file ${echoedValue(p)} is unreadable: it ${problem}. Uninstall left it untouched, along with the hook shim it may still name and the file's backups; fix the file and run uninstall again.` : `settings file ${echoedValue(p)} ${problem}. Fix or back up the file before running install.`) + (detail === void 0 ? "" : ` (${detail})`)
798
+ );
799
+ let raw;
800
+ try {
801
+ raw = fs2.readFileSync(p, "utf8");
802
+ } catch (e) {
803
+ const code = e.code;
804
+ if (opts.strict === true && code !== "ENOENT" && code !== "ENOTDIR") throw refuse("exists but cannot be read", code ?? extractErrorMessage(e));
805
+ return {};
806
+ }
807
+ let parsed;
808
+ try {
809
+ parsed = JSON.parse(stripBom(raw));
810
+ } catch {
811
+ if (opts.strict === true) throw refuse("exists but contains invalid JSON");
812
+ return {};
813
+ }
814
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
815
+ const hooksValue = parsed.hooks;
816
+ if (hooksValue === void 0 || typeof hooksValue === "object" && hooksValue !== null && !Array.isArray(hooksValue)) return parsed;
817
+ if (opts.strict === true) throw refuse('has a "hooks" value that is not a JSON object');
818
+ const { hooks: _ignored, ...rest } = parsed;
819
+ return rest;
820
+ }
821
+ if (opts.strict === true) throw refuse("does not contain a JSON object at the top level");
822
+ return {};
823
+ }
824
+ function wiredClaudeHookWords(scope) {
825
+ const out = [];
826
+ for (const groups of Object.values(readSettings(settingsPath(scope)).hooks ?? {})) {
827
+ for (const g of Array.isArray(groups) ? groups : []) {
828
+ for (const h of Array.isArray(g?.hooks) ? g.hooks : []) {
829
+ const fields = commandHookFields(h);
830
+ if (fields === void 0 || !isTokenGoatHookCommand(fields.command, fields.args)) continue;
831
+ out.push(fields.args !== void 0 && fields.args.length > 0 ? [fields.command, ...fields.args] : stringFormHookWords(fields.command));
832
+ }
833
+ }
834
+ }
835
+ return out;
836
+ }
837
+ function hooksOf(group) {
838
+ return Array.isArray(group?.hooks) ? group.hooks : [];
839
+ }
840
+ function groupHasTokenGoat(groups, predicate) {
841
+ if (!Array.isArray(groups)) return false;
842
+ for (const group of groups) {
843
+ for (const h of hooksOf(group)) {
844
+ if (isOwnHookEntry(h, predicate)) return true;
845
+ }
846
+ }
847
+ return false;
848
+ }
849
+ function installHooks(scope = "user") {
850
+ return withInstallScope(hookScopeRoot(scope), () => installHooksScoped(scope));
851
+ }
852
+ function hookScopeRoot(scope) {
853
+ return scope === "project" ? normalizeDarwinSystemAlias(process.cwd()) : void 0;
854
+ }
855
+ function installHooksScoped(scope) {
856
+ const p = settingsPath(scope);
857
+ const settings = readSettings(p, { strict: true });
858
+ const hooks = settings.hooks ?? {};
859
+ const scriptPath = claudeHookScriptPath();
860
+ const scriptChanged = withInstallScope(void 0, () => {
861
+ ensureDirRecordingCreation(path2.dirname(scriptPath));
862
+ const forwarderChanged = writeIfDifferent(claudeLegacyHookScriptPath(), legacyShimForwarder("{}"));
863
+ const scriptChanged2 = writeIfDifferent(scriptPath, CLAUDECODE_HOOK_SCRIPT);
864
+ return scriptChanged2 || forwarderChanged;
865
+ });
866
+ let settingsChanged = false;
867
+ const skippedEvents = [];
868
+ for (const [eventKey, eventArg] of HOOK_EVENT_MAP) {
869
+ const expected = expectedHookEntryFor(scriptPath, eventArg);
870
+ const rawGroups = hooks[eventKey];
871
+ if (rawGroups !== void 0 && !Array.isArray(rawGroups)) {
872
+ skippedEvents.push(eventKey);
873
+ continue;
874
+ }
875
+ const existingGroups = rawGroups ?? [];
876
+ const groups = [];
877
+ let strippedStale = false;
878
+ for (const group of existingGroups) {
879
+ if (typeof group !== "object" || group === null) {
880
+ groups.push(group);
881
+ continue;
882
+ }
883
+ const keptHooks = hooksOf(group).filter((h) => {
884
+ const isStale = isOwnHookEntry(h, (command, args) => isTokenGoatHookCommand(command, args) && !hookEntryMatches(command, args, expected));
885
+ if (isStale) strippedStale = true;
886
+ return !isStale;
887
+ });
888
+ if (keptHooks.length > 0) {
889
+ groups.push({ ...group, hooks: keptHooks });
890
+ } else if (hooksOf(group).length === 0) {
891
+ groups.push(group);
892
+ }
893
+ }
894
+ const isOurs = (command, args) => hookEntryMatches(command, args, expected);
895
+ if (groupHasTokenGoat(groups, isOurs)) {
896
+ const narrowed = toolMatcherFor(eventArg);
897
+ let renarrowed = false;
898
+ if (narrowed !== null) {
899
+ for (let i = 0; i < groups.length; i++) {
900
+ const group = groups[i];
901
+ if (group === void 0) continue;
902
+ const ownHooks = hooksOf(group);
903
+ const isOwnGroup = ownHooks.length > 0 && ownHooks.every((h) => isOwnHookEntry(h, isOurs));
904
+ if (isOwnGroup && group.matcher !== narrowed) {
905
+ groups[i] = { ...group, matcher: narrowed };
906
+ renarrowed = true;
907
+ }
908
+ }
909
+ }
910
+ if (strippedStale || renarrowed) {
911
+ hooks[eventKey] = groups;
912
+ settingsChanged = true;
913
+ }
914
+ continue;
915
+ }
916
+ const matcher = toolMatcherFor(eventArg) ?? "";
917
+ groups.push({ matcher, hooks: [{ type: "command", command: expected.command, ...expected.args !== void 0 ? { args: expected.args } : {} }] });
918
+ hooks[eventKey] = groups;
919
+ settingsChanged = true;
920
+ }
921
+ if (skippedEvents.length > 0) {
922
+ process.stderr.write(`token-goat: left ${p} untouched under hooks.${skippedEvents.join(", hooks.")}: not a list of hook groups, so no token-goat hook was added there. Fix the value and run install again.
923
+ `);
924
+ }
925
+ if (!settingsChanged && !scriptChanged) {
926
+ return { scope, settingsPath: p, alreadyInstalled: true, ...skippedEvents.length > 0 ? { skippedEvents } : {} };
927
+ }
928
+ if (settingsChanged) {
929
+ settings.hooks = hooks;
930
+ recordCreatedBy([p], () => writeJsonSettings(p, settings));
931
+ }
932
+ return { scope, settingsPath: p, alreadyInstalled: false, ...skippedEvents.length > 0 ? { skippedEvents } : {} };
933
+ }
934
+ function uninstallHooks(scope = "user") {
935
+ return withInstallScope(hookScopeRoot(scope), () => uninstallHooksScoped(scope));
936
+ }
937
+ function uninstallHooksScoped(scope) {
938
+ const p = settingsPath(scope);
939
+ const settings = readSettings(p, { strict: true, command: "uninstall" });
940
+ const hooks = settings.hooks ?? {};
941
+ const removed = stripOwnHooksFromMap(hooks, isTokenGoatHookCommand);
942
+ const scriptPath = claudeHookScriptPath();
943
+ let removedScript = false;
944
+ if (!anyScopeReferencesShim([scriptPath, claudeLegacyHookScriptPath()], scope, hooks)) {
945
+ removedScript = withInstallScope(void 0, () => {
946
+ const removedAny = [scriptPath, claudeLegacyHookScriptPath()].map((p2) => removeFileInScope(p2)).some(Boolean);
947
+ removeCreatedIfEmpty(path2.dirname(scriptPath));
948
+ return removedAny;
949
+ });
950
+ }
951
+ const result = finishHookUninstall(p, settings, removed) || removedScript;
952
+ removeCreatedTree([p]);
953
+ return result;
954
+ }
955
+ function finishHookUninstall(p, settings, removed, write = writeJsonSettings) {
956
+ if (removed) {
957
+ if (settings.hooks !== void 0 && Object.keys(settings.hooks).length === 0) delete settings.hooks;
958
+ write(p, settings);
959
+ }
960
+ removeCreatedBackups(p);
961
+ return removed;
962
+ }
963
+ function isInstalled(scope = "user") {
964
+ if (!fs2.existsSync(claudeHookScriptPath())) return false;
965
+ const gaps = hookEventGaps(scope);
966
+ return gaps !== null && gaps.missing.length === 0 && gaps.outdated.length === 0 && gaps.broken.length === 0;
967
+ }
968
+ function hasFiringHook(scope) {
969
+ const gaps = hookEventGaps(scope);
970
+ return gaps !== null && gaps.missing.length + gaps.broken.length < HOOK_EVENT_MAP.length;
971
+ }
972
+ function claudeHooksInstalledAnyScope() {
973
+ return hasFiringHook("user") || hasFiringHook("project");
974
+ }
975
+ var LIVE_HOOK_MARKER_PATTERNS = [SHIM_COMMAND_MARKER, COMMAND_MARKER].map(anchoredMarkerPattern);
976
+ var SHIM_FILE_WORD = /(?:^|[\\/])token-goat-shim\.c?js$/;
977
+ function isLiveTokenGoatHookCommand(command, args) {
978
+ const execForm = args !== void 0 && args.length > 0;
979
+ const haystack = execForm ? `${command} ${args.join(" ")}` : command;
980
+ if (!LIVE_HOOK_MARKER_PATTERNS.some((pattern) => pattern.test(haystack))) return false;
981
+ const words = execForm ? [command, ...args] : stringFormHookWords(command);
982
+ const native = parseNativeInvocation(words);
983
+ if (native !== void 0 && !fs2.existsSync(native.bin)) return false;
984
+ const shimFile = words.find((word) => SHIM_FILE_WORD.test(word));
985
+ return shimFile === void 0 || fs2.existsSync(shimFile);
986
+ }
987
+ function isBrokenNativeHookCommand(command, args) {
988
+ const words = args !== void 0 && args.length > 0 ? [command, ...args] : stringFormHookWords(command);
989
+ const native = parseNativeInvocation(words);
990
+ return native !== void 0 && native.wrapped.some((word) => SHIM_FILE_WORD.test(word)) && !fs2.existsSync(native.bin);
991
+ }
992
+ function hookEventGaps(scope = "user") {
993
+ const hooks = readSettings(settingsPath(scope)).hooks;
994
+ if (hooks === void 0) return null;
995
+ if (!HOOK_EVENT_MAP.some(([eventKey]) => groupHasTokenGoat(hooks[eventKey], isTokenGoatHookCommand))) return null;
996
+ const scriptPath = claudeHookScriptPath();
997
+ const missing = [];
998
+ const outdated = [];
999
+ const broken = [];
1000
+ for (const [eventKey, eventArg] of HOOK_EVENT_MAP) {
1001
+ const expected = expectedHookEntryFor(scriptPath, eventArg, { sync: false });
1002
+ if (groupHasTokenGoat(hooks[eventKey], (c, a) => hookEntryMatches(c, a, expected) && isLiveTokenGoatHookCommand(c, a))) continue;
1003
+ if (groupHasTokenGoat(hooks[eventKey], isLiveTokenGoatHookCommand)) outdated.push(eventKey);
1004
+ else if (groupHasTokenGoat(hooks[eventKey], isBrokenNativeHookCommand)) broken.push(eventKey);
1005
+ else missing.push(eventKey);
1006
+ }
1007
+ return { missing, outdated, broken };
1008
+ }
1009
+ var CLAUDE_MD_BEGIN = "<!-- token-goat-begin -->";
1010
+ var CLAUDE_MD_END = "<!-- token-goat-end -->";
1011
+ function claudeMdPath(scope = "user") {
1012
+ return scope === "user" ? path2.join(claudeConfigDir(), "CLAUDE.md") : path2.join(normalizeDarwinSystemAlias(process.cwd()), "CLAUDE.md");
1013
+ }
1014
+ function buildClaudeMdBlock() {
1015
+ return buildGuidanceBlock({
1016
+ beginMarker: CLAUDE_MD_BEGIN,
1017
+ endMarker: CLAUDE_MD_END,
1018
+ fallbackToolClause: "Claude Code's own Read, Grep, and Glob preference rules",
1019
+ gdrive: loadConfig().gdrive.enabled
1020
+ });
1021
+ }
1022
+ function writeClaudeMdBlock(p) {
1023
+ return upsertDelimitedBlock(p, CLAUDE_MD_BEGIN, CLAUDE_MD_END, buildClaudeMdBlock());
1024
+ }
1025
+ function stripClaudeMdBlock(p) {
1026
+ return stripDelimitedBlock(p, CLAUDE_MD_BEGIN, CLAUDE_MD_END);
1027
+ }
1028
+ function installedClaudeMdBlock() {
1029
+ let text;
1030
+ try {
1031
+ text = fs2.readFileSync(claudeMdPath(), "utf8");
1032
+ } catch {
1033
+ return null;
1034
+ }
1035
+ const begin = text.indexOf(CLAUDE_MD_BEGIN);
1036
+ if (begin === -1) return null;
1037
+ const end = text.indexOf(CLAUDE_MD_END, begin + CLAUDE_MD_BEGIN.length);
1038
+ return end === -1 ? null : text.slice(begin, end + CLAUDE_MD_END.length);
1039
+ }
1040
+ function installClaudeMd(scope = "user") {
1041
+ return withInstallScope(hookScopeRoot(scope), () => {
1042
+ const p = claudeMdPath(scope);
1043
+ const existed = fs2.existsSync(p);
1044
+ const changed = writeClaudeMdBlock(p);
1045
+ if (!existed) recordCreatedConfig(p);
1046
+ return { path: p, alreadyInstalled: !changed };
1047
+ });
1048
+ }
1049
+ function uninstallClaudeMd(scope = "user") {
1050
+ return withInstallScope(hookScopeRoot(scope), () => {
1051
+ const p = claudeMdPath(scope);
1052
+ const stripped = stripClaudeMdBlock(p);
1053
+ removeCreatedIfEmpty(p);
1054
+ return stripped;
1055
+ });
1056
+ }
1057
+ function findStrayClaudeMdBlocks(searchRoot) {
1058
+ const root = searchRoot ?? claudeConfigDir();
1059
+ const canonical = path2.resolve(claudeMdPath());
1060
+ const found = [];
1061
+ if (!fs2.existsSync(root)) return found;
1062
+ const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
1063
+ const MAX_DEPTH = 6;
1064
+ const hasRealBlock = (text) => {
1065
+ let sawBegin = false;
1066
+ for (const line of text.split(/\r?\n/)) {
1067
+ const trimmed = line.trim();
1068
+ if (trimmed === CLAUDE_MD_BEGIN) sawBegin = true;
1069
+ else if (sawBegin && trimmed === CLAUDE_MD_END) return true;
1070
+ }
1071
+ return false;
1072
+ };
1073
+ const walk = (dir, depth) => {
1074
+ if (depth > MAX_DEPTH) return;
1075
+ let entries;
1076
+ try {
1077
+ entries = fs2.readdirSync(dir, { withFileTypes: true });
1078
+ } catch {
1079
+ return;
1080
+ }
1081
+ for (const entry of entries) {
1082
+ const full = path2.join(dir, entry.name);
1083
+ if (entry.isDirectory()) {
1084
+ if (!SKIP_DIRS.has(entry.name)) walk(full, depth + 1);
1085
+ continue;
1086
+ }
1087
+ if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
1088
+ if (path2.resolve(full) === canonical) continue;
1089
+ let text;
1090
+ try {
1091
+ text = fs2.readFileSync(full, "utf8");
1092
+ } catch {
1093
+ continue;
1094
+ }
1095
+ if (hasRealBlock(text)) found.push(full);
1096
+ }
1097
+ };
1098
+ walk(root, 0);
1099
+ return found.sort();
1100
+ }
1101
+ function skillMdContent() {
1102
+ return CANONICAL_SKILL_MD;
1103
+ }
1104
+ function skillDir(scope = "user") {
1105
+ return path2.join(scope === "user" ? claudeConfigDir() : path2.join(normalizeDarwinSystemAlias(process.cwd()), ".claude"), "skills", "token-goat");
1106
+ }
1107
+ function skillPath(scope = "user") {
1108
+ return path2.join(skillDir(scope), "SKILL.md");
1109
+ }
1110
+ function installedSkillDescription() {
1111
+ let text;
1112
+ try {
1113
+ text = fs2.readFileSync(skillPath(), "utf8");
1114
+ } catch {
1115
+ return null;
1116
+ }
1117
+ const lines = text.split(/\r?\n/);
1118
+ if (lines[0] !== "---") return null;
1119
+ for (const line of lines.slice(1)) {
1120
+ if (line === "---") return null;
1121
+ if (line.startsWith("description:")) return line;
1122
+ }
1123
+ return null;
1124
+ }
1125
+ function installSkill(scope = "user") {
1126
+ return withInstallScope(hookScopeRoot(scope), () => installSkillScoped(scope));
1127
+ }
1128
+ function installSkillScoped(scope) {
1129
+ const p = skillPath(scope);
1130
+ let existing = null;
1131
+ try {
1132
+ existing = fs2.readFileSync(p, "utf8");
1133
+ } catch {
1134
+ }
1135
+ const skillContent = skillMdContent();
1136
+ if (existing === skillContent) return { path: p, alreadyInstalled: true };
1137
+ recordCreatedBy([skillDir(scope)], () => ensureDirSync(skillDir(scope)));
1138
+ backupFile(p);
1139
+ atomicWriteText(p, skillContent);
1140
+ return { path: p, alreadyInstalled: false };
1141
+ }
1142
+ function uninstallSkill(scope = "user") {
1143
+ return withInstallScope(hookScopeRoot(scope), () => uninstallSkillScoped(scope));
1144
+ }
1145
+ function uninstallSkillScoped(scope) {
1146
+ const dir = skillDir(scope);
1147
+ const present = fs2.existsSync(dir);
1148
+ if (present) {
1149
+ removeCreatedBackups(skillPath(scope));
1150
+ assertWriteInScope(dir);
1151
+ fs2.rmSync(dir, { recursive: true, force: true });
1152
+ }
1153
+ removeCreatedTree([dir]);
1154
+ return present;
1155
+ }
1156
+
1157
+ // src/bridges/codex.ts
1158
+ init_define_import_meta_env();
1159
+ var CODEX_HOOK_SCRIPT = `#!/usr/bin/env node
1160
+ // token-goat Codex hook shim. Forwards the hook payload to \`token-goat hook <event>\`, then strips _tg_* keys and injects hookEventName so the response satisfies Codex's strict (additionalProperties:false) schema.
1161
+ 'use strict'
1162
+ ${SHIM_REQUIRES}
1163
+
1164
+ ${SHIM_VALID_HOOK_EVENTS}
1165
+
1166
+ // Codex sets no env var of its own identifying a hook subprocess; see this module's docblock.
1167
+ process.env.TOKEN_GOAT_HARNESS_OVERRIDE = 'codex'
1168
+
1169
+ // Keep in sync with CLAUDE_CODE_EVENT_NAMES in src/hook_registry.ts -- the
1170
+ // live/wired 'token-goat hook <event>' path (src/relay.ts) always emits this
1171
+ // PascalCase spelling for hookSpecificOutput.hookEventName, never the raw
1172
+ // snake_case event name.
1173
+ const HOOK_EVENT_NAME_MAP = {
1174
+ pre_tool_use: 'PreToolUse',
1175
+ post_tool_use: 'PostToolUse',
1176
+ notification: 'Notification',
1177
+ stop: 'Stop',
1178
+ pre_compact: 'PreCompact',
1179
+ post_compact: 'PostCompact',
1180
+ user_prompt_submit: 'UserPromptSubmit',
1181
+ subagent_stop: 'SubagentStop',
1182
+ subagent_start: 'SubagentStart',
1183
+ session_start: 'SessionStart',
1184
+ post_tool_use_failure: 'PostToolUseFailure',
1185
+ }
1186
+
1187
+ function stripTg(value) {
1188
+ if (Array.isArray(value)) return value.map(stripTg)
1189
+ if (value && typeof value === 'object') {
1190
+ const out = {}
1191
+ for (const [k, v] of Object.entries(value)) {
1192
+ if (k.startsWith('_tg_')) continue
1193
+ out[k] = stripTg(v)
1194
+ }
1195
+ return out
1196
+ }
1197
+ return value
1198
+ }
1199
+
1200
+ ${SHIM_TRY_SERVER}
1201
+
1202
+ ${SHIM_TRY_IN_PROCESS}
1203
+
1204
+ ${SHIM_MAX_BUFFER_CONST}
1205
+ ${shimFallbackTimeoutConst("codex")}
1206
+
1207
+ async function main() {
1208
+ const eventName = process.argv[2] || ''
1209
+ if (!VALID_HOOK_EVENTS.has(eventName)) {
1210
+ process.stdout.write('{}')
1211
+ return
1212
+ }
1213
+ let input = ''
1214
+ try {
1215
+ input = require('node:fs').readFileSync(0, 'utf8')
1216
+ } catch {
1217
+ process.stdout.write('{}')
1218
+ return
1219
+ }
1220
+ // process.argv[3], when present, is the absolute path to the token-goat CLI entry
1221
+ // that ran 'token-goat install --codex' (baked in by hookCommandFor in
1222
+ // codex_install.ts). Try the in-process hook lib first (tryInProcess above) --
1223
+ // this avoids spawning a second node process altogether. If that's unavailable,
1224
+ // invoking the entry directly via process.execPath sidesteps PATH/shell resolution
1225
+ // for this inner call, the same single-point-of-failure class fixed for the Copilot
1226
+ // CLI bridge's inner hook call (a bare 'token-goat' on PATH failing to resolve
1227
+ // crashes this call, and Codex -- like Copilot -- fails closed on a non-zero-exit
1228
+ // hook). Falls back further to the old PATH-based shell:true invocation when argv[3]
1229
+ // is absent (an older cached hook config). A SHIM_FALLBACK_TIMEOUT_MS timeout/killSignal on both
1230
+ // spawnSync fallbacks bounds them well under Codex's own hook timeout budget, so
1231
+ // token-goat degrades to its own fail-open '{}' rather than being force-killed by
1232
+ // Codex first.
1233
+ const entryPath = process.argv[3]
1234
+ let stdout = await tryInProcess(entryPath, eventName, input)
1235
+ if (stdout === undefined) {
1236
+ ${SHIM_SPAWN_LADDER}
1237
+ : spawnSync('token-goat hook ' + eventName, {
1238
+ input,
1239
+ encoding: 'utf8',
1240
+ shell: true,
1241
+ timeout: SHIM_FALLBACK_TIMEOUT_MS,
1242
+ killSignal: 'SIGKILL',
1243
+ maxBuffer: SHIM_MAX_BUFFER_BYTES,
1244
+ })
1245
+ if (res.status !== 0 || !res.stdout) {
1246
+ process.stdout.write('{}')
1247
+ return
1248
+ }
1249
+ stdout = res.stdout
1250
+ }
1251
+ let parsed
1252
+ try {
1253
+ parsed = JSON.parse(stdout)
1254
+ } catch {
1255
+ process.stdout.write('{}')
1256
+ return
1257
+ }
1258
+ parsed = stripTg(parsed)
1259
+ const hso = parsed && parsed.hookSpecificOutput
1260
+ if (hso && typeof hso === 'object' && !hso.hookEventName) {
1261
+ hso.hookEventName = HOOK_EVENT_NAME_MAP[eventName] || eventName
1262
+ }
1263
+ process.stdout.write(JSON.stringify(parsed))
1264
+ }
1265
+
1266
+ main().catch(() => {
1267
+ process.stdout.write('{}')
1268
+ })
1269
+ `;
1270
+
1271
+ // src/bridges/codex_install.ts
1272
+ init_define_import_meta_env();
1273
+ import * as crypto2 from "node:crypto";
1274
+ import * as fs3 from "node:fs";
1275
+ import * as os from "node:os";
1276
+ import * as path3 from "node:path";
1277
+
1278
+ // src/bridges/matcher_group.ts
1279
+ init_define_import_meta_env();
1280
+ function findTokenGoatEntryPosition(groups, matcher, predicate) {
1281
+ if (groups === void 0) return void 0;
1282
+ for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
1283
+ const group = groups[groupIndex];
1284
+ if (group.matcher !== matcher) continue;
1285
+ const hookList = group.hooks ?? [];
1286
+ for (let hookIndex = 0; hookIndex < hookList.length; hookIndex++) {
1287
+ if (predicate(hookList[hookIndex].command)) return { groupIndex, hookIndex };
1288
+ }
1289
+ }
1290
+ return void 0;
1291
+ }
1292
+ function findAnyTokenGoatEntryPosition(groups, predicate) {
1293
+ if (groups === void 0) return void 0;
1294
+ for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
1295
+ const hookList = groups[groupIndex].hooks ?? [];
1296
+ for (let hookIndex = 0; hookIndex < hookList.length; hookIndex++) {
1297
+ if (predicate(hookList[hookIndex].command)) return { groupIndex, hookIndex };
1298
+ }
1299
+ }
1300
+ return void 0;
1301
+ }
1302
+ function groupHasTokenGoat2(groups, matcher, predicate) {
1303
+ return findTokenGoatEntryPosition(groups, matcher, predicate) !== void 0;
1304
+ }
1305
+
1306
+ // src/bridges/codex_install.ts
1307
+ var CODEX_COMMAND_MARKER = "token-goat-shim";
1308
+ var CODEX_MATCHERS = ["view_image|Bash|exec|shell|bash", "apply_patch", "web_search", "spawn_agent|multi_agent_v1wait_agent"];
1309
+ var CODEX_HOOK_EVENTS = ["PreToolUse", "PostToolUse"];
1310
+ var CODEX_EVENT_ARG = {
1311
+ PreToolUse: "pre_tool_use",
1312
+ PostToolUse: "post_tool_use"
1313
+ };
1314
+ var CODEX_GLOBAL_HOOK_EVENTS = ["PreCompact", "UserPromptSubmit", "Stop", "SubagentStop"];
1315
+ var CODEX_GLOBAL_EVENT_ARG = {
1316
+ PreCompact: "pre_compact",
1317
+ UserPromptSubmit: "user_prompt_submit",
1318
+ Stop: "stop",
1319
+ SubagentStop: "subagent_stop"
1320
+ };
1321
+ var CodexConfigParseError = class extends Error {
1322
+ };
1323
+ function codexHomeDir() {
1324
+ const override = process.env["CODEX_HOME"];
1325
+ if (override !== void 0 && override !== "") return path3.resolve(override);
1326
+ return path3.join(os.homedir(), ".codex");
1327
+ }
1328
+ function codexConfigPath() {
1329
+ return path3.join(codexHomeDir(), "config.toml");
1330
+ }
1331
+ function codexAgentsPath() {
1332
+ return path3.join(codexHomeDir(), "AGENTS.md");
1333
+ }
1334
+ function codexHookScriptPath() {
1335
+ return path3.join(codexHomeDir(), "hooks", SHIM_FILE);
1336
+ }
1337
+ function codexLegacyHookScriptPath() {
1338
+ return path3.join(codexHomeDir(), "hooks", LEGACY_SHIM_FILE);
1339
+ }
1340
+ function readCodexConfig(p, opts = {}) {
1341
+ const refuse = (problem, detail) => new CodexConfigParseError(
1342
+ (opts.command === "uninstall" ? `Codex config file ${echoedValue(p)} is unreadable: it ${problem}. Uninstall left it untouched, along with the hook shim it may still name, the AGENTS.md block and the file's backups; fix the file and run uninstall again.` : `Codex config file ${echoedValue(p)} ${problem}. Fix or back up the file before running install.`) + (detail === void 0 ? "" : ` (${detail})`)
1343
+ );
1344
+ let raw;
1345
+ try {
1346
+ raw = fs3.readFileSync(p, "utf8");
1347
+ } catch (e) {
1348
+ const code = e.code;
1349
+ if (opts.strict === true && code !== "ENOENT" && code !== "ENOTDIR") throw refuse("exists but cannot be read", code ?? extractErrorMessage(e));
1350
+ return {};
1351
+ }
1352
+ try {
1353
+ const parsed = parse(raw);
1354
+ return parsed;
1355
+ } catch (e) {
1356
+ if (opts.strict === true) throw refuse("exists but contains invalid TOML", extractErrorMessage(e));
1357
+ return {};
1358
+ }
1359
+ }
1360
+ var CODEX_MARKER_PATTERN = anchoredMarkerPattern(CODEX_COMMAND_MARKER);
1361
+ function isCodexTokenGoatCommand(command) {
1362
+ return typeof command === "string" && CODEX_MARKER_PATTERN.test(command);
1363
+ }
1364
+ function anyGroupHasTokenGoat(groups, predicate = isCodexTokenGoatCommand) {
1365
+ return findAnyTokenGoatEntryPosition(groups, predicate) !== void 0;
1366
+ }
1367
+ function codexHookCommandFor(scriptPath, eventArg, opts = {}) {
1368
+ const bin = nativeHookBinary(process.argv[1], opts);
1369
+ const native = bin === void 0 ? void 0 : nativeHookCommandLine(process.platform === "win32" ? "powershell" : "sh", bin, "codex", scriptPath, eventArg);
1370
+ if (native !== void 0) return native;
1371
+ return process.platform === "win32" ? powershellHookLine(hookPowershellCommand(scriptPath, eventArg)) : hookCommandFor(scriptPath, eventArg);
1372
+ }
1373
+ function computeCodexHookHash(eventArg, command, matcher) {
1374
+ const norm = {
1375
+ event_name: eventArg,
1376
+ hooks: [
1377
+ {
1378
+ async: false,
1379
+ command,
1380
+ timeout: 600,
1381
+ type: "command"
1382
+ }
1383
+ ]
1384
+ };
1385
+ if (matcher !== void 0) {
1386
+ norm["matcher"] = matcher;
1387
+ }
1388
+ const serialized = JSON.stringify(norm);
1389
+ const hash = crypto2.createHash("sha256").update(serialized).digest("hex");
1390
+ return `sha256:${hash}`;
1391
+ }
1392
+ var MANAGED_BEGIN = "# >>> token-goat codex hooks (managed block; token-goat uninstall removes it) >>>";
1393
+ var MANAGED_BEGIN_NO_EOL = "# >>> token-goat codex hooks (managed block; token-goat uninstall removes it; file had no final newline) >>>";
1394
+ var MANAGED_END = "# <<< token-goat codex hooks <<<";
1395
+ function removeManagedBlock(text) {
1396
+ for (const begin of [MANAGED_BEGIN_NO_EOL, MANAGED_BEGIN]) {
1397
+ const start = text.indexOf(begin);
1398
+ if (start < 0 || start > 0 && text[start - 1] !== "\n") continue;
1399
+ const endMark = text.indexOf(MANAGED_END, start);
1400
+ if (endMark < 0) return void 0;
1401
+ const nl = text.indexOf("\n", endMark);
1402
+ const end = nl < 0 ? text.length : nl + 1;
1403
+ const sep = start === 0 ? 0 : begin === MANAGED_BEGIN_NO_EOL ? 2 : 1;
1404
+ if (text.slice(start - sep, start) !== "\n".repeat(sep)) return void 0;
1405
+ return text.slice(0, start - sep) + text.slice(end);
1406
+ }
1407
+ return void 0;
1408
+ }
1409
+ function canonicalToml(v) {
1410
+ if (Array.isArray(v)) return `[${v.map(canonicalToml).join(",")}]`;
1411
+ if (v instanceof Date) return `d${v.toISOString()}`;
1412
+ if (typeof v === "bigint") return `n${v}`;
1413
+ if (typeof v === "object" && v !== null) {
1414
+ return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonicalToml(v[k])}`).join(",")}}`;
1415
+ }
1416
+ return JSON.stringify(v) ?? "undefined";
1417
+ }
1418
+ function appendManagedBlock(original, target) {
1419
+ const base = removeManagedBlock(original) ?? original;
1420
+ try {
1421
+ const baseHooks = parse(base).hooks ?? {};
1422
+ const targetHooks = target.hooks ?? {};
1423
+ const added = {};
1424
+ for (const [event, groups] of Object.entries(targetHooks)) {
1425
+ if (event === "state" || !Array.isArray(groups)) continue;
1426
+ const have = Array.isArray(baseHooks[event]) ? baseHooks[event].length : 0;
1427
+ if (groups.length > have) added[event] = groups.slice(have);
1428
+ }
1429
+ const baseState = baseHooks["state"] ?? {};
1430
+ const addedState = Object.fromEntries(Object.entries(targetHooks["state"] ?? {}).filter(([k]) => !(k in baseState)));
1431
+ if (Object.keys(addedState).length > 0) added["state"] = addedState;
1432
+ let candidate = base;
1433
+ if (Object.keys(added).length > 0) {
1434
+ const noEol = base.length > 0 && !base.endsWith("\n");
1435
+ const lead = base.length === 0 ? "" : noEol ? "\n\n" : "\n";
1436
+ candidate = `${base}${lead}${noEol ? MANAGED_BEGIN_NO_EOL : MANAGED_BEGIN}
1437
+ ${stringify({ hooks: added }).trimEnd()}
1438
+ ${MANAGED_END}
1439
+ `;
1440
+ }
1441
+ return canonicalToml(parse(candidate)) === canonicalToml(target) ? candidate : void 0;
1442
+ } catch {
1443
+ return void 0;
1444
+ }
1445
+ }
1446
+ function removeManagedBlockText(original, target, rekey) {
1447
+ const without = removeManagedBlock(original);
1448
+ if (without === void 0) return void 0;
1449
+ const renamed = without.replace(/^([ \t]*)\[hooks\.state\.("(?:[^"\\\n]|\\.)*"|'[^'\n]*')\]([ \t]*(?:#[^\r\n]*)?)(\r?)$/gm, (whole, indent, quoted, tail, cr) => {
1450
+ let key;
1451
+ try {
1452
+ key = quoted.startsWith('"') ? JSON.parse(quoted) : quoted.slice(1, -1);
1453
+ } catch {
1454
+ return whole;
1455
+ }
1456
+ const next = rekey.get(key);
1457
+ return next === void 0 ? whole : `${indent}[hooks.state.${JSON.stringify(next)}]${tail}${cr}`;
1458
+ });
1459
+ try {
1460
+ return canonicalToml(parse(renamed)) === canonicalToml(target) ? renamed : void 0;
1461
+ } catch {
1462
+ return void 0;
1463
+ }
1464
+ }
1465
+ function codexEventLabel(event) {
1466
+ const known = CODEX_EVENT_ARG[event] ?? CODEX_GLOBAL_EVENT_ARG[event];
1467
+ return known ?? event.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
1468
+ }
1469
+ function planStateCleanup(configPath, hooks) {
1470
+ const ownKeys = /* @__PURE__ */ new Set();
1471
+ const ownHashes = /* @__PURE__ */ new Set();
1472
+ const rekey = /* @__PURE__ */ new Map();
1473
+ for (const [event, groups] of Object.entries(hooks)) {
1474
+ if (event === "state" || !Array.isArray(groups)) continue;
1475
+ const label = codexEventLabel(event);
1476
+ const keyOf = (g, h) => `${configPath}:${label}:${g}:${h}`;
1477
+ let newGroup = 0;
1478
+ groups.forEach((group, gi) => {
1479
+ const list = Array.isArray(group?.hooks) ? group.hooks : [];
1480
+ const matcher = typeof group?.matcher === "string" ? group.matcher : void 0;
1481
+ const mine = list.map((h) => isCodexTokenGoatCommand(h?.command));
1482
+ list.forEach((h, hi) => {
1483
+ if (!mine[hi]) return;
1484
+ ownKeys.add(keyOf(gi, hi));
1485
+ ownHashes.add(computeCodexHookHash(label, h.command, matcher));
1486
+ ownHashes.add(computeCodexHookHash(label, h.command));
1487
+ });
1488
+ const kept = mine.filter((m) => !m).length;
1489
+ if (kept === 0 && list.length > 0) return;
1490
+ let newHook = 0;
1491
+ list.forEach((_h, hi) => {
1492
+ if (mine[hi]) return;
1493
+ if (newGroup !== gi || newHook !== hi) rekey.set(keyOf(gi, hi), keyOf(newGroup, newHook));
1494
+ newHook++;
1495
+ });
1496
+ newGroup++;
1497
+ });
1498
+ }
1499
+ return { ownKeys, ownHashes, rekey };
1500
+ }
1501
+ function installCodex() {
1502
+ return recordCreatedBy(codexArtifactPaths(), installCodexFiles);
1503
+ }
1504
+ function codexArtifactPaths() {
1505
+ return [codexConfigPath(), codexAgentsPath(), codexHookScriptPath(), codexLegacyHookScriptPath()];
1506
+ }
1507
+ function installCodexFiles() {
1508
+ const configPath = codexConfigPath();
1509
+ const agentsPath = codexAgentsPath();
1510
+ const scriptPath = codexHookScriptPath();
1511
+ ensureDirSync(path3.dirname(scriptPath));
1512
+ atomicWriteText(scriptPath, CODEX_HOOK_SCRIPT);
1513
+ atomicWriteText(codexLegacyHookScriptPath(), legacyShimForwarder("{}"));
1514
+ const config = readCodexConfig(configPath, { strict: true });
1515
+ const hooks = config.hooks ?? {};
1516
+ let hooksChanged = false;
1517
+ const staleStateKeysToRemove = [];
1518
+ for (const event of CODEX_HOOK_EVENTS) {
1519
+ const eventArg = CODEX_EVENT_ARG[event];
1520
+ const expectedCommand = codexHookCommandFor(scriptPath, eventArg);
1521
+ const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
1522
+ let removedGroups = 0;
1523
+ for (let idx = 0; idx < groups.length; idx++) {
1524
+ const g = groups[idx];
1525
+ if (g.matcher === void 0 || CODEX_MATCHERS.includes(g.matcher)) continue;
1526
+ const hookList = g.hooks ?? [];
1527
+ const keptHooks = hookList.filter((h) => !isCodexTokenGoatCommand(h.command));
1528
+ if (keptHooks.length === hookList.length) continue;
1529
+ const originalIdx = idx + removedGroups;
1530
+ hookList.forEach((h, hi) => {
1531
+ if (isCodexTokenGoatCommand(h.command)) staleStateKeysToRemove.push(`${configPath}:${eventArg}:${originalIdx}:${hi}`);
1532
+ });
1533
+ hooksChanged = true;
1534
+ if (keptHooks.length > 0) {
1535
+ groups[idx] = { ...g, hooks: keptHooks };
1536
+ } else {
1537
+ groups.splice(idx, 1);
1538
+ idx--;
1539
+ removedGroups++;
1540
+ }
1541
+ }
1542
+ for (const matcher of CODEX_MATCHERS) {
1543
+ if (groupHasTokenGoat2(groups, matcher, (c) => c === expectedCommand)) continue;
1544
+ const nextGroups = stripStaleGroupHooks(groups, isCodexTokenGoatCommand, { matcher });
1545
+ groups.length = 0;
1546
+ groups.push(...nextGroups);
1547
+ groups.push({ matcher, hooks: [{ type: "command", command: expectedCommand }] });
1548
+ hooksChanged = true;
1549
+ }
1550
+ hooks[event] = groups;
1551
+ }
1552
+ for (const event of CODEX_GLOBAL_HOOK_EVENTS) {
1553
+ const eventArg = CODEX_GLOBAL_EVENT_ARG[event];
1554
+ const expectedCommand = codexHookCommandFor(scriptPath, eventArg);
1555
+ const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
1556
+ if (anyGroupHasTokenGoat(groups, (c) => c === expectedCommand)) {
1557
+ hooks[event] = groups;
1558
+ continue;
1559
+ }
1560
+ const nextGroups = stripStaleGroupHooks(groups, isCodexTokenGoatCommand);
1561
+ nextGroups.push({ hooks: [{ type: "command", command: expectedCommand }] });
1562
+ hooks[event] = nextGroups;
1563
+ hooksChanged = true;
1564
+ }
1565
+ const agentsChanged = writeAgentsBlock(agentsPath);
1566
+ const hooksState = hooks["state"] ?? {};
1567
+ for (const key of staleStateKeysToRemove) {
1568
+ if (key in hooksState) {
1569
+ delete hooksState[key];
1570
+ hooksChanged = true;
1571
+ }
1572
+ }
1573
+ for (const event of CODEX_HOOK_EVENTS) {
1574
+ const eventArg = CODEX_EVENT_ARG[event];
1575
+ const groups = hooks[event] ?? [];
1576
+ for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
1577
+ const g = groups[groupIndex];
1578
+ if (g && g.matcher && CODEX_MATCHERS.includes(g.matcher)) {
1579
+ const hookList = g.hooks ?? [];
1580
+ for (let hookIndex = 0; hookIndex < hookList.length; hookIndex++) {
1581
+ const h = hookList[hookIndex];
1582
+ if (h && isCodexTokenGoatCommand(h.command)) {
1583
+ const stateKey = `${configPath}:${eventArg}:${groupIndex}:${hookIndex}`;
1584
+ const hash = computeCodexHookHash(eventArg, h.command, g.matcher);
1585
+ if (hooksState[stateKey]?.trusted_hash !== hash) {
1586
+ hooksState[stateKey] = { ...hooksState[stateKey], trusted_hash: hash };
1587
+ hooksChanged = true;
1588
+ }
1589
+ }
1590
+ }
1591
+ }
1592
+ }
1593
+ }
1594
+ for (const event of CODEX_GLOBAL_HOOK_EVENTS) {
1595
+ const eventArg = CODEX_GLOBAL_EVENT_ARG[event];
1596
+ const groups = hooks[event] ?? [];
1597
+ for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
1598
+ const g = groups[groupIndex];
1599
+ if (!g) continue;
1600
+ const hookList = g.hooks ?? [];
1601
+ for (let hookIndex = 0; hookIndex < hookList.length; hookIndex++) {
1602
+ const h = hookList[hookIndex];
1603
+ if (h && isCodexTokenGoatCommand(h.command)) {
1604
+ const stateKey = `${configPath}:${eventArg}:${groupIndex}:${hookIndex}`;
1605
+ const hash = computeCodexHookHash(eventArg, h.command);
1606
+ if (hooksState[stateKey]?.trusted_hash !== hash) {
1607
+ hooksState[stateKey] = { ...hooksState[stateKey], trusted_hash: hash };
1608
+ hooksChanged = true;
1609
+ }
1610
+ }
1611
+ }
1612
+ }
1613
+ }
1614
+ if (Object.keys(hooksState).length > 0) {
1615
+ hooks["state"] = hooksState;
1616
+ }
1617
+ if (hooksChanged) {
1618
+ config.hooks = hooks;
1619
+ ensureDirSync(path3.dirname(configPath));
1620
+ backupFile(configPath);
1621
+ const before = fs3.existsSync(configPath) ? fs3.readFileSync(configPath, "utf8") : "";
1622
+ writeConfigText(configPath, appendManagedBlock(before, config) ?? stringify(config));
1623
+ }
1624
+ return {
1625
+ configPath,
1626
+ agentsPath,
1627
+ hookScriptPath: scriptPath,
1628
+ alreadyInstalled: !hooksChanged && !agentsChanged
1629
+ };
1630
+ }
1631
+ function uninstallCodex() {
1632
+ const configPath = codexConfigPath();
1633
+ const agentsPath = codexAgentsPath();
1634
+ const scriptPath = codexHookScriptPath();
1635
+ let removedAny = false;
1636
+ const config = readCodexConfig(configPath, { strict: true, command: "uninstall" });
1637
+ const hooks = config.hooks;
1638
+ let keepBackups = false;
1639
+ if (hooks !== void 0) {
1640
+ const plan = planStateCleanup(configPath, structuredClone(hooks));
1641
+ const hooksRemoved = stripOwnHooksFromMap(hooks, isCodexTokenGoatCommand);
1642
+ const hooksState = hooks["state"];
1643
+ let stateChanged = false;
1644
+ if (hooksState && typeof hooksState === "object") {
1645
+ const kept = [];
1646
+ const moved = [];
1647
+ for (const [key, value] of Object.entries(hooksState)) {
1648
+ const hash = value?.trusted_hash;
1649
+ if (plan.ownKeys.has(key) || typeof hash === "string" && plan.ownHashes.has(hash)) {
1650
+ stateChanged = true;
1651
+ continue;
1652
+ }
1653
+ const next = plan.rekey.get(key);
1654
+ if (next === void 0) kept.push([key, value]);
1655
+ else {
1656
+ moved.push([next, value]);
1657
+ stateChanged = true;
1658
+ }
1659
+ }
1660
+ for (const k of Object.keys(hooksState)) delete hooksState[k];
1661
+ for (const [k, v] of [...kept, ...moved]) hooksState[k] = v;
1662
+ if (Object.keys(hooksState).length === 0) {
1663
+ delete hooks["state"];
1664
+ }
1665
+ }
1666
+ if (hooksRemoved || stateChanged) {
1667
+ if (Object.keys(hooks).length === 0) {
1668
+ delete config.hooks;
1669
+ } else {
1670
+ config.hooks = hooks;
1671
+ }
1672
+ backupFile(configPath);
1673
+ const before = fs3.readFileSync(configPath, "utf8");
1674
+ const text = removeManagedBlockText(before, config, plan.rekey);
1675
+ if (text === void 0) {
1676
+ try {
1677
+ keepBackups = stringify(parse(before)) !== before;
1678
+ } catch {
1679
+ keepBackups = true;
1680
+ }
1681
+ }
1682
+ writeConfigText(configPath, text ?? stringify(config));
1683
+ removedAny = true;
1684
+ }
1685
+ }
1686
+ if (stripAgentsBlock(agentsPath)) {
1687
+ removedAny = true;
1688
+ }
1689
+ for (const p of [scriptPath, codexLegacyHookScriptPath()]) {
1690
+ try {
1691
+ fs3.unlinkSync(p);
1692
+ removedAny = true;
1693
+ } catch {
1694
+ }
1695
+ }
1696
+ if (!keepBackups) removeCreatedBackups(configPath);
1697
+ removeCreatedTree(codexArtifactPaths());
1698
+ return removedAny;
1699
+ }
1700
+ function wiredCodexHookWords() {
1701
+ const hooks = readCodexConfig(codexConfigPath()).hooks ?? {};
1702
+ const scriptPath = codexHookScriptPath();
1703
+ const expected = new Set([...Object.values(CODEX_EVENT_ARG), ...Object.values(CODEX_GLOBAL_EVENT_ARG)].map((arg) => codexHookCommandFor(scriptPath, arg, { sync: false })));
1704
+ const out = [];
1705
+ for (const [event, groups] of Object.entries(hooks)) {
1706
+ if (event === "state" || !Array.isArray(groups)) continue;
1707
+ for (const g of groups) {
1708
+ for (const h of Array.isArray(g?.hooks) ? g.hooks : []) {
1709
+ if (isCodexTokenGoatCommand(h?.command)) out.push({ words: splitHookCommand(h.command, process.platform === "win32" ? "powershell" : "sh"), current: expected.has(h.command) });
1710
+ }
1711
+ }
1712
+ }
1713
+ return out;
1714
+ }
1715
+ function isCodexInstalled() {
1716
+ const configPath = codexConfigPath();
1717
+ const config = readCodexConfig(configPath);
1718
+ const hooks = config.hooks;
1719
+ if (hooks === void 0) return false;
1720
+ const scriptPath = codexHookScriptPath();
1721
+ const hooksState = hooks["state"] ?? {};
1722
+ for (const event of CODEX_HOOK_EVENTS) {
1723
+ const eventArg = CODEX_EVENT_ARG[event];
1724
+ const expectedCommand = codexHookCommandFor(scriptPath, eventArg, { sync: false });
1725
+ const groups = hooks[event] ?? [];
1726
+ for (const matcher of CODEX_MATCHERS) {
1727
+ const position = findTokenGoatEntryPosition(groups, matcher, (c) => c === expectedCommand);
1728
+ if (position === void 0) return false;
1729
+ const stateKey = `${configPath}:${eventArg}:${position.groupIndex}:${position.hookIndex}`;
1730
+ const expectedHash = computeCodexHookHash(eventArg, expectedCommand, matcher);
1731
+ if (hooksState[stateKey]?.trusted_hash !== expectedHash) return false;
1732
+ }
1733
+ }
1734
+ for (const event of CODEX_GLOBAL_HOOK_EVENTS) {
1735
+ const eventArg = CODEX_GLOBAL_EVENT_ARG[event];
1736
+ const expectedCommand = codexHookCommandFor(scriptPath, eventArg, { sync: false });
1737
+ const groups = hooks[event] ?? [];
1738
+ const position = findAnyTokenGoatEntryPosition(groups, (c) => c === expectedCommand);
1739
+ if (position === void 0) return false;
1740
+ const stateKey = `${configPath}:${eventArg}:${position.groupIndex}:${position.hookIndex}`;
1741
+ const expectedHash = computeCodexHookHash(eventArg, expectedCommand);
1742
+ if (hooksState[stateKey]?.trusted_hash !== expectedHash) return false;
1743
+ }
1744
+ if (!fs3.existsSync(scriptPath)) return false;
1745
+ let agents;
1746
+ try {
1747
+ agents = fs3.readFileSync(codexAgentsPath(), "utf8");
1748
+ } catch {
1749
+ return false;
1750
+ }
1751
+ return agents.includes(AGENTS_BEGIN) && agents.includes(AGENTS_END);
1752
+ }
1753
+ var AGENTS_BEGIN = "<!-- token-goat-codex-begin -->";
1754
+ var AGENTS_END = "<!-- token-goat-codex-end -->";
1755
+ function buildAgentsBlock() {
1756
+ return buildGuidanceBlock({
1757
+ beginMarker: AGENTS_BEGIN,
1758
+ endMarker: AGENTS_END,
1759
+ fallbackToolClause: "Codex's native `exec`, `apply_patch`, and `view_image` tools (shell commands like `cat`/`type` run inside `exec`)",
1760
+ gdrive: loadConfig().gdrive.enabled
1761
+ });
1762
+ }
1763
+ function writeAgentsBlock(p) {
1764
+ return upsertDelimitedBlock(p, AGENTS_BEGIN, AGENTS_END, buildAgentsBlock());
1765
+ }
1766
+ function stripAgentsBlock(p) {
1767
+ return stripDelimitedBlock(p, AGENTS_BEGIN, AGENTS_END);
1768
+ }
1769
+
1770
+ // src/bridges/copilot_cli.ts
1771
+ init_define_import_meta_env();
1772
+
1773
+ // src/bridges/copilot_session_start.ts
1774
+ init_define_import_meta_env();
1775
+ var COPILOT_SESSION_START_HELPERS = `
1776
+ const SHIM_SESSION_START_TIMEOUT_MS = ${SHIM_SESSION_START_TIMEOUT_MS}
1777
+
1778
+ // Test-only seam, mirrored by the native owner: a plain decimal in range replaces the budget so a test need not race the clock; anything else is ignored.
1779
+ function sessionStartBudgetMs() {
1780
+ const text = process.env.${SESSION_START_TEST_BUDGET_ENV}
1781
+ if (typeof text !== 'string' || !/^[0-9]+$/.test(text)) return SHIM_SESSION_START_TIMEOUT_MS
1782
+ const value = Number(text)
1783
+ return value >= ${SESSION_START_TEST_BUDGET_MIN_MS} && value <= ${SESSION_START_TEST_BUDGET_MAX_MS} ? value : SHIM_SESSION_START_TIMEOUT_MS
1784
+ }
1785
+ const SESSION_START_CLEANUP_RESERVE_MS = 1000
1786
+ // How long past the deadline the shim keeps its stdin listener for a payload already in the pipe: on Windows the pipe read completes on a worker thread, so the completion can land many event-loop turns after the listener attaches. Measured resume-to-data under 52 busy loops on 26 CPUs: p50 5.7 ms, max 204 ms.
1787
+ const SESSION_START_LATE_PAYLOAD_GRACE_MS = 1000
1788
+ const SESSION_START_MAX_INPUT_BYTES = 1024 * 1024
1789
+
1790
+ function failOpenSessionStart(reason) {
1791
+ process.stderr.write('[token-goat] SessionStart failed open: ' + reason + '\\n')
1792
+ process.stdout.write('{}')
1793
+ }
1794
+
1795
+ function readSessionStartInput(timeoutMs) {
1796
+ return new Promise((resolve, reject) => {
1797
+ const chunks = []
1798
+ const closers = []
1799
+ let totalBytes = 0
1800
+ let frameStart = -1
1801
+ let frameEnd = -1
1802
+ let inString = false
1803
+ let escaped = false
1804
+ let settled = false
1805
+ let timer
1806
+
1807
+ function cleanup() {
1808
+ clearTimeout(timer)
1809
+ process.stdin.removeListener('data', onData)
1810
+ process.stdin.removeListener('end', onEnd)
1811
+ process.stdin.removeListener('error', onError)
1812
+ process.stdin.pause()
1813
+ if (typeof process.stdin.unref === 'function') process.stdin.unref()
1814
+ process.stdin.destroy()
1815
+ }
1816
+
1817
+ function finish(error, value) {
1818
+ if (settled) return
1819
+ settled = true
1820
+ cleanup()
1821
+ if (error) reject(new Error(error))
1822
+ else resolve(value)
1823
+ }
1824
+
1825
+ function onData(chunk) {
1826
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
1827
+ const chunkStart = totalBytes
1828
+ totalBytes += bytes.length
1829
+ if (totalBytes > SESSION_START_MAX_INPUT_BYTES) {
1830
+ finish('input exceeded 1 MiB')
1831
+ return
1832
+ }
1833
+ chunks.push(bytes)
1834
+
1835
+ for (let index = 0; index < bytes.length; index += 1) {
1836
+ const byte = bytes[index]
1837
+ const position = chunkStart + index
1838
+ if (frameEnd !== -1) {
1839
+ if (byte !== 0x20 && byte !== 0x09 && byte !== 0x0a && byte !== 0x0d) {
1840
+ finish('input contained trailing data')
1841
+ return
1842
+ }
1843
+ continue
1844
+ }
1845
+ if (frameStart === -1) {
1846
+ if (byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d) continue
1847
+ if (byte !== 0x7b) {
1848
+ finish('input was not a JSON object')
1849
+ return
1850
+ }
1851
+ frameStart = position
1852
+ closers.push(0x7d)
1853
+ continue
1854
+ }
1855
+ if (inString) {
1856
+ if (escaped) escaped = false
1857
+ else if (byte === 0x5c) escaped = true
1858
+ else if (byte === 0x22) inString = false
1859
+ continue
1860
+ }
1861
+ if (byte === 0x22) {
1862
+ inString = true
1863
+ } else if (byte === 0x7b) {
1864
+ closers.push(0x7d)
1865
+ } else if (byte === 0x5b) {
1866
+ closers.push(0x5d)
1867
+ } else if (byte === 0x7d || byte === 0x5d) {
1868
+ if (closers.pop() !== byte) {
1869
+ finish('input contained malformed JSON')
1870
+ return
1871
+ }
1872
+ if (closers.length === 0) frameEnd = position + 1
1873
+ }
1874
+ }
1875
+
1876
+ if (frameEnd !== -1) {
1877
+ const framed = Buffer.concat(chunks, totalBytes)
1878
+ const raw = framed.subarray(frameStart, frameEnd).toString('utf8')
1879
+ try {
1880
+ const parsed = JSON.parse(raw)
1881
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
1882
+ finish('input was not a JSON object')
1883
+ return
1884
+ }
1885
+ } catch {
1886
+ finish('input contained malformed JSON')
1887
+ return
1888
+ }
1889
+ finish(undefined, raw)
1890
+ }
1891
+ }
1892
+
1893
+ function onEnd() {
1894
+ finish(frameStart === -1 ? 'stdin closed without input' : 'stdin closed before the JSON object was complete')
1895
+ }
1896
+
1897
+ function onError() {
1898
+ finish('stdin could not be read')
1899
+ }
1900
+
1901
+ // A payload already in the pipe must be framed even when the deadline has passed: expiry opens one bounded grace window that data, end or error closes at once, so only a payload that never arrives costs the full grace.
1902
+ timer = setTimeout(() => { timer = setTimeout(() => finish('input deadline exceeded'), SESSION_START_LATE_PAYLOAD_GRACE_MS) }, Math.max(0, timeoutMs))
1903
+ process.stdin.on('data', onData)
1904
+ process.stdin.on('end', onEnd)
1905
+ process.stdin.on('error', onError)
1906
+ process.stdin.resume()
1907
+ })
1908
+ }
1909
+
1910
+ // Plain-JS mirror of session_start_owed.ts::resetSessionStartOwed for a main-thread key: tests/session_start_owed_reset.test.ts pins both to the same files. It runs before anything can throw so a helper killed before its own stamp still leaves the start begun, and an earlier start's marker cannot read settled.
1911
+ function resetSessionStartOwedFiles(payload) {
1912
+ stampSessionStartOwedFiles(payload, true)
1913
+ }
1914
+
1915
+ // A start this shim failed open on is owed whatever the helper's own marker says: the helper judges "settled in time" from its own clock, and the shim can still discard its response (a helper whose exit outlasts the budget, a cleanup that fails, an unreadable response). The shim is the party that knows it printed nothing, so it re-stamps the start begun after the helper tree is gone. A plain-JS mirror of session_start_owed.ts::recordSessionStartBegun.
1916
+ function stampSessionStartOwedFiles(payload, dropClaim) {
1917
+ try {
1918
+ const raw = payload && (payload.session_id !== undefined ? payload.session_id : payload.sessionId)
1919
+ if (typeof raw !== 'string' || raw === '' || raw.indexOf(':agent:') !== -1) return
1920
+ // A subagent's key is salted, and its start is not the parent's to reset.
1921
+ if (payload.agent_id || payload.agentId) return
1922
+ const override = (process.env.TOKEN_GOAT_HOME || '').trim()
1923
+ const rooted = override !== '' && require('node:path').isAbsolute(override) &&
1924
+ (process.platform !== 'win32' || !['', '/', String.fromCharCode(92)].includes(require('node:path').parse(override).root))
1925
+ const home = rooted ? override : require('node:path').join(require('node:os').homedir(), '.token-goat')
1926
+ const stem = raw.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64)
1927
+ if (stem === '') return
1928
+ // The claim goes, and the settle file is rewritten as a begun stamp: only a begun, unsettled start is owed.
1929
+ if (dropClaim) require('node:fs').rmSync(require('node:path').join(home, 'sessions', stem + '.session-start-owed'), { force: true })
1930
+ require('node:fs').mkdirSync(require('node:path').join(home, 'sessions'), { recursive: true })
1931
+ require('node:fs').writeFileSync(require('node:path').join(home, 'sessions', stem + '.session-start'), JSON.stringify({ begunAtMs: Date.now() }), 'utf8')
1932
+ } catch {
1933
+ // Best-effort: the helper clears the marker itself when it gets that far.
1934
+ }
1935
+ }
1936
+
1937
+ async function runBoundedSessionStart(entryPath, eventName, payload, harness, deadline) {
1938
+ resetSessionStartOwedFiles(payload)
1939
+ // Do not probe entryPath here: even existsSync can block on a network filesystem.
1940
+ if (typeof entryPath !== 'string' || entryPath === '') throw new Error('hook entry unavailable')
1941
+ const remainingMs = deadline - Date.now()
1942
+ if (remainingMs <= 0) throw new Error('overall deadline exceeded')
1943
+ // Reject before spawning when only the process-tree teardown reserve remains.
1944
+ if (remainingMs <= SESSION_START_CLEANUP_RESERVE_MS) {
1945
+ throw new Error('insufficient time for process-tree cleanup')
1946
+ }
1947
+
1948
+ const args = [entryPath, 'hook', eventName]
1949
+ const env = Object.assign({}, process.env, { TOKEN_GOAT_HOOK_SERVER: '0' })
1950
+ if (process.platform === 'win32') env.TG_SESSION_START_DEFER_WORKER = '1'
1951
+ // Lets the helper's settle marker say how close to the deadline it finished.
1952
+ env.TG_SESSION_START_DEADLINE_EPOCH = String(deadline)
1953
+ if (harness === 'vscode') {
1954
+ args.push('--harness', 'vscode')
1955
+ env.TOKEN_GOAT_HARNESS_OVERRIDE = 'vscode'
1956
+ env.TOKEN_GOAT_VSCODE_HOOKS_DIR = __dirname
1957
+ } else {
1958
+ env.TOKEN_GOAT_HARNESS_OVERRIDE = 'copilot_cli'
1959
+ env.TOKEN_GOAT_COPILOT_HOOKS_DIR = __dirname
1960
+ }
1961
+
1962
+ let stdout
1963
+ try {
1964
+ stdout = await superviseSessionStart({ args, env, payload, deadline })
1965
+ } catch (error) {
1966
+ // The caller prints the failed-open line and exactly {}: the start is owed, whatever marker the helper left.
1967
+ stampSessionStartOwedFiles(payload, false)
1968
+ throw error
1969
+ }
1970
+ try {
1971
+ const parsed = JSON.parse(stdout.trim())
1972
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
1973
+ throw new Error('hook helper returned a non-object response')
1974
+ }
1975
+ return JSON.stringify(parsed)
1976
+ } catch {
1977
+ stampSessionStartOwedFiles(payload, false)
1978
+ throw new Error('hook helper returned invalid JSON')
1979
+ }
1980
+ }
1981
+
1982
+ function superviseSessionStart({ args, env, payload, deadline }) {
1983
+ if (process.platform === 'win32') return superviseWindowsSessionStart({ args, env, payload, deadline })
1984
+ const { spawn } = require('node:child_process')
1985
+ // On POSIX, retain the canonical root until its detached group is killed.
1986
+ const supervisor = [
1987
+ "const keeper = setInterval(() => {}, 1000); keeper.unref()",
1988
+ "let finished = false",
1989
+ "function done(code) { if (finished) return; finished = true; keeper.ref(); process.send({ type: 'hookExit', code }) }",
1990
+ "process.exit = code => done(code === undefined ? (process.exitCode || 0) : Number(code))",
1991
+ "process.once('beforeExit', code => done(code))",
1992
+ "process.once('uncaughtException', () => done(1))",
1993
+ "process.once('unhandledRejection', () => done(1))",
1994
+ "import(require('node:url').pathToFileURL(require('node:path').resolve(process.argv[1])).href).catch(() => done(1))",
1995
+ ].join(';')
1996
+ return new Promise((resolve, reject) => {
1997
+ const root = spawn(process.execPath, ['-e', supervisor, ...args], {
1998
+ env, windowsHide: true,
1999
+ // owner: this shim, which SIGKILLs the whole group in beginCleanup and, when that fails or the supervisor died first, in cleanupFailed; dies when the helper reports its exit, on an error, a timeout or the hard deadline (descendants that left the group, or the shim itself being SIGKILLed, are not covered).
2000
+ detached: process.platform !== 'win32',
2001
+ stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
2002
+ })
2003
+ let helperCode
2004
+ let rootClosed = false
2005
+ let cleaning = false
2006
+ let cleaned = false
2007
+ let settled = false
2008
+ let failure
2009
+ let outputBytes = 0
2010
+ const output = []
2011
+ let stderrBytes = 0
2012
+ let timer
2013
+ let hardTimer
2014
+
2015
+ function alive(pid) {
2016
+ if (!pid) return false
2017
+ try { process.kill(pid, 0); return true } catch (error) { return error.code === 'EPERM' }
2018
+ }
2019
+
2020
+ function finish(error) {
2021
+ if (settled) return
2022
+ settled = true
2023
+ clearTimeout(timer)
2024
+ clearTimeout(hardTimer)
2025
+ root.removeListener('message', onMessage)
2026
+ root.removeListener('exit', onExit)
2027
+ root.removeListener('close', onClose)
2028
+ root.stdout.removeListener('data', onOutput)
2029
+ root.stderr.removeListener('data', onStderr)
2030
+ root.stdin.destroy()
2031
+ root.stdout.destroy()
2032
+ root.stderr.destroy()
2033
+ if (root.connected) root.disconnect()
2034
+ // owner: this shim, which has killed the group by now (beginCleanup, or the group kill in cleanupFailed); dies when that kill lands, and the unref only lets the shim exit.
2035
+ root.unref()
2036
+ if (error) reject(new Error(error))
2037
+ else resolve(Buffer.concat(output, outputBytes).toString('utf8'))
2038
+ }
2039
+
2040
+ function complete() {
2041
+ // close, not exit, proves all buffered stdout was drained. The hard deadline
2042
+ // below also covers an escaped descendant retaining a pipe indefinitely.
2043
+ if (!cleaned || !rootClosed) return
2044
+ if (!failure && (helperCode !== 0 || outputBytes === 0)) failure = 'hook helper exited without a response'
2045
+ finish(failure)
2046
+ }
2047
+
2048
+ function killGroup() { process.kill(-root.pid, 'SIGKILL') }
2049
+
2050
+ function cleanupFailed(reason) {
2051
+ // The group outlives its leader while any member runs, so this reaches descendants even after the supervisor died; ESRCH means no member is left. Descendants that left the group are still unverified.
2052
+ if (root.pid) {
2053
+ try { killGroup() } catch (error) {
2054
+ if (error.code !== 'ESRCH') reason += '; group kill failed'
2055
+ }
2056
+ }
2057
+ // Verify the known owned PIDs before falling back to direct kills. Unknown
2058
+ // descendants cannot be certified clean when the tree utility has failed.
2059
+ const remaining = [root.pid].filter(alive)
2060
+ for (const pid of remaining) {
2061
+ try { process.kill(pid, 'SIGKILL') } catch (error) {
2062
+ if (error.code !== 'ESRCH') reason += '; direct kill failed for PID ' + pid
2063
+ }
2064
+ }
2065
+ finish((failure ? failure + '; ' : '') + 'process-tree cleanup failed: ' + reason
2066
+ + '; remaining owned PIDs before fallback: ' + (remaining.join(',') || 'none')
2067
+ + '; remaining known PIDs after fallback: ' + (remaining.filter(alive).join(',') || 'none')
2068
+ + '; descendant termination not verified')
2069
+ }
2070
+
2071
+ function beginCleanup(reason) {
2072
+ if (reason && !failure) failure = reason
2073
+ if (settled || cleaning) return
2074
+ cleaning = true
2075
+ clearTimeout(timer)
2076
+ if (!root.pid) { finish(failure || 'hook helper could not run'); return }
2077
+ try {
2078
+ killGroup()
2079
+ cleaned = true
2080
+ complete()
2081
+ } catch (error) {
2082
+ cleanupFailed(error.code || 'group kill failed')
2083
+ }
2084
+ }
2085
+
2086
+ function onOutput(chunk) {
2087
+ outputBytes += chunk.length
2088
+ if (outputBytes > SESSION_START_MAX_INPUT_BYTES) { beginCleanup('hook helper output exceeded 1 MiB'); return }
2089
+ output.push(chunk)
2090
+ }
2091
+
2092
+ function onStderr(chunk) {
2093
+ stderrBytes += chunk.length
2094
+ if (stderrBytes > SESSION_START_MAX_INPUT_BYTES) beginCleanup('hook helper stderr exceeded 1 MiB')
2095
+ }
2096
+
2097
+ function onMessage(message) {
2098
+ if (message && message.type === 'hookExit') {
2099
+ helperCode = message.code
2100
+ beginCleanup(message.code === 0 ? undefined : 'hook helper exited without a response')
2101
+ }
2102
+ }
2103
+
2104
+ function onExit() {
2105
+ if (!cleaning) cleanupFailed('supervisor exited before tree cleanup')
2106
+ }
2107
+
2108
+ function onClose() { rootClosed = true; complete() }
2109
+
2110
+ root.on('message', onMessage)
2111
+ root.once('exit', onExit)
2112
+ root.once('close', onClose)
2113
+ root.once('error', () => beginCleanup('hook helper could not run'))
2114
+ root.stdin.on('error', () => beginCleanup('hook helper input failed'))
2115
+ root.stdout.on('data', onOutput)
2116
+ root.stderr.on('data', onStderr)
2117
+ root.stdout.on('error', () => beginCleanup('hook helper stdout failed'))
2118
+ root.stderr.on('error', () => beginCleanup('hook helper stderr failed'))
2119
+ const remaining = deadline - Date.now()
2120
+ // Reserve the final second of the single budget for owned-tree teardown.
2121
+ timer = setTimeout(() => beginCleanup('hook helper timed out'), Math.max(0, remaining - SESSION_START_CLEANUP_RESERVE_MS))
2122
+ hardTimer = setTimeout(() => cleanupFailed('overall deadline exceeded'), Math.max(0, remaining))
2123
+ root.stdin.end(JSON.stringify(payload))
2124
+ })
2125
+ }
2126
+
2127
+ function superviseWindowsSessionStart({ args, env, payload, deadline }) {
2128
+ const { spawn } = require('node:child_process')
2129
+ const path = require('node:path')
2130
+ // Pure path arithmetic on the trusted installed entry, never PATH lookup or
2131
+ // synchronous filesystem probing in the externally visible parent.
2132
+ const binary = path.join(path.dirname(args[0]), 'native', 'win32-' + process.arch, 'tg-hook.exe')
2133
+ return new Promise((resolve, reject) => {
2134
+ // owner: the native tg-hook client, which holds a strict Job Object and ends every descendant when this shim closes its handle; dies when the job closes or the deadline passes.
2135
+ const root = spawn(binary, ['--session-start-owner-v1', String(deadline), process.execPath, ...args], {
2136
+ env, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
2137
+ })
2138
+ const output = []
2139
+ const diagnostics = []
2140
+ let outputBytes = 0
2141
+ let diagnosticBytes = 0
2142
+ let settled = false
2143
+ let timer
2144
+ function finish(error, candidate) {
2145
+ if (settled) return
2146
+ settled = true
2147
+ clearTimeout(timer)
2148
+ root.stdin.destroy()
2149
+ root.stdout.destroy()
2150
+ root.stderr.destroy()
2151
+ // Killing the owner closes the sole non-inherited job handle. Never
2152
+ // traverse a dead canonical PID or launch an unowned Windows fallback.
2153
+ if (error && root.pid) root.kill('SIGKILL')
2154
+ // owner: this shim, which kills the native owner on any error (its job handle closing ends the tree) and finds it closed on success; dies when that kill lands or the owner has exited.
2155
+ root.unref()
2156
+ if (error) reject(new Error(error))
2157
+ else resolve(JSON.stringify(candidate))
2158
+ }
2159
+ root.once('error', error => finish('native startup owner unavailable: ' + error.message))
2160
+ root.stdin.on('error', error => finish('native startup owner input failed: ' + error.message))
2161
+ root.stdout.on('error', error => finish('native startup owner stdout failed: ' + error.message))
2162
+ root.stderr.on('error', error => finish('native startup owner stderr failed: ' + error.message))
2163
+ root.stdout.on('data', chunk => {
2164
+ outputBytes += chunk.length
2165
+ if (outputBytes > SESSION_START_MAX_INPUT_BYTES + 4096) finish('native startup owner output exceeded 1 MiB')
2166
+ else output.push(chunk)
2167
+ })
2168
+ root.stderr.on('data', chunk => {
2169
+ diagnosticBytes += chunk.length
2170
+ if (diagnosticBytes > SESSION_START_MAX_INPUT_BYTES) finish('native startup owner stderr exceeded 1 MiB')
2171
+ else diagnostics.push(chunk)
2172
+ })
2173
+ root.once('close', code => {
2174
+ if (settled) return
2175
+ const diagnostic = Buffer.concat(diagnostics).toString('utf8').trim()
2176
+ if (code !== 0) { finish('native startup owner failed: ' + (diagnostic || 'exit ' + code)); return }
2177
+ let ack
2178
+ try { ack = JSON.parse(Buffer.concat(output).toString('utf8')) } catch {
2179
+ finish('native startup owner returned invalid protocol JSON'); return
2180
+ }
2181
+ if (!ack || ack.protocol !== 'tg-session-start-owner-v1' || ack.capability !== 'atomic-job-list'
2182
+ || ack.activeProcesses !== 0 || ack.rootExited !== true || ack.pipesDrained !== true
2183
+ || !ack.candidate || typeof ack.candidate !== 'object' || Array.isArray(ack.candidate)) {
2184
+ finish('native startup owner incompatible protocol or unverified cleanup'); return
2185
+ }
2186
+ if (ack.workerWake === true) {
2187
+ // The installed native entry owns this shim too. Relay only a bounded
2188
+ // request; it, not an inner owner, must launch outside every startup job.
2189
+ if (process.argv[4] && process.argv[4].startsWith('--session-start-deadline=')) {
2190
+ process.stderr.write('\\x1eTG_WORKER_WAKE_V1\\x1e\\n')
2191
+ } else if (process.env.TOKEN_GOAT_NO_WORKER_SPAWN !== '1') {
2192
+ const workerEnv = Object.assign({}, process.env)
2193
+ delete workerEnv.TG_SESSION_START_DEFER_WORKER
2194
+ try {
2195
+ const worker = spawn(process.execPath, [args[0], '--worker-daemon', '--claim-worker'], {
2196
+ env: workerEnv, cwd: require('node:os').tmpdir(), windowsHide: true, stdio: 'ignore',
2197
+ // owner: none, the daemon outlives this shim on purpose; dies when its pid file or data directory is gone, when it sits idle, or on worker stop.
2198
+ detached: true,
2199
+ })
2200
+ worker.on('error', () => {})
2201
+ // owner: none, the daemon outlives this shim on purpose; dies when its pid file or data directory is gone, when it sits idle, or on worker stop.
2202
+ worker.unref()
2203
+ } catch {
2204
+ // Queue remains durable; ordinary lifecycle activation can retry.
2205
+ }
2206
+ }
2207
+ }
2208
+ finish(undefined, ack.candidate)
2209
+ })
2210
+ timer = setTimeout(() => finish('native startup owner overall deadline exceeded'), Math.max(0, deadline - Date.now()))
2211
+ root.stdin.end(JSON.stringify(payload))
2212
+ })
2213
+ }
2214
+ `;
2215
+
2216
+ // src/bridges/shrink_block.ts
2217
+ init_define_import_meta_env();
2218
+ var MATERIALIZE_SHRUNK_IMAGE_JS = `// Best-effort sweep of previously materialized shrunk copies in the OS temp dir. The temp file only needs to outlive the single tool call whose path argument was rewritten to it, so anything older than an hour is finished with; the "token-goat-shrink-" prefix check confines the sweep to this mechanism's own files (same defense-in-depth rule as pruneShrinkCache in src/image_shrink.ts). Throttled per process; runs only when a shrink payload actually arrives, so the common no-image path never pays for it.
2219
+ const MATERIALIZED_SHRINK_MAX_AGE_MS = 60 * 60 * 1000
2220
+ let lastMaterializedShrinkSweepAtMs = 0
2221
+ function pruneMaterializedShrinks() {
2222
+ const now = Date.now()
2223
+ if (now - lastMaterializedShrinkSweepAtMs < MATERIALIZED_SHRINK_MAX_AGE_MS) return
2224
+ lastMaterializedShrinkSweepAtMs = now
2225
+ try {
2226
+ const dir = os.tmpdir()
2227
+ for (const file of fs.readdirSync(dir)) {
2228
+ if (!file.startsWith("token-goat-shrink-")) continue
2229
+ const full = path.join(dir, file)
2230
+ try {
2231
+ const st = fs.statSync(full)
2232
+ if (st.isFile() && now - st.mtimeMs > MATERIALIZED_SHRINK_MAX_AGE_MS) fs.unlinkSync(full)
2233
+ } catch {
2234
+ // Best-effort per-file cleanup; one bad stat/unlink must not abort the sweep.
2235
+ }
2236
+ }
2237
+ } catch {
2238
+ // Best-effort; a readdir failure must never break the materialization below.
2239
+ }
2240
+ }
2241
+
2242
+ // Decode a token-goat image-shrink additionalContext payload ("<summary>\\ndata:image/<fmt>;base64,<data>") into a real file on disk, since the host's pre-tool hook has no context-injection channel to hand the shrunk image to the model directly -- only argument rewriting. The temp filename is derived from pid/time/random alone, never from the source image's own name, so an attacker-chosen filename cannot steer the write; the format suffix comes from the data-URL's media subtype, whose character class ([a-zA-Z0-9.+-]) admits no path separators. Returns undefined (leaving the original path argument untouched, so a failed shrink falls back to the original image) if the context isn't a shrink payload or anything goes wrong writing it.
2243
+ function materializeShrunkImage(context) {
2244
+ if (typeof context !== "string") return undefined
2245
+ const idx = context.indexOf("data:image/")
2246
+ if (idx === -1) return undefined
2247
+ const match = /^data:image\\/([a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)$/.exec(context.slice(idx).trim())
2248
+ if (!match) return undefined
2249
+ try {
2250
+ pruneMaterializedShrinks()
2251
+ const buf = Buffer.from(match[2], "base64")
2252
+ const name = \`token-goat-shrink-\${process.pid}-\${Date.now()}-\${globalThis.crypto.randomUUID()}.\${match[1]}\`
2253
+ const file = path.join(os.tmpdir(), name)
2254
+ // Owner-only: the copy is of a workspace image and sits in the shared temp dir.
2255
+ fs.writeFileSync(file, buf, { mode: 0o600 })
2256
+ return file
2257
+ } catch {
2258
+ return undefined
2259
+ }
2260
+ }`;
2261
+
2262
+ // src/bridges/copilot_cli.ts
2263
+ var COPILOT_CLI_HOOK_SCRIPT = `#!/usr/bin/env node
2264
+ // token-goat Copilot CLI hook shim. Translates Copilot's hook event names and
2265
+ // request/response schema to/from token-goat's internal hook protocol.
2266
+ 'use strict'
2267
+ ${SHIM_MAX_BUFFER_CONST}
2268
+ const { spawnSync } = require('node:child_process')
2269
+ const fs = require('node:fs')
2270
+ const os = require('node:os')
2271
+ const path = require('node:path')
2272
+ const { pathToFileURL } = require('node:url')
2273
+
2274
+ // How long the fallback spawn of the real hook may run before the shim answers an empty fail-open '{}' for it. The hook entries token-goat installs give Copilot 60 s (HOOK_TIMEOUT_SEC in copilot_cli_install.ts); the number is SHIM_FALLBACK_TIMEOUT_MS.copilot in shim_common.ts, and a hook Copilot kills fails open the same way, so a cap of a few seconds only discarded the verdict of a cold start that was merely slow (bundle load, DB open, a loaded machine). Kept under that 60 s so the shim, not Copilot, ends a hook that really is stuck.
2275
+ ${shimFallbackTimeoutConst("copilot")}
2276
+ ${COPILOT_SESSION_START_HELPERS}
2277
+
2278
+ // Copilot event name -> token-goat internal HookEventName (src/types.ts's
2279
+ // HOOK_EVENTS). Only these nine have a token-goat handler; every other real
2280
+ // Copilot event (sessionEnd, errorOccurred, notification, permissionRequest)
2281
+ // is left unimplemented rather than guessed at, and falls through to the
2282
+ // default no-op below. subagentStart answers with the spawn briefing as
2283
+ // additionalContext, which Copilot CLI 1.0.88 prepends to the subagent's own
2284
+ // prompt (capture C4a), so the briefing no longer rides a rewrite of the
2285
+ // parent's task call and stays out of the parent's history.
2286
+ // postToolUseFailure is the newest of the eight and the only one whose channel
2287
+ // costs tokens instead of saving them: it fires instead of postToolUse when a
2288
+ // tool result is a failure, and accepts only additionalContext back --
2289
+ // modifiedResult and suppressOutput are not honored there, so a failed result
2290
+ // still cannot be fenced, compressed or shrunk. It is wired anyway because
2291
+ // additionalContext demonstrably reaches the model (see the postToolUseFailure
2292
+ // branch in translate() for the bundle offset), and hooks_tool_failure.ts
2293
+ // spends that channel only on an exact repeat failure, where staying silent
2294
+ // costs a whole wasted retry.
2295
+ // 'sessionStart' was previously a permanent no-op on the stated grounds that
2296
+ // token-goat has no internal session_start handler. That was simply wrong --
2297
+ // hooks_session_start.ts has long emitted the command-routing reminder that
2298
+ // every other harness receives -- and the no-op was the reason Copilot CLI
2299
+ // sessions alone never got told token-goat exists. It is wired now: verified
2300
+ // against Copilot CLI 1.0.77 that a hooks.json sessionStart entry returning
2301
+ // {additionalContext} does reach the model. The github/copilot-cli#2142
2302
+ // fire-and-forget bug that would have made this dead wiring was fixed in a
2303
+ // pre-release months before that version, and its companion multi-extension
2304
+ // hook-overwrite bug never applied here: it hit runtime *extension* hooks,
2305
+ // while this config-file hooks.json path goes through Copilot's own merge.
2306
+ const COPILOT_TO_TG_EVENT = {
2307
+ sessionStart: 'session_start',
2308
+ preToolUse: 'pre_tool_use',
2309
+ postToolUse: 'post_tool_use',
2310
+ preCompact: 'pre_compact',
2311
+ agentStop: 'stop',
2312
+ subagentStop: 'subagent_stop',
2313
+ userPromptSubmitted: 'user_prompt_submit',
2314
+ postToolUseFailure: 'post_tool_use_failure',
2315
+ subagentStart: 'subagent_start',
2316
+ }
2317
+
2318
+ // Keys only VS Code reads. VS Code 1.137 parses this same hooks file and
2319
+ // accepts a key either from its camelCase Copilot table, which has no
2320
+ // subagentStart, or when the key is already its own PascalCase hook type
2321
+ // (workbench.desktop.main.js: iut(key) ?? eut(key)). So VS Code's subagent
2322
+ // hook needs the key spelled SubagentStart. A Copilot CLI payload arriving on
2323
+ // one of these keys is answered with nothing, so a Copilot build that also
2324
+ // honored the PascalCase spelling could not brief a subagent twice.
2325
+ const VSCODE_ONLY_TO_TG_EVENT = {
2326
+ SessionStart: 'session_start',
2327
+ SubagentStart: 'subagent_start',
2328
+ }
2329
+
2330
+ // Copilot built-in tool name -> token-goat internal tool name. Confirmed via
2331
+ // @github/copilot-sdk type definitions and multiple real GitHub issue payload
2332
+ // dumps -- supersedes an earlier docs-based guess (shell/read/write/url) that
2333
+ // didn't hold up in practice; 'write' in particular was never a real
2334
+ // toolName value, only a Copilot permission-pattern keyword. 'powershell'
2335
+ // maps to the same 'Bash' handler as 'bash' since both are shell-command
2336
+ // execution from token-goat's perspective (mirrors how hooks_bash.ts's own
2337
+ // filters already treat powershell-wrapped commands as part of the Bash
2338
+ // pipeline, not a separate tool). 'task', 'ask_user', 'memory', and
2339
+ // MCP-server tool invocations (<server-name>-<tool-name>) have no
2340
+ // token-goat equivalent and are passed through unmapped (safe no-op for
2341
+ // handlers that don't recognize the name).
2342
+ // 'read_bash'/'read_powershell' are the background-shell output pollers, and
2343
+ // they are the exact shape of Claude Code's BashOutput: Copilot's shell tool
2344
+ // is async, so a long-running command is started once and then re-read over
2345
+ // and over within one turn, each read returning the accumulated output again.
2346
+ // hooks_bashoutput.ts already collapses that into a delta (or a short
2347
+ // "unchanged" marker) for Claude Code, and now does the same here. Both the
2348
+ // names and the argument key were read out of the shipping 1.0.80 bundle
2349
+ // rather than guessed: the builtin tool-name table in
2350
+ // prebuilds/win32-x64/runtime.node lists read_bash/stop_bash/list_bash and
2351
+ // read_powershell/stop_powershell/list_powershell, and the poller's own input
2352
+ // schema in app.js is {shellId, delay} -- so the shell id needs the
2353
+ // POLL_ID_ARG_KEY remap below to reach postBashOutputHandler, which reads
2354
+ // 'bash_id'. read_shell/stop_shell/list_shells, which an earlier static sweep
2355
+ // of the same binary suggested were the real names, are internal Rust
2356
+ // identifiers (tool_read_shell_prepare_input, tool_list_shells_descriptor,
2357
+ // PreparedStopShellInput, and the serde field names of the shell config
2358
+ // struct), never wire tool names, so they are deliberately absent here.
2359
+ // stop_bash/list_bash and their powershell twins stay unmapped because
2360
+ // token-goat has no KillShell-equivalent handler for them to reach; a mapping
2361
+ // would be pure decoration.
2362
+ // 'bash'/'powershell' stay exactly as they were. The same sweep suggested the
2363
+ // executor had been renamed to write_bash/write_powershell and that this
2364
+ // mapping was dead, and that is not what the bundle says: app.js resolves the
2365
+ // executor as shellConfig?.shellToolName ?? "bash" (with "powershell" as the
2366
+ // Windows default of the same config), and its command lives under 'command'
2367
+ // exactly as hooks_bash.ts expects. write_bash/write_powershell are real tool
2368
+ // names, but they are a different tool -- their schema is {shellId, input,
2369
+ // delay} and the bundle files them under the subtype "write_shell", i.e. send
2370
+ // stdin to an already-running shell, not run a command. Mapping them to Bash
2371
+ // would label a stdin write as a shell execution, so they are left unmapped.
2372
+ // 'task', 'read_agent' and 'memory'-family tools are likewise left alone:
2373
+ // task's result is assembled in the native addon and its shape is unknown,
2374
+ // read_agent is an incremental poll that postAgentHandler is not written for,
2375
+ // and the real tool names behind memory were never confirmed. Each would put
2376
+ // a handler that rewrites model-visible output in front of a payload shape
2377
+ // nobody has seen, which is worse than leaving the compression on the table.
2378
+ // Own properties only, for every map in this shim -- see the event map in main() for why a
2379
+ // bare lookup on a plain object literal is not a gate. A tool named 'constructor' resolved to
2380
+ // a Function, which JSON.stringify drops, so the forwarded payload carried no tool_name and
2381
+ // every hook keyed on one no-opped; '__proto__' forwarded a whole prototype instead. The key
2382
+ // is third-party text: Copilot namespaces MCP tools as server:tool and the stripped half is
2383
+ // whatever that server called its tool.
2384
+ function ownGet(map, key) {
2385
+ return typeof key === 'string' && Object.prototype.hasOwnProperty.call(map, key) ? map[key] : undefined
2386
+ }
2387
+
2388
+ const TOOL_TO_TG = {
2389
+ bash: 'Bash',
2390
+ powershell: 'Bash',
2391
+ read_bash: 'BashOutput',
2392
+ read_powershell: 'BashOutput',
2393
+ view: 'Read',
2394
+ create: 'Write',
2395
+ edit: 'Edit',
2396
+ web_fetch: 'WebFetch',
2397
+ web_search: 'WebSearch',
2398
+ grep: 'Grep',
2399
+ glob: 'Glob',
2400
+ skill: 'Skill',
2401
+ exit_plan_mode: 'ExitPlanMode',
2402
+ }
2403
+
2404
+ function foldToolName(name) {
2405
+ return typeof name === 'string' ? name.toLowerCase().replace(/[_-]/g, '') : ''
2406
+ }
2407
+
2408
+ const FOLDED_TOOL_TO_TG = {}
2409
+ for (const [k, v] of Object.entries(TOOL_TO_TG)) {
2410
+ FOLDED_TOOL_TO_TG[foldToolName(k)] = v
2411
+ }
2412
+ // Also register folded entries for canonical TG tool names directly (e.g. WebSearch -> websearch)
2413
+ for (const v of Object.values(TOOL_TO_TG)) {
2414
+ FOLDED_TOOL_TO_TG[foldToolName(v)] = v
2415
+ }
2416
+
2417
+ function resolveCanonicalToolName(name) {
2418
+ if (!name || typeof name !== 'string') return name
2419
+ const direct = ownGet(TOOL_TO_TG, name)
2420
+ if (direct !== undefined) return direct
2421
+ const stripped = name.indexOf(':') !== -1 ? name.split(':').pop() : name
2422
+ const strippedDirect = ownGet(TOOL_TO_TG, stripped)
2423
+ if (strippedDirect !== undefined) return strippedDirect
2424
+ const folded = foldToolName(name)
2425
+ const foldedStripped = foldToolName(stripped)
2426
+ return ownGet(FOLDED_TOOL_TO_TG, folded) || ownGet(FOLDED_TOOL_TO_TG, foldedStripped) || name
2427
+ }
2428
+
2429
+ // Confirmed via github/copilot-cli#3349 (open, unresolved as of writing): some
2430
+ // real Copilot CLI invocations send toolArgs as a JSON-*encoded string*
2431
+ // rather than a parsed object, contradicting the documented schema. Left
2432
+ // unhandled, canonical.tool_input would become a raw string and every
2433
+ // downstream event.toolInput[key] lookup in token-goat's handlers would
2434
+ // silently return undefined -- the deny/dedup mechanism would no-op with no
2435
+ // error surfaced. Parse it defensively; on a non-string, absent, or
2436
+ // unparsable value, fall back to {} rather than throwing (this shim's
2437
+ // convention throughout is fail-open, never crash on a malformed payload).
2438
+ function parseMaybeJsonObject(value) {
2439
+ if (value && typeof value === 'object') return value
2440
+ if (typeof value === 'string') {
2441
+ try {
2442
+ const parsed = JSON.parse(value)
2443
+ if (parsed && typeof parsed === 'object') return parsed
2444
+ } catch {
2445
+ // fall through to {}
2446
+ }
2447
+ }
2448
+ return {}
2449
+ }
2450
+
2451
+ // view/edit/create send the file path under 'path'; every token-goat handler these
2452
+ // three tools reach only ever looks for 'file_path' (getFilePath in hooks_common.ts).
2453
+ // Keyed by the ORIGINAL Copilot tool name (before TOOL_TO_TG renames it) since that's
2454
+ // the name toolArgs' shape is keyed to, not token-goat's internal tool name.
2455
+ const FILE_PATH_ARG_KEY = {
2456
+ view: 'path',
2457
+ edit: 'path',
2458
+ create: 'path',
2459
+ }
2460
+
2461
+ // read_bash/read_powershell send the background shell's id under 'shellId' (confirmed
2462
+ // against the poller's input schema in the shipping 1.0.80 app.js: {shellId, delay});
2463
+ // postBashOutputHandler reads 'bash_id'. Without this the tool-name mapping above would
2464
+ // be inert -- the handler bails on a missing bash_id and every poll would keep costing
2465
+ // the full accumulated output. Same keying convention as FILE_PATH_ARG_KEY: the ORIGINAL
2466
+ // Copilot tool name, since that is what the argument shape belongs to.
2467
+ const POLL_ID_ARG_KEY = {
2468
+ read_bash: 'shellId',
2469
+ read_powershell: 'shellId',
2470
+ }
2471
+
2472
+ // Copilot spawns a brand-new process for every single hook invocation (no long-lived plugin
2473
+ // process the way OpenClaw's is -- OPENCLAW_HOOK_SCRIPT's own \`copilot-\${process.pid}-\${Date.now()}\`
2474
+ // fallback is safe there specifically because that process lives for the whole session, so the
2475
+ // pid stays constant across calls). If Copilot ever omits \`sessionId\` from a payload, falling
2476
+ // back to \`process.pid\` here would mint a DIFFERENT id on every single call for what's really
2477
+ // the same session, since process.pid varies per invocation -- breaking token-goat's
2478
+ // session-based dedup/state ledger, which never accumulates across calls as a result. Derive a
2479
+ // stable id instead from the one thing that's actually constant across calls for the same
2480
+ // session: the working directory Copilot reports. Copilot CLI's command hooks send it as \`cwd\`:
2481
+ // every payload 1.0.88 sent in the tg-captures run carried \`cwd\` and none carried
2482
+ // \`workingDirectory\` (tests/fixtures/copilot_cli_1_0_88/). The SDK's BaseHookInput in
2483
+ // copilot-sdk/types.d.ts declares the same value as \`workingDirectory\`, so both names are read,
2484
+ // that one first. Were neither read, the fallback id would come from process.cwd() and
2485
+ // \`canonical.cwd\` below would be undefined; tests/harness_schema_manifest.test.ts drives the
2486
+ // captured payload from a different directory to catch exactly that.
2487
+ function stableFallbackSessionId(cwd) {
2488
+ const key = typeof cwd === 'string' && cwd ? cwd : process.cwd()
2489
+ const hash = require('node:crypto').createHash('sha256').update(key).digest('hex').slice(0, 16)
2490
+ return 'copilot-' + hash
2491
+ }
2492
+
2493
+ function remapToolInput(copilotToolName, input) {
2494
+ if (!input || typeof input !== 'object') return input
2495
+ let out = input
2496
+ // Add the canonical key alongside the original rather than renaming it, so nothing that
2497
+ // might read the original 'path'/'shellId' key elsewhere (e.g. a future handler) loses it.
2498
+ const stripped = typeof copilotToolName === 'string' && copilotToolName.indexOf(':') !== -1
2499
+ ? copilotToolName.split(':').pop()
2500
+ : copilotToolName
2501
+ const folded = foldToolName(stripped)
2502
+ const pathKey = ownGet(FILE_PATH_ARG_KEY, copilotToolName) || ownGet(FILE_PATH_ARG_KEY, stripped) || (folded === 'view' || folded === 'edit' || folded === 'create' ? 'path' : undefined)
2503
+ if (pathKey !== undefined && pathKey in out) {
2504
+ out = Object.assign({}, out, { file_path: out[pathKey] })
2505
+ }
2506
+ const idKey = ownGet(POLL_ID_ARG_KEY, copilotToolName) || ownGet(POLL_ID_ARG_KEY, stripped) || (folded === 'readbash' || folded === 'readpowershell' ? 'shellId' : undefined)
2507
+ if (idKey !== undefined && idKey in out) {
2508
+ out = Object.assign({}, out, { bash_id: out[idKey] })
2509
+ }
2510
+ if (Array.isArray(out.view_range) && out.view_range.length >= 1) {
2511
+ const rawStart = Number(out.view_range[0])
2512
+ if (Number.isFinite(rawStart) && rawStart >= 1) {
2513
+ const mapped = { offset: Math.floor(rawStart) }
2514
+ if (out.view_range.length >= 2) {
2515
+ const rawEnd = Number(out.view_range[1])
2516
+ if (Number.isFinite(rawEnd) && rawEnd >= rawStart) {
2517
+ mapped.limit = Math.floor(rawEnd - rawStart + 1)
2518
+ }
2519
+ }
2520
+ out = Object.assign({}, out, mapped)
2521
+ }
2522
+ }
2523
+ if ('old_str' in out && !('old_string' in out)) {
2524
+ out = Object.assign({}, out, { old_string: out.old_str })
2525
+ }
2526
+ if ('new_str' in out && !('new_string' in out)) {
2527
+ out = Object.assign({}, out, { new_string: out.new_str })
2528
+ }
2529
+ return out
2530
+ }
2531
+
2532
+ ${SHIM_TRY_SERVER}
2533
+
2534
+ // Attempts the resident server (tryServer above), then the in-process hook call: import()s dist/token-goat-hook.mjs (a sibling of
2535
+ // the baked token-goat entry path, built with zero load-time side effects -- unlike
2536
+ // the CLI entry, which runs the full argv-parsing CLI as a side effect of being
2537
+ // loaded) and calls its exported relayInProcess() directly, avoiding a second node
2538
+ // process spawn entirely. Returns undefined (triggering the spawnSync fallback below)
2539
+ // when entryPath is absent, the sibling file doesn't exist (an older install predating
2540
+ // this file), or anything else goes wrong -- this must never throw.
2541
+ async function tryInProcess(entryPath, tgEvent, canonical, harness) {
2542
+ if (!entryPath) return undefined
2543
+ try {
2544
+ // Set before either path: the resident server runs the call under this process's environment.
2545
+ process.env.TOKEN_GOAT_HARNESS_OVERRIDE = harness
2546
+ // Which hooks directory VS Code loaded this shim from is the only thing separating a
2547
+ // user-scope copy from a project-scope one -- the two files are byte-identical and so are
2548
+ // their payloads. vscode_duplicate.ts needs it to stand the redundant copy down.
2549
+ if (harness === 'vscode') process.env.TOKEN_GOAT_VSCODE_HOOKS_DIR = __dirname
2550
+ // Whether this hooks file wires subagentStart decides where a subagent's spawn briefing goes
2551
+ // (hooks_agent_spawn.ts subagentStartWired), and only this directory says which file that is.
2552
+ else process.env.TOKEN_GOAT_COPILOT_HOOKS_DIR = __dirname
2553
+ const served = await tryServer(entryPath, tgEvent, canonical)
2554
+ if (served !== undefined) return served
2555
+ const hookLibPath = path.join(path.dirname(entryPath), 'token-goat-hook.mjs')
2556
+ if (!require('node:fs').existsSync(hookLibPath)) return undefined
2557
+ const mod = await import(pathToFileURL(hookLibPath).href)
2558
+ return await mod.relayInProcess(tgEvent, canonical)
2559
+ } catch {
2560
+ return undefined
2561
+ }
2562
+ }
2563
+
2564
+ // VS Code's built-in agent reads this same hooks file (~/.copilot/hooks and .github/hooks are both
2565
+ // on its hook-source list) and runs these same entries, but it sends Claude Code's payload shape:
2566
+ // hook_event_name, session_id, tool_name, tool_input, tool_response. Copilot CLI's payload never
2567
+ // carries hook_event_name and always names its tool in toolName, so that pair tells them apart.
2568
+ // The payload goes to token-goat as-is under the vscode harness, which does its own tool-name
2569
+ // mapping and writes a response VS Code reads directly, so nothing here translates it.
2570
+ function isVscodePayload(payload) {
2571
+ return payload !== null && typeof payload === 'object' && typeof payload.hook_event_name === 'string' && payload.toolName === undefined
2572
+ }
2573
+
2574
+ async function relayVscode(entryPath, tgEvent, payload) {
2575
+ // VS Code omits session_id when the chat has no session resource yet; key the state on the workspace instead, the same fallback the Copilot path uses.
2576
+ const input = typeof payload.session_id === 'string' && payload.session_id !== ''
2577
+ ? payload
2578
+ : Object.assign({}, payload, { session_id: stableFallbackSessionId(process.cwd()) })
2579
+ let stdout = await tryInProcess(entryPath, tgEvent, input, 'vscode')
2580
+ if (stdout === undefined) {
2581
+ // tgEvent is a value of the closed COPILOT_TO_TG_EVENT map (gated in main) and the harness is a literal, so nothing caller-controlled reaches the shell string below.
2582
+ const spawnOpts = {
2583
+ input: JSON.stringify(input),
2584
+ encoding: 'utf8',
2585
+ windowsHide: true,
2586
+ timeout: SHIM_FALLBACK_TIMEOUT_MS,
2587
+ killSignal: 'SIGKILL',
2588
+ maxBuffer: SHIM_MAX_BUFFER_BYTES,
2589
+ env: Object.assign({}, process.env, { TOKEN_GOAT_HARNESS_OVERRIDE: 'vscode', TOKEN_GOAT_VSCODE_HOOKS_DIR: __dirname }),
2590
+ }
2591
+ const res = entryPath
2592
+ ? spawnSync(process.execPath, [entryPath, 'hook', tgEvent, '--harness', 'vscode'], spawnOpts)
2593
+ : spawnSync('token-goat hook ' + tgEvent + ' --harness vscode', Object.assign({ shell: true }, spawnOpts))
2594
+ if (res.status !== 0 || !res.stdout) return '{}'
2595
+ stdout = res.stdout
2596
+ }
2597
+ try {
2598
+ const parsed = JSON.parse(stdout)
2599
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? JSON.stringify(parsed) : '{}'
2600
+ } catch {
2601
+ return '{}'
2602
+ }
2603
+ }
2604
+
2605
+ async function main() {
2606
+ const startedAt = Math.floor(require('node:perf_hooks').performance.timeOrigin)
2607
+ const copilotEvent = process.argv[2] || ''
2608
+
2609
+ // ownGet, not a bare lookup: the map is a plain object literal, so eight keys it never declares ('constructor', 'toString', '__proto__' and the rest of Object.prototype) come back truthy and sail past the check below. tgEvent is then a Function, and 'token-goat hook ' + tgEvent is concatenated into a shell command string by the fallback at the bottom of this shim. No member of Object.prototype stringifies with a shell metacharacter, so this was a failed spawn rather than a second command -- but argv reaching a shell string through a gate that reads like it stops it is the shape worth closing, not the payload that happened to be harmless. The four shims built on shim_common.ts validate against a closed Set, which has no equivalent hole.
2610
+ const vscodeOnly = ownGet(VSCODE_ONLY_TO_TG_EVENT, copilotEvent)
2611
+ const tgEvent = ownGet(COPILOT_TO_TG_EVENT, copilotEvent) || vscodeOnly
2612
+ if (!tgEvent) {
2613
+ process.stdout.write('{}')
2614
+ return
2615
+ }
2616
+
2617
+ let sessionStartDeadline
2618
+ if (tgEvent === 'session_start') {
2619
+ const inherited = process.argv[4]
2620
+ const epoch = inherited === undefined ? startedAt + sessionStartBudgetMs()
2621
+ : inherited.startsWith('--session-start-deadline=') ? Number(inherited.slice('--session-start-deadline='.length)) : NaN
2622
+ if (!Number.isSafeInteger(epoch) || epoch <= 0 || epoch > startedAt + sessionStartBudgetMs()) {
2623
+ failOpenSessionStart('invalid inherited startup deadline')
2624
+ return
2625
+ }
2626
+ sessionStartDeadline = epoch
2627
+ }
2628
+ let raw = ''
2629
+ if (sessionStartDeadline !== undefined) {
2630
+ try {
2631
+ raw = await readSessionStartInput(sessionStartDeadline - Date.now())
2632
+ } catch (error) {
2633
+ failOpenSessionStart(error && error.message ? error.message : 'input could not be read')
2634
+ return
2635
+ }
2636
+ } else {
2637
+ try {
2638
+ raw = require('node:fs').readFileSync(0, 'utf8')
2639
+ } catch {
2640
+ process.stdout.write('{}')
2641
+ return
2642
+ }
2643
+ }
2644
+
2645
+ let payload
2646
+ try {
2647
+ payload = JSON.parse(raw)
2648
+ } catch {
2649
+ if (sessionStartDeadline !== undefined) failOpenSessionStart('input contained malformed JSON')
2650
+ else process.stdout.write('{}')
2651
+ return
2652
+ }
2653
+
2654
+ if (isVscodePayload(payload)) {
2655
+ if (sessionStartDeadline === undefined) {
2656
+ process.stdout.write(await relayVscode(process.argv[3], tgEvent, payload))
2657
+ } else {
2658
+ try {
2659
+ const input = typeof payload.session_id === 'string' && payload.session_id !== ''
2660
+ ? payload
2661
+ : Object.assign({}, payload, { session_id: stableFallbackSessionId(process.cwd()) })
2662
+ process.stdout.write(await runBoundedSessionStart(process.argv[3], tgEvent, input, 'vscode', sessionStartDeadline))
2663
+ } catch (error) {
2664
+ failOpenSessionStart(error && error.message ? error.message : 'hook helper failed')
2665
+ }
2666
+ }
2667
+ return
2668
+ }
2669
+ if (vscodeOnly) {
2670
+ process.stdout.write('{}')
2671
+ return
2672
+ }
2673
+
2674
+ const toolName = payload && payload.toolName
2675
+ const canonical = {
2676
+ session_id:
2677
+ (payload && payload.sessionId) ||
2678
+ stableFallbackSessionId(payload && (payload.workingDirectory || payload.cwd)),
2679
+ cwd: payload && (payload.workingDirectory || payload.cwd),
2680
+ }
2681
+
2682
+ // Subagent correlation and W3C Trace Context propagation from Copilot CLI payloads
2683
+ const agentId = payload && (payload.agent_id || payload.agentId)
2684
+ if (typeof agentId === 'string' && agentId !== '') {
2685
+ canonical.agent_id = agentId
2686
+ }
2687
+ const traceparent = payload && (payload.traceparent || payload.traceParent)
2688
+ if (typeof traceparent === 'string' && traceparent !== '') {
2689
+ canonical.traceparent = traceparent
2690
+ }
2691
+ const tracestate = payload && (payload.tracestate || payload.traceState)
2692
+ if (typeof tracestate === 'string' && tracestate !== '') {
2693
+ canonical.tracestate = tracestate
2694
+ }
2695
+
2696
+ // userPromptSubmitted only: Copilot declares \`prompt\` required on UserPromptSubmittedHookInput.
2697
+ // hooks_session.ts's userPromptSubmitHandler reads it as \`event.raw['prompt']\` and gates every
2698
+ // branch it has on the text, so without this it saw '' on every Copilot prompt and the
2699
+ // embedded-skill dedup hint could never fire. Same shape as the postToolUseFailure \`error\`
2700
+ // drop: a required field the canonical builder simply did not list.
2701
+ if (typeof (payload && payload.prompt) === 'string' && payload.prompt !== '') {
2702
+ canonical.prompt = payload.prompt
2703
+ }
2704
+ // subagentStop only: the subagent's final answer arrives as response (Copilot CLI 1.0.88,
2705
+ // tg-captures C4a-013). Forwarded under Claude Code's name for the same text,
2706
+ // last_assistant_message, which both subagent_stop handlers read. Unforwarded, they saw ''.
2707
+ if (typeof (payload && payload.response) === 'string' && payload.response !== '') {
2708
+ canonical.last_assistant_message = payload.response
2709
+ }
2710
+ let originalToolArgs = {}
2711
+ if (toolName) {
2712
+ originalToolArgs = parseMaybeJsonObject(payload && payload.toolArgs)
2713
+ canonical.tool_name = resolveCanonicalToolName(toolName)
2714
+ canonical.tool_input = remapToolInput(toolName, originalToolArgs)
2715
+ }
2716
+
2717
+ // postToolUse only: confirmed via https://docs.github.com/en/copilot/reference/hooks-reference
2718
+ // that Copilot's toolResult is an object ({resultType, textResultForLlm}), not a bare string
2719
+ // or array. Without this, token-goat's tool_response consumers (hooks_read.ts's
2720
+ // extractReadOutput, hooks_bash.ts's extractBashOutput, etc. -- all of which check for a
2721
+ // string or an object keyed by output/content/text/body) never see any content, so
2722
+ // post-read/post-bash stats stay empty no matter how many tool calls happen. Extract the
2723
+ // LLM-facing text directly rather than forwarding the raw object, since textResultForLlm
2724
+ // isn't one of those recognized object keys.
2725
+ // postToolUseFailure only: Copilot's PostToolUseFailureHookInput (copilot-sdk/types.d.ts:1042)
2726
+ // carries {toolName, toolArgs, error} and no toolResult at all -- the failure text lives in
2727
+ // \`error\`, a plain string. Without forwarding it, hooks_tool_failure.ts's extractFailureText
2728
+ // finds nothing to key on and the repeat-failure brake returns pass on every single call: wired,
2729
+ // green, and doing nothing. Found by driving the installed shim rather than by a test, because
2730
+ // the handler's own tests hand it a raw payload that already has the field.
2731
+ if (typeof (payload && payload.error) === 'string' && payload.error !== '') {
2732
+ canonical.error = payload.error
2733
+ }
2734
+
2735
+ const rawResult = payload && payload.toolResult
2736
+ if (rawResult && typeof rawResult === 'object') {
2737
+ const tr = rawResult
2738
+ // text_result_for_llm: Copilot's docs also describe a "VS Code compatible" snake_case
2739
+ // wire format (tool_result.text_result_for_llm) alongside the camelCase one above; try
2740
+ // both rather than assuming only the camelCase shape ever reaches this shim.
2741
+ const text = typeof tr.textResultForLlm === 'string' ? tr.textResultForLlm : tr.text_result_for_llm
2742
+ if (typeof text === 'string') canonical.tool_response = text
2743
+ }
2744
+
2745
+ // process.argv[3], when present, is the absolute path to the token-goat CLI entry that ran
2746
+ // \`token-goat install --copilot\` (baked in by hookCommandFor in copilot_cli_install.ts).
2747
+ // Invoking it directly via process.execPath sidesteps PATH/cmd.exe resolution for this
2748
+ // inner call too -- confirmed live-production root cause of an *intermittent* (not the
2749
+ // original, already-fixed github/copilot-cli#4001) "(hook errored)" deny-all: even once the
2750
+ // outer command launches via its own baked process.execPath, this inner call still shelled
2751
+ // out to a bare \`token-goat\`, which depends on the npm global bin being on whatever PATH
2752
+ // Copilot spawns this hook subprocess with. Falls back to the old PATH-based shell:true
2753
+ // invocation when argv[3] is absent (an older cached hook config still pointing at a
2754
+ // freshly-reinstalled shim, or a direct dev/test invocation), so this is a pure hardening,
2755
+ // never a behavior break for configs installed before this fix. An args array (not a
2756
+ // template string) is safe here specifically because entryPath and tgEvent never touch a
2757
+ // shell -- no DEP0190 concern, unlike the shell:true fallback below.
2758
+ const entryPath = process.argv[3]
2759
+ // SessionStart bypasses the resident server and in-process path: only a child-process deadline can interrupt synchronous handler or SQLite work. TOKEN_GOAT_HOOK_SERVER=0 below also prevents that child from leaving a server process behind.
2760
+ let stdout
2761
+ if (sessionStartDeadline !== undefined) {
2762
+ try {
2763
+ stdout = await runBoundedSessionStart(entryPath, tgEvent, canonical, 'copilot_cli', sessionStartDeadline)
2764
+ } catch (error) {
2765
+ failOpenSessionStart(error && error.message ? error.message : 'hook helper failed')
2766
+ return
2767
+ }
2768
+ } else {
2769
+ // Other events retain the in-process fast path and existing fallback behavior.
2770
+ stdout = await tryInProcess(entryPath, tgEvent, canonical, 'copilot_cli')
2771
+ }
2772
+ if (stdout === undefined) {
2773
+ const res = entryPath
2774
+ ? spawnSync(process.execPath, [entryPath, 'hook', tgEvent], {
2775
+ input: JSON.stringify(canonical),
2776
+ encoding: 'utf8',
2777
+ windowsHide: true,
2778
+ timeout: SHIM_FALLBACK_TIMEOUT_MS,
2779
+ killSignal: 'SIGKILL',
2780
+ maxBuffer: SHIM_MAX_BUFFER_BYTES,
2781
+ env: Object.assign({}, process.env, { TOKEN_GOAT_HARNESS_OVERRIDE: 'copilot_cli', TOKEN_GOAT_COPILOT_HOOKS_DIR: __dirname }),
2782
+ })
2783
+ : spawnSync('token-goat hook ' + tgEvent, {
2784
+ input: JSON.stringify(canonical),
2785
+ encoding: 'utf8',
2786
+ shell: true,
2787
+ windowsHide: true,
2788
+ timeout: SHIM_FALLBACK_TIMEOUT_MS,
2789
+ killSignal: 'SIGKILL',
2790
+ maxBuffer: SHIM_MAX_BUFFER_BYTES,
2791
+ env: Object.assign({}, process.env, { TOKEN_GOAT_HARNESS_OVERRIDE: 'copilot_cli', TOKEN_GOAT_COPILOT_HOOKS_DIR: __dirname }),
2792
+ })
2793
+ if (res.status !== 0 || !res.stdout) {
2794
+ process.stdout.write('{}')
2795
+ return
2796
+ }
2797
+ stdout = res.stdout
2798
+ }
2799
+
2800
+ let resp
2801
+ try {
2802
+ resp = JSON.parse(stdout)
2803
+ } catch {
2804
+ process.stdout.write('{}')
2805
+ return
2806
+ }
2807
+
2808
+ process.stdout.write(JSON.stringify(translate(copilotEvent, resp, toolName, originalToolArgs)))
2809
+ }
2810
+
2811
+ ${MATERIALIZE_SHRUNK_IMAGE_JS}
2812
+
2813
+ function translate(copilotEvent, resp, toolName, originalToolArgs) {
2814
+ if (copilotEvent === 'preToolUse') {
2815
+ const hso = resp && resp.hookSpecificOutput
2816
+ const denied = resp && (resp.decision === 'block' || (hso && hso.permissionDecision === 'deny'))
2817
+ if (denied) {
2818
+ const reason =
2819
+ (resp && resp.reason) || (hso && hso.permissionDecisionReason) || 'blocked by token-goat'
2820
+ return { permissionDecision: 'deny', permissionDecisionReason: reason }
2821
+ }
2822
+ const context = extractContext(resp)
2823
+ // A shrink payload is a base64 data URL, not prose. It rides the same additionalContext field every other pre-tool hint does, so the two have to be told apart before either is forwarded: handing the model a data URL would cost far more context than the image it was shrunk from ever did. It is only ever materialized to a temp file the read argument is pointed at.
2824
+ const shrinkPayload = typeof context === 'string' && context.indexOf('data:image/') !== -1
2825
+ const out = {}
2826
+ const updated = hso && hso.updatedInput
2827
+ if (updated && typeof updated === 'object') {
2828
+ out.modifiedArgs = updated
2829
+ } else if (shrinkPayload && (toolName === 'view' || foldToolName(toolName) === 'view')) {
2830
+ // modifiedArgs REPLACES the tool call's args wholesale (ESr in the 1.0.80 bundle, see this module's header docblock), so the full original toolArgs are spread and only Copilot's own path key is swapped.
2831
+ const shrunkPath = materializeShrunkImage(context)
2832
+ if (shrunkPath) out.modifiedArgs = Object.assign({}, originalToolArgs, { path: shrunkPath })
2833
+ }
2834
+ // Every pre-tool hint token-goat emits -- read-dedup nudges, the bash surgical-read advisories, the write-path notices -- used to stop here, on the stated grounds that Copilot's preToolUse output schema has no additionalContext field. It has one, and has had one for every version this bridge was ever checked against: copilot-sdk/types.d.ts declares additionalContext as an optional string on PreToolUseHookOutput in 1.0.80, 1.0.82, 1.0.86, 1.0.87 and 1.0.88 alike, and the 1.0.88 native runtime (prebuilds/win32-x64/runtime.node) carries it in the preToolUse response key list beside the three keys already known to work -- permissionDecision, permissionDecisionReason, modifiedArgs, additionalContext, in that order, immediately before "preToolUse hook timed out; allowing the tool call to proceed". The same binary carries an aggregation limit message naming the field generically ("Ignoring additionalContext returned by a ... hook because aggregating it would exceed the ... byte limit"), which is the mechanism 1.0.88's own release note calls combining contributions within a limit. Forwarded alongside modifiedArgs rather than instead of it: the schema accepts both, and a rewritten argument is no reason to drop the sentence explaining it.
2835
+ if (context && !shrinkPayload) out.additionalContext = context
2836
+ return out
2837
+ }
2838
+
2839
+ if (copilotEvent === 'postToolUse') {
2840
+ // Each field carries one thing. modifiedResult replaces the tool output, so it is sent only for a real rewrite (compression, fencing, image shrink). additionalContext carries the hint: Copilot CLI 1.0.88 appends it to the tool output itself, under the heading Tool "<name>" succeeded. Additional guidance from postToolUse hooks:, followed by the text, once (captured on the wire, tg-captures C1a; with a modifiedResult alongside, C1c, each arrives once). This branch used to also fold the hint into modifiedResult, on a 1.0.80 reading that additionalContext was dropped here, so on 1.0.88 the model got every post-tool hint twice.
2841
+ const hso = resp && resp.hookSpecificOutput
2842
+ const updatedToolOutput = hso && hso.updatedToolOutput
2843
+ const context = extractContext(resp)
2844
+ const out = {}
2845
+ if (typeof updatedToolOutput === 'string') out.modifiedResult = { resultType: 'success', textResultForLlm: updatedToolOutput }
2846
+ if (context) out.additionalContext = context
2847
+ return out
2848
+ }
2849
+
2850
+ if (copilotEvent === 'postToolUseFailure') {
2851
+ // The failed-tool twin of postToolUse, and the only response field it accepts is
2852
+ // additionalContext: the shipped copilot-sdk/types.d.ts says "modifiedResult or suppressOutput
2853
+ // are not honored for failure hooks", so nothing here can fence, compress or shrink the failed
2854
+ // output -- that gap is real and stays open. What is NOT open, and was the reason this event
2855
+ // went unwired for so long, is whether additionalContext reaches the model at all. It does:
2856
+ // app.js 1.0.80 at offset 2043380 either folds it into textResultForLlm (when
2857
+ // appendFailureContextToToolResult is set) or has the native
2858
+ // hookAppendPostToolUseFailureContext push {content, source:'system'} onto
2859
+ // toolResult.newMessages. That is the exact opposite of postToolUse, whose additionalContext
2860
+ // the JS side drops on the floor, so neither event's behaviour generalises to the other.
2861
+ //
2862
+ // Because this channel spends tokens rather than saving them, the handler behind it
2863
+ // (hooks_tool_failure.ts) is silent on a first failure and speaks only on an exact repeat.
2864
+ const context = extractContext(resp)
2865
+ if (context) return { additionalContext: context }
2866
+ return {}
2867
+ }
2868
+
2869
+ if (copilotEvent === 'sessionStart' || copilotEvent === 'subagentStart') {
2870
+ // Neither event has a tool result to modify, so only additionalContext applies. sessionStart's
2871
+ // is the one channel that reaches the model before it picks its first read tool, so the routing
2872
+ // reminder lands there. subagentStart's is prepended to the subagent's own prompt (Copilot CLI
2873
+ // 1.0.88, capture C4a), which is where the spawn briefing belongs.
2874
+ const context = extractContext(resp)
2875
+ if (context) return { additionalContext: context }
2876
+ return {}
2877
+ }
2878
+
2879
+ if (copilotEvent === 'agentStop' || copilotEvent === 'subagentStop') {
2880
+ // Confirmed against the hooks reference doc: the only accepted response
2881
+ // shape for these two events is {decision, reason} -- additionalContext
2882
+ // is not part of their schema and Copilot silently ignores it there.
2883
+ // token-goat's internal 'stop'/'subagent_stop' handlers never return a
2884
+ // real deny today (subagentStopHandler only ever logs and passes), but a
2885
+ // future deny is mapped through here rather than silently dropped.
2886
+ if (resp && resp.decision === 'block') {
2887
+ const reason = (resp && resp.reason) || 'blocked by token-goat'
2888
+ return { decision: 'block', reason: reason }
2889
+ }
2890
+ // subagentStop also takes modifiedResponse, which replaces the subagent's answer in the
2891
+ // parent's task result and in the model request built from it (Copilot CLI 1.0.88,
2892
+ // tg-captures C4a: the marker a hook returned there was all the parent's model saw).
2893
+ const stopHso = resp && resp.hookSpecificOutput
2894
+ const modifiedResponse = stopHso && stopHso.updatedToolOutput
2895
+ if (copilotEvent === 'subagentStop' && typeof modifiedResponse === 'string') {
2896
+ return { modifiedResponse: modifiedResponse }
2897
+ }
2898
+ return { decision: 'allow' }
2899
+ }
2900
+
2901
+ if (copilotEvent === 'userPromptSubmitted') {
2902
+ // Copilot's own hooks reference says command-hook output here "is dropped, including
2903
+ // modifiedPrompt", and this branch used to believe it and return nothing. That is wrong for
2904
+ // additionalContext on 1.0.80, established by experiment rather than by reading: a config-file
2905
+ // command hook (at <cwd>/.github/hooks/, the project scope -- the user scope ~/.copilot/hooks/
2906
+ // was never exercised) returned {"additionalContext":"<marker>"} and the marker turned up
2907
+ // verbatim inside the session's user.message.transformedContent, wrapped in a
2908
+ // <system_reminder> block. What settles that it reached the model rather than only the on-disk
2909
+ // record is the billing: the provider's returned usage charged ~140 input tokens for a turn
2910
+ // whose raw content field is 35 bytes, so the marker's bytes were paid for whichever field
2911
+ // carried them. (The weaker argument first offered for this -- that transformedContent carries
2912
+ // an envelope the content field lacks, 29 B vs 195 B -- was measured on the control turn that
2913
+ // had no marker in it, and proves nothing about the marker.) That the model sees it is the
2914
+ // whole point: hook.start/hook.end records also persist and reach nothing.
2915
+ //
2916
+ // On 1.0.80 that was demonstrated once: of two turns, one delivered the marker and one fired
2917
+ // a hook that never ran the script. On 1.0.88 it held 5 times in 5 clean runs (tg-captures
2918
+ // C3): each wire request carried the context once, in the user message itself, as the prompt,
2919
+ // a blank line, and the context inside a <system_reminder> block.
2920
+ //
2921
+ // modifiedPrompt is NOT claimed to work and is not wanted: rewriting a user's prompt is far
2922
+ // more invasive than anything token-goat does, so only additionalContext is forwarded.
2923
+ //
2924
+ // This is also where the compaction manifest reaches the model. Copilot has no postCompact
2925
+ // hook and does not read preCompact's answer (below), so token-goat queues the manifest at
2926
+ // preCompact and the prompt handler puts it first in this answer on the next prompt
2927
+ // (PRE_COMPACT_RESUMES_ON_PROMPT in ../harness_channels.ts; tests/copilot_compaction_resume.test.ts
2928
+ // drives the captured C7 payloads through this shim). Whether the summary is recoverable from
2929
+ // events.jsonl is still not settled; see COPILOT_NO_POST_COMPACT_REASON in ../bridges_status.ts.
2930
+ const context = extractContext(resp)
2931
+ if (context) return { additionalContext: context }
2932
+ return {}
2933
+ }
2934
+
2935
+ // preCompact is the genuine notification-only case. The hooks reference marks it "No --
2936
+ // notification only", both preCompact call sites in app.js (1.0.79, re-checked in 1.0.80) await
2937
+ // the hook and never assign its result, and on 1.0.88 a marker returned here as
2938
+ // additionalContext appeared zero times in the wire requests, the OTel spans and the model's
2939
+ // reply after a manual /compact (tg-captures C7 and C7d). The payload is {sessionId, timestamp,
2940
+ // cwd, transcriptPath, trigger, customInstructions}. There is no field to aim at here.
2941
+ return {}
2942
+ }
2943
+
2944
+ function extractContext(resp) {
2945
+ const hso = resp && resp.hookSpecificOutput
2946
+ if (hso && typeof hso.additionalContext === 'string') return hso.additionalContext
2947
+ if (resp && typeof resp.systemMessage === 'string') return resp.systemMessage
2948
+ return undefined
2949
+ }
2950
+
2951
+ // Hard outer safety net, on top of main()'s own per-step try/catch fallbacks
2952
+ // (JSON.parse, readFileSync, spawnSync): a hook error must never itself cause
2953
+ // Copilot's fail-closed "(hook errored)" behavior, which denies EVERY tool
2954
+ // call unconditionally (the exact live-production failure mode behind
2955
+ // github/copilot-cli#4001). Any uncaught exception anywhere in this script --
2956
+ // including one a future code path adds that the existing per-step guards
2957
+ // don't anticipate -- still guarantees stdout gets valid JSON and the
2958
+ // process exits 0. process.exitCode is set explicitly and unconditionally at
2959
+ // the very end of every path so nothing upstream (e.g. an unhandled-rejection
2960
+ // warning in some Node versions nudging exit-code inference) can flip it.
2961
+ main()
2962
+ .catch(() => {
2963
+ try {
2964
+ process.stdout.write('{}')
2965
+ } catch {
2966
+ // stdout itself is broken; nothing more can be done here.
2967
+ }
2968
+ })
2969
+ .finally(() => {
2970
+ process.exitCode = 0
2971
+ })
2972
+ `;
2973
+ var TOOL_TO_TG_MAPPING = COPILOT_CLI_TOOL_NAME_MAP;
2974
+ var FOLDED_MAPPING = {};
2975
+ for (const [k, v] of Object.entries(TOOL_TO_TG_MAPPING)) {
2976
+ FOLDED_MAPPING[foldToolName(k)] = v;
2977
+ FOLDED_MAPPING[foldToolName(v)] = v;
2978
+ }
2979
+
2980
+ // src/bridges/copilot_cli_install.ts
2981
+ init_define_import_meta_env();
2982
+ import * as fs7 from "node:fs";
2983
+ import * as path7 from "node:path";
2984
+
2985
+ // src/bridges/copilot_mcp_install.ts
2986
+ init_define_import_meta_env();
2987
+ import * as fs5 from "node:fs";
2988
+ import * as path5 from "node:path";
2989
+
2990
+ // src/bridges/mcp_servers_json.ts
2991
+ init_define_import_meta_env();
2992
+ import * as fs4 from "node:fs";
2993
+ import * as path4 from "node:path";
2994
+ import { fileURLToPath } from "node:url";
2995
+ function bundledCliPath() {
2996
+ const here = path4.dirname(fileURLToPath(import.meta.url));
2997
+ const bundled = path4.join(here, "token-goat.mjs");
2998
+ if (fs4.existsSync(bundled)) return bundled;
2999
+ return path4.resolve(here, "..", "..", "dist", "token-goat.mjs");
3000
+ }
3001
+ function managedServer() {
3002
+ return {
3003
+ type: "stdio",
3004
+ command: process.execPath,
3005
+ args: [bundledCliPath(), "mcp-serve"]
3006
+ };
3007
+ }
3008
+ function isManagedServer(value) {
3009
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
3010
+ const entry = value;
3011
+ const args = entry["args"];
3012
+ return entry["type"] === "stdio" && Array.isArray(args) && args.length === 2 && typeof args[0] === "string" && path4.basename(args[0]).toLowerCase() === "token-goat.mjs" && args[1] === "mcp-serve";
3013
+ }
3014
+ function readServersJson(filePath, label) {
3015
+ const text = fs4.existsSync(filePath) ? stripBom(fs4.readFileSync(filePath, "utf8")) : "{}\n";
3016
+ const errors = [];
3017
+ const parsed = jsonc().parse(text, errors, { allowTrailingComma: true, disallowComments: false });
3018
+ if (errors.length > 0) {
3019
+ throw new Error(`malformed ${label} MCP JSON at ${filePath}`);
3020
+ }
3021
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
3022
+ throw new Error(`malformed ${label} MCP JSON at ${filePath}: expected a JSON object`);
3023
+ }
3024
+ return { text, value: parsed };
3025
+ }
3026
+ function serversOf(config, filePath, label, rootKey = "servers") {
3027
+ const servers = config.value[rootKey];
3028
+ if (servers === void 0) return {};
3029
+ if (servers === null || typeof servers !== "object" || Array.isArray(servers)) {
3030
+ throw new Error(`malformed ${label} MCP JSON at ${filePath}: ${rootKey} must be an object`);
3031
+ }
3032
+ return servers;
3033
+ }
3034
+ var FORMAT = { insertSpaces: true, tabSize: 2, eol: "\n" };
3035
+ function editAt(text, jsonPath, value) {
3036
+ let out = text;
3037
+ for (const e of [...jsonc().modify(text, jsonPath, value, {})].sort((a, b) => b.offset - a.offset)) {
3038
+ out = out.slice(0, e.offset) + e.content + out.slice(e.offset + e.length);
3039
+ if (e.content.length > 0) out = jsonc().applyEdits(out, jsonc().format(out, { offset: e.offset, length: e.content.length }, FORMAT));
3040
+ }
3041
+ return out;
3042
+ }
3043
+ function setTokenGoatServer(text, value, rootKey = "servers") {
3044
+ if (value !== void 0 && /^\s*\{\s*\}\s*$/.test(text)) return `${JSON.stringify({ [rootKey]: { "token-goat": value } }, null, 2)}
3045
+ `;
3046
+ return editAt(text, [rootKey, "token-goat"], value);
3047
+ }
3048
+ function holdsOnlyManagedServer(value) {
3049
+ const keys = Object.keys(value);
3050
+ if (keys.length !== 1) return false;
3051
+ const root = keys[0];
3052
+ if (root !== "servers" && root !== "mcpServers") return false;
3053
+ const servers = value[root];
3054
+ if (servers === null || typeof servers !== "object" || Array.isArray(servers)) return false;
3055
+ const names = Object.keys(servers);
3056
+ return names.length === 1 && names[0] === "token-goat" && isManagedServer(servers["token-goat"]);
3057
+ }
3058
+ function isResidueServersJson(text, rootKey = "servers") {
3059
+ const trimmed = text.trim();
3060
+ if (trimmed === "") return true;
3061
+ const parsed = parseObject(trimmed);
3062
+ if (parsed === null) return false;
3063
+ const keys = Object.keys(parsed);
3064
+ if (keys.length !== 1 || keys[0] !== rootKey) return false;
3065
+ const servers = parsed[rootKey];
3066
+ return servers !== null && typeof servers === "object" && !Array.isArray(servers) && Object.keys(servers).length === 0;
3067
+ }
3068
+ function noteRootKeyCreation(filePath, config, rootKey = "servers") {
3069
+ if (config.value[rootKey] === void 0) recordCreatedRootKey(filePath, rootKey);
3070
+ }
3071
+ function dropEmptyServers(text, filePath, rootKey = "servers") {
3072
+ const owned = takeCreatedRootKey(filePath, rootKey) || hasCreatedConfig(filePath);
3073
+ const parsed = parseObject(text);
3074
+ if (!owned || parsed === null) return text;
3075
+ const servers = parsed[rootKey];
3076
+ if (servers === null || typeof servers !== "object" || Array.isArray(servers) || Object.keys(servers).length > 0) return text;
3077
+ return editAt(text, [rootKey], void 0);
3078
+ }
3079
+ function parseObject(text) {
3080
+ const parsed = jsonc().parse(text, [], { allowTrailingComma: true, disallowComments: false });
3081
+ return parsed === null || typeof parsed !== "object" || Array.isArray(parsed) ? null : parsed;
3082
+ }
3083
+ function ensureMcpServersKey(text) {
3084
+ const parsed = parseObject(text);
3085
+ if (parsed === null || "mcpServers" in parsed) return text;
3086
+ return editAt(text, ["mcpServers"], {});
3087
+ }
3088
+ function dropLoneEmptyMcpServers(text) {
3089
+ const parsed = parseObject(text);
3090
+ if (parsed === null || Object.keys(parsed).length !== 1) return text;
3091
+ const mcpServers = parsed["mcpServers"];
3092
+ if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers) || Object.keys(mcpServers).length > 0) return text;
3093
+ return editAt(text, ["mcpServers"], void 0);
3094
+ }
3095
+ function managedServerEntry(filePath, label, rootKey = "servers", isManaged = isManagedServer) {
3096
+ if (!fs4.existsSync(filePath)) return null;
3097
+ try {
3098
+ const entry = serversOf(readServersJson(filePath, label), filePath, label, rootKey)["token-goat"];
3099
+ return isManaged(entry) ? entry : null;
3100
+ } catch {
3101
+ return null;
3102
+ }
3103
+ }
3104
+ function hasManagedServer(filePath, label, rootKey = "servers", isManaged = isManagedServer) {
3105
+ return managedServerEntry(filePath, label, rootKey, isManaged) !== null;
3106
+ }
3107
+
3108
+ // src/bridges/copilot_mcp_install.ts
3109
+ var MCP_SERVERS_KEY = "mcpServers";
3110
+ var TOKEN_GOAT_ENTRY_KEY = "token-goat";
3111
+ var LABEL = "Copilot CLI";
3112
+ function copilotMcpConfigPath() {
3113
+ return path5.join(copilotCliUserRoot(), "mcp-config.json");
3114
+ }
3115
+ function copilotManagedServer() {
3116
+ return { tools: ["*"], type: "local", command: process.execPath, args: [bundledCliPath(), "mcp-serve"] };
3117
+ }
3118
+ function isCopilotManagedServer(value) {
3119
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
3120
+ const entry = value;
3121
+ const args = entry["args"];
3122
+ return entry["type"] === "local" && typeof entry["command"] === "string" && Array.isArray(args) && args.length === 2 && typeof args[0] === "string" && path5.basename(args[0]).toLowerCase() === "token-goat.mjs" && args[1] === "mcp-serve";
3123
+ }
3124
+ function isReadmeServer(value) {
3125
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
3126
+ const entry = value;
3127
+ const args = entry["args"];
3128
+ return Object.keys(entry).every((k) => k === "command" || k === "args") && entry["command"] === "token-goat" && Array.isArray(args) && args.length === 1 && args[0] === "mcp-serve";
3129
+ }
3130
+ function installCopilotMcpServer() {
3131
+ const mcpPath = copilotMcpConfigPath();
3132
+ const fileExisted = fs5.existsSync(mcpPath);
3133
+ const config = readServersJson(mcpPath, LABEL);
3134
+ const current = serversOf(config, mcpPath, LABEL, MCP_SERVERS_KEY)[TOKEN_GOAT_ENTRY_KEY];
3135
+ if (current !== void 0 && !isCopilotManagedServer(current) && !isReadmeServer(current)) {
3136
+ throw new Error(`Copilot CLI mcp-config.json already has a ${echoedValue(TOKEN_GOAT_ENTRY_KEY)} MCP server entry token-goat did not write, at ${mcpPath}; remove it manually first`);
3137
+ }
3138
+ const nextText = setTokenGoatServer(config.text, copilotManagedServer(), MCP_SERVERS_KEY);
3139
+ if (nextText === config.text) return false;
3140
+ ensureDirSync(path5.dirname(mcpPath));
3141
+ if (!fileExisted) recordCreatedConfig(mcpPath);
3142
+ backupFile(mcpPath);
3143
+ writeConfigText(mcpPath, nextText);
3144
+ noteRootKeyCreation(mcpPath, config, MCP_SERVERS_KEY);
3145
+ return true;
3146
+ }
3147
+ function uninstallCopilotMcpServer() {
3148
+ const mcpPath = copilotMcpConfigPath();
3149
+ if (!fs5.existsSync(mcpPath)) return false;
3150
+ const config = readServersJson(mcpPath, LABEL);
3151
+ if (!isCopilotManagedServer(serversOf(config, mcpPath, LABEL, MCP_SERVERS_KEY)[TOKEN_GOAT_ENTRY_KEY])) return false;
3152
+ const next = dropEmptyServers(setTokenGoatServer(config.text, void 0, MCP_SERVERS_KEY), mcpPath, MCP_SERVERS_KEY);
3153
+ if (/^\s*\{\s*\}\s*$/.test(next) && takeCreatedConfig(mcpPath)) {
3154
+ removeFileInScope(mcpPath);
3155
+ } else {
3156
+ backupFile(mcpPath);
3157
+ writeConfigText(mcpPath, next);
3158
+ }
3159
+ removeCreatedBackups(mcpPath);
3160
+ return true;
3161
+ }
3162
+
3163
+ // src/bridges/visualstudio_install.ts
3164
+ init_define_import_meta_env();
3165
+ import * as fs6 from "node:fs";
3166
+ import * as os2 from "node:os";
3167
+ import * as path6 from "node:path";
3168
+ var LABEL2 = "Visual Studio";
3169
+ var VISUALSTUDIO_GUIDANCE_BEGIN = "<!-- token-goat-visualstudio-begin -->";
3170
+ var VISUALSTUDIO_GUIDANCE_END = "<!-- token-goat-visualstudio-end -->";
3171
+ var SIBLING_GATE_MARKERS = [
3172
+ ["<!-- token-goat-vscode-begin -->", "<!-- token-goat-vscode-end -->"],
3173
+ ["<!-- token-goat-begin -->", "<!-- token-goat-end -->"]
3174
+ ];
3175
+ function visualStudioUserMcpPath() {
3176
+ return path6.join(os2.homedir(), ".mcp.json");
3177
+ }
3178
+ function visualStudioProjectMcpPath(projectRoot = process.cwd()) {
3179
+ return path6.join(path6.resolve(projectRoot), ".mcp.json");
3180
+ }
3181
+ function visualStudioMcpPath(opts = {}) {
3182
+ return opts.project === true ? visualStudioProjectMcpPath(opts.projectRoot) : visualStudioUserMcpPath();
3183
+ }
3184
+ function otherScopeMcpPath(opts) {
3185
+ return opts.project === true ? visualStudioUserMcpPath() : visualStudioProjectMcpPath(opts.projectRoot);
3186
+ }
3187
+ function visualStudioInstructionsPath(opts = {}) {
3188
+ return opts.project === true ? path6.join(path6.resolve(opts.projectRoot ?? process.cwd()), ".github", "copilot-instructions.md") : path6.join(os2.homedir(), "copilot-instructions.md");
3189
+ }
3190
+ function visualStudioOtherScopeHasManagedServer(opts = {}) {
3191
+ return hasManagedServer(otherScopeMcpPath(opts), LABEL2);
3192
+ }
3193
+ function isVisualStudioInstalled(opts = {}) {
3194
+ return hasManagedServer(visualStudioMcpPath(opts), LABEL2);
3195
+ }
3196
+ function visualStudioSolutionVscodeMcpPath(projectRoot = process.cwd()) {
3197
+ return path6.join(path6.resolve(projectRoot), ".vscode", "mcp.json");
3198
+ }
3199
+ function visualStudioManagedRegistrations(projectRoot = process.cwd()) {
3200
+ return [visualStudioUserMcpPath(), visualStudioProjectMcpPath(projectRoot), visualStudioSolutionVscodeMcpPath(projectRoot)].filter((p) => hasManagedServer(p, LABEL2));
3201
+ }
3202
+ function visualStudioDuplicateNote(projectRoot = process.cwd()) {
3203
+ const found = visualStudioManagedRegistrations(projectRoot);
3204
+ if (found.length < 2) return null;
3205
+ const lead = `Note: Visual Studio reads ${found.join(" and ")}, and each registers token-goat, so it will list the token-goat server more than once for this solution.`;
3206
+ if (!found.includes(visualStudioSolutionVscodeMcpPath(projectRoot))) return `${lead} Keep one: run ${fencedCommand("token-goat uninstall --visualstudio")} or ${fencedCommand("token-goat uninstall --visualstudio -p")}.`;
3207
+ return `${lead} Keep one: if you do not use VS Code in this folder, run ${fencedCommand("token-goat uninstall --vscode -p")}; otherwise run ${fencedCommand("token-goat uninstall --visualstudio" + (found.includes(visualStudioUserMcpPath()) ? "" : " -p"))}.`;
3208
+ }
3209
+ function visualStudioMcpStatus(opts = {}) {
3210
+ const checkedPaths = [visualStudioUserMcpPath()];
3211
+ if (opts.projectRoot !== void 0) checkedPaths.push(visualStudioProjectMcpPath(opts.projectRoot));
3212
+ return { configured: checkedPaths.some((p) => hasManagedServer(p, LABEL2)), checkedPaths };
3213
+ }
3214
+ function visualStudioManagedEntry(filePath) {
3215
+ const entry = managedServerEntry(filePath, LABEL2);
3216
+ if (entry === null) return null;
3217
+ const { command, args } = entry;
3218
+ return { command: typeof command === "string" ? command : "", bundlePath: args[0] ?? "" };
3219
+ }
3220
+ var FALLBACK_TOOL_CLAUSE = "Visual Studio\u2019s built-in readfile, code_search, and find_references tools";
3221
+ var RETRIEVE_NOTE = "**Compressed payloads:** a message containing a token-goat payload block (recognizable by a `recovery: token-goat retrieve <id>` line) is compressed text, not an answer. Call the MCP tool `retrieve_text` with that id to recover the original text, then answer the question the message asks using the recovered text. Never present the raw payload to the user as the response; if the `retrieve_text` tool is unavailable (the MCP server is not running, or its tools are not ticked in the chat Tools picker), say so plainly and ask the user to turn on the token-goat tools.";
3222
+ var SUPPORT_NOTE = "Visual Studio support: token-goat install --visualstudio registers a stdio MCP server under the servers root key of %USERPROFILE%\\.mcp.json (with --project, the .mcp.json in the solution folder). Visual Studio runs no token-goat hooks, so nothing denies a repeated read, adds a hint, shrinks an image, or folds what readfile returns there: the narrow reads come only from calling the token-goat MCP tools, or the token-goat CLI through runcommandinterminal.";
3223
+ function buildVisualStudioGuidanceBlock(sharesFileWithGate) {
3224
+ const lead = sharesFileWithGate ? ["## token-goat in Visual Studio", "", `The token-goat gate elsewhere in this file applies in Visual Studio too; there, ${FALLBACK_TOOL_CLAUSE} are the fallback it names.`] : [buildGuidanceBody(FALLBACK_TOOL_CLAUSE, { gdrive: loadConfig().gdrive.enabled })];
3225
+ return [VISUALSTUDIO_GUIDANCE_BEGIN, ...lead, "", RETRIEVE_NOTE, "", SUPPORT_NOTE, VISUALSTUDIO_GUIDANCE_END].join("\n");
3226
+ }
3227
+ function hasBlock(text, begin, end) {
3228
+ const b = text.indexOf(begin);
3229
+ const e = text.indexOf(end);
3230
+ return b !== -1 && e > b;
3231
+ }
3232
+ function readText(filePath) {
3233
+ try {
3234
+ return fs6.readFileSync(filePath, "utf8");
3235
+ } catch {
3236
+ return "";
3237
+ }
3238
+ }
3239
+ function writeGuidance(filePath) {
3240
+ const text = readText(filePath);
3241
+ const shares = SIBLING_GATE_MARKERS.some(([begin, end]) => hasBlock(text, begin, end));
3242
+ return upsertDelimitedBlock(filePath, VISUALSTUDIO_GUIDANCE_BEGIN, VISUALSTUDIO_GUIDANCE_END, buildVisualStudioGuidanceBlock(shares));
3243
+ }
3244
+ function syncVisualStudioProjectGuidance(filePath) {
3245
+ if (!hasBlock(readText(filePath), VISUALSTUDIO_GUIDANCE_BEGIN, VISUALSTUDIO_GUIDANCE_END)) return false;
3246
+ return writeGuidance(filePath);
3247
+ }
3248
+ function installVisualStudio(opts = {}) {
3249
+ return withInstallScope(projectScopeRoot(opts), () => installVisualStudioScoped(opts));
3250
+ }
3251
+ function installVisualStudioScoped(opts) {
3252
+ const scope = opts.project === true ? "project" : "user";
3253
+ const mcpPath = visualStudioMcpPath(opts);
3254
+ const instructionsPath = visualStudioInstructionsPath(opts);
3255
+ if (visualStudioOtherScopeHasManagedServer(opts)) {
3256
+ const otherScope = scope === "project" ? "user" : "project";
3257
+ throw new Error(
3258
+ `token-goat is already registered in Visual Studio ${otherScope} scope (${otherScopeMcpPath(opts)}). Visual Studio reads both files, so installing into ${scope} scope too would register it twice. Run ${fencedCommand("token-goat uninstall --visualstudio" + (otherScope === "project" ? " --project" : ""))} first if you want to move it, or drop --visualstudio from this run.`
3259
+ );
3260
+ }
3261
+ const mcpExisted = fs6.existsSync(mcpPath);
3262
+ const config = readServersJson(mcpPath, LABEL2);
3263
+ const ownedAlready = mcpExisted && holdsOnlyManagedServer(config.value);
3264
+ const current = serversOf(config, mcpPath, LABEL2)["token-goat"];
3265
+ if (current !== void 0 && !isManagedServer(current)) {
3266
+ throw new Error(`Visual Studio MCP JSON at ${mcpPath} already has a non-token-goat-managed server named "token-goat"`);
3267
+ }
3268
+ const next = ensureMcpServersKey(setTokenGoatServer(config.text, managedServer()));
3269
+ if (config.text !== next) {
3270
+ ensureDirSync(path6.dirname(mcpPath));
3271
+ backupFile(mcpPath);
3272
+ writeConfigText(mcpPath, next);
3273
+ noteRootKeyCreation(mcpPath, config);
3274
+ if (!mcpExisted || ownedAlready) recordCreatedConfig(mcpPath);
3275
+ }
3276
+ const guidanceChanged = writeGuidance(instructionsPath);
3277
+ return { mcpPath, instructionsPath, alreadyInstalled: config.text === next && !guidanceChanged, scope };
3278
+ }
3279
+ function uninstallVisualStudio(opts = {}) {
3280
+ return withInstallScope(projectScopeRoot(opts), () => uninstallVisualStudioScoped(opts));
3281
+ }
3282
+ function uninstallVisualStudioScoped(opts) {
3283
+ const mcpPath = visualStudioMcpPath(opts);
3284
+ let removed = false;
3285
+ if (fs6.existsSync(mcpPath)) {
3286
+ const config = readServersJson(mcpPath, LABEL2);
3287
+ if (isManagedServer(serversOf(config, mcpPath, LABEL2)["token-goat"])) {
3288
+ const next = dropEmptyServers(setTokenGoatServer(config.text, void 0), mcpPath);
3289
+ if (/^\s*\{\s*\}\s*$/.test(dropLoneEmptyMcpServers(next)) && takeCreatedConfig(mcpPath)) removeFileInScope(mcpPath);
3290
+ else {
3291
+ backupFile(mcpPath);
3292
+ writeConfigText(mcpPath, ensureMcpServersKey(next));
3293
+ }
3294
+ removeCreatedBackups(mcpPath);
3295
+ removed = true;
3296
+ }
3297
+ }
3298
+ const instructionsPath = visualStudioInstructionsPath(opts);
3299
+ if (stripDelimitedBlock(instructionsPath, VISUALSTUDIO_GUIDANCE_BEGIN, VISUALSTUDIO_GUIDANCE_END)) {
3300
+ removed = true;
3301
+ if (readText(instructionsPath).trim() === "") removeFileInScope(instructionsPath);
3302
+ }
3303
+ return removed;
3304
+ }
3305
+
3306
+ // src/bridges/copilot_cli_install.ts
3307
+ var COPILOT_CLI_HOOK_EVENTS = [
3308
+ "sessionStart",
3309
+ "preToolUse",
3310
+ "postToolUse",
3311
+ "preCompact",
3312
+ "agentStop",
3313
+ "subagentStop",
3314
+ "userPromptSubmitted",
3315
+ "postToolUseFailure",
3316
+ "subagentStart"
3317
+ ];
3318
+ var VSCODE_ONLY_HOOK_EVENTS = ["SubagentStart"];
3319
+ function copilotCliUserHooksDir() {
3320
+ return path7.join(copilotCliUserRoot(), "hooks");
3321
+ }
3322
+ function canonicalProjectRoot(projectRoot) {
3323
+ return process.platform === "win32" ? projectRoot.replace(/^[a-z]:/, (drive) => drive.toUpperCase()) : projectRoot;
3324
+ }
3325
+ function copilotCliProjectHooksDir(projectRoot) {
3326
+ return path7.join(canonicalProjectRoot(projectRoot ?? process.cwd()), ".github", "hooks");
3327
+ }
3328
+ function copilotCliHooksDir(opts = {}) {
3329
+ return opts.local === true ? copilotCliProjectHooksDir(opts.projectRoot) : copilotCliUserHooksDir();
3330
+ }
3331
+ function copilotCliConfigPath(opts = {}) {
3332
+ return path7.join(copilotCliHooksDir(opts), HOOKS_CONFIG_FILE);
3333
+ }
3334
+ function copilotCliScriptPath(opts = {}) {
3335
+ return path7.join(copilotCliHooksDir(opts), HOOKS_SCRIPT_FILE);
3336
+ }
3337
+ function copilotCliInstructionsPath(opts = {}) {
3338
+ return path7.join(path7.dirname(copilotCliHooksDir(opts)), "copilot-instructions.md");
3339
+ }
3340
+ var COPILOT_INSTRUCTIONS_BEGIN = "<!-- token-goat-begin -->";
3341
+ var COPILOT_INSTRUCTIONS_END = "<!-- token-goat-end -->";
3342
+ function buildCopilotInstructionsBlock() {
3343
+ return stripInlineCodeSpans(
3344
+ buildGuidanceBlock({
3345
+ beginMarker: COPILOT_INSTRUCTIONS_BEGIN,
3346
+ endMarker: COPILOT_INSTRUCTIONS_END,
3347
+ fallbackToolClause: "Copilot CLI's native `view`, `grep`, and `glob` tools (with PowerShell commands `Get-Content`/`Select-String` as search fallbacks)",
3348
+ gdrive: loadConfig().gdrive?.enabled ?? false
3349
+ })
3350
+ );
3351
+ }
3352
+ function stripInlineCodeSpans(text) {
3353
+ return text.replace(/`([^`]+)`/g, "$1");
3354
+ }
3355
+ function writeCopilotInstructionsBlock(p) {
3356
+ const changed = upsertDelimitedBlock(p, COPILOT_INSTRUCTIONS_BEGIN, COPILOT_INSTRUCTIONS_END, buildCopilotInstructionsBlock());
3357
+ const visualStudioChanged = syncVisualStudioProjectGuidance(p);
3358
+ return changed || visualStudioChanged;
3359
+ }
3360
+ function stripCopilotInstructionsBlock(p) {
3361
+ const removed = stripDelimitedBlock(p, COPILOT_INSTRUCTIONS_BEGIN, COPILOT_INSTRUCTIONS_END);
3362
+ if (removed) syncVisualStudioProjectGuidance(p);
3363
+ return removed;
3364
+ }
3365
+ function hookPowershellCommandFor(scriptPath, event) {
3366
+ return powershellHookLine(hookPowershellCommand(scriptPath, event));
3367
+ }
3368
+ var HOOK_TIMEOUT_SEC = 60;
3369
+ var ALLOWED_ENV_VARS = [
3370
+ "TRACEPARENT",
3371
+ "TRACESTATE",
3372
+ "COPILOT_HOME",
3373
+ "COPILOT_CACHE_HOME",
3374
+ "TOKEN_GOAT_LOG"
3375
+ ];
3376
+ function copilotHookCommandsFor(scriptPath, event, opts = {}) {
3377
+ const bin = nativeHookBinary(process.argv[1], opts);
3378
+ const command = bin === void 0 ? void 0 : nativeHookCommandLine(process.platform === "win32" ? "cmd" : "sh", bin, "copilot_cli", scriptPath, event);
3379
+ const sh = bin === void 0 ? void 0 : nativeHookCommandLine("sh", bin, "copilot_cli", scriptPath, event);
3380
+ const ps = bin === void 0 ? void 0 : nativeHookCommandLine("powershell", bin, "copilot_cli", scriptPath, event);
3381
+ if (command !== void 0 && sh !== void 0 && ps !== void 0) return { command, bash: sh, powershell: ps };
3382
+ return { command: hookCommandFor(scriptPath, event), bash: hookCommandFor(scriptPath, event), powershell: hookPowershellCommandFor(scriptPath, event) };
3383
+ }
3384
+ function wiredCopilotHookWords(opts = {}) {
3385
+ let config;
3386
+ try {
3387
+ config = JSON.parse(stripBom(fs7.readFileSync(copilotCliConfigPath(opts), "utf8")));
3388
+ } catch {
3389
+ return [];
3390
+ }
3391
+ const scriptPath = copilotCliScriptPath(opts);
3392
+ const out = [];
3393
+ for (const [event, entries] of Object.entries(config?.hooks ?? {})) {
3394
+ const known = COPILOT_CLI_HOOK_EVENTS.includes(event) || VSCODE_ONLY_HOOK_EVENTS.includes(event);
3395
+ const expected = known ? copilotHookCommandsFor(scriptPath, event, { sync: false }) : void 0;
3396
+ for (const h of Array.isArray(entries) ? entries : []) {
3397
+ if (typeof h?.command !== "string") continue;
3398
+ const current = expected !== void 0 && h.command === expected.command && h.bash === expected.bash && h.powershell === expected.powershell;
3399
+ out.push({ words: splitHookCommand(h.command, process.platform === "win32" ? "cmd" : "sh"), current });
3400
+ }
3401
+ }
3402
+ return out;
3403
+ }
3404
+ function buildConfig(scriptPath, owners) {
3405
+ const hooks = {};
3406
+ const events = owners.has("vscode") ? [...COPILOT_CLI_HOOK_EVENTS, ...VSCODE_ONLY_HOOK_EVENTS] : COPILOT_CLI_HOOK_EVENTS;
3407
+ for (const event of events) {
3408
+ hooks[event] = [
3409
+ {
3410
+ type: "command",
3411
+ ...copilotHookCommandsFor(scriptPath, event),
3412
+ timeoutSec: HOOK_TIMEOUT_SEC,
3413
+ allowedEnvVars: [...ALLOWED_ENV_VARS]
3414
+ }
3415
+ ];
3416
+ }
3417
+ return { version: 1, hooks };
3418
+ }
3419
+ var HOOKS_SCRIPT_FILE = "token-goat-shim.cjs";
3420
+ var LEGACY_HOOKS_SCRIPT_FILE = "token-goat-shim.js";
3421
+ var HOOKS_SCRIPT_FORWARDER = `#!/usr/bin/env node
3422
+ // Forwarder for compatibility with cached hook definitions in running Copilot CLI sessions.
3423
+ import('./token-goat-shim.cjs').catch(() => {
3424
+ try { process.stdout.write('{}') } catch {}
3425
+ process.exitCode = 0
3426
+ });
3427
+ `;
3428
+ function copilotHooksFilePaths(hooksDir) {
3429
+ return [
3430
+ path7.join(hooksDir, HOOKS_CONFIG_FILE),
3431
+ path7.join(hooksDir, HOOKS_SCRIPT_FILE),
3432
+ path7.join(hooksDir, LEGACY_HOOKS_SCRIPT_FILE),
3433
+ copilotHooksOwnersPath(hooksDir)
3434
+ ];
3435
+ }
3436
+ function installCopilotHooksFile(hooksDir, owner) {
3437
+ const configPath = path7.join(hooksDir, HOOKS_CONFIG_FILE);
3438
+ const scriptPath = path7.join(hooksDir, HOOKS_SCRIPT_FILE);
3439
+ const legacyScriptPath = path7.join(hooksDir, LEGACY_HOOKS_SCRIPT_FILE);
3440
+ const owners = readCopilotHooksOwners(hooksDir);
3441
+ ensureDirRecordingCreation(path7.dirname(hooksDir));
3442
+ ensureDirRecordingCreation(hooksDir);
3443
+ const scriptChanged = writeIfDifferent(scriptPath, COPILOT_CLI_HOOK_SCRIPT);
3444
+ const legacyScriptChanged = writeIfDifferent(legacyScriptPath, HOOKS_SCRIPT_FORWARDER);
3445
+ owners.add(owner);
3446
+ const desiredText = JSON.stringify(buildConfig(scriptPath, owners), null, 2) + "\n";
3447
+ const configChanged = writeIfDifferent(configPath, desiredText, true);
3448
+ recordCreatedConfig(configPath);
3449
+ const ownersChanged = writeIfDifferent(copilotHooksOwnersPath(hooksDir), [...owners].sort().join("\n") + "\n");
3450
+ return { configPath, scriptPath, changed: scriptChanged || legacyScriptChanged || configChanged || ownersChanged };
3451
+ }
3452
+ function releaseCopilotHooksFile(hooksDir, owner, keepBackups = false) {
3453
+ const ownersPath = copilotHooksOwnersPath(hooksDir);
3454
+ const hadSidecar = fs7.existsSync(ownersPath);
3455
+ const owners = readCopilotHooksOwners(hooksDir);
3456
+ if (!owners.has(owner) && (hadSidecar || owner !== "copilot")) return false;
3457
+ owners.delete(owner);
3458
+ if (owners.size > 0) {
3459
+ writeIfDifferent(ownersPath, [...owners].sort().join("\n") + "\n");
3460
+ const configPath = path7.join(hooksDir, HOOKS_CONFIG_FILE);
3461
+ if (fs7.existsSync(configPath)) writeIfDifferent(configPath, JSON.stringify(buildConfig(path7.join(hooksDir, HOOKS_SCRIPT_FILE), owners), null, 2) + "\n", true);
3462
+ return true;
3463
+ }
3464
+ const configRemoved = removeFileInScope(path7.join(hooksDir, HOOKS_CONFIG_FILE));
3465
+ if (!keepBackups) removeCreatedBackups(path7.join(hooksDir, HOOKS_CONFIG_FILE));
3466
+ takeCreatedConfig(path7.join(hooksDir, HOOKS_CONFIG_FILE));
3467
+ const scriptRemoved = removeFileInScope(path7.join(hooksDir, HOOKS_SCRIPT_FILE));
3468
+ const legacyScriptRemoved = removeFileInScope(path7.join(hooksDir, LEGACY_HOOKS_SCRIPT_FILE));
3469
+ const ownersRemoved = removeFileInScope(ownersPath);
3470
+ removeCreatedIfEmpty(hooksDir);
3471
+ return configRemoved || scriptRemoved || legacyScriptRemoved || ownersRemoved;
3472
+ }
3473
+ function installCopilotCli(opts = {}) {
3474
+ return withInstallScope(projectScopeRoot(opts), () => installCopilotCliScoped(opts));
3475
+ }
3476
+ function syncCopilotHooksAndInstructions(opts) {
3477
+ const hooks = installCopilotHooksFile(copilotCliHooksDir(opts), "copilot");
3478
+ const instructionsPath = copilotCliInstructionsPath(opts);
3479
+ const instructionsExisted = fs7.existsSync(instructionsPath);
3480
+ const instructionsChanged = writeCopilotInstructionsBlock(instructionsPath);
3481
+ if (!instructionsExisted) recordCreatedConfig(instructionsPath);
3482
+ return { hooks, instructionsChanged };
3483
+ }
3484
+ function repairCopilotCliHooks(opts = {}) {
3485
+ return withInstallScope(projectScopeRoot(opts), () => {
3486
+ const { hooks, instructionsChanged } = syncCopilotHooksAndInstructions(opts);
3487
+ return hooks.changed || instructionsChanged;
3488
+ });
3489
+ }
3490
+ function installCopilotCliScoped(opts) {
3491
+ const instructionsPath = copilotCliInstructionsPath(opts);
3492
+ const userScope = opts.local !== true;
3493
+ const scopeDir = path7.dirname(instructionsPath);
3494
+ const scopeDirExisted = fs7.existsSync(scopeDir);
3495
+ const mcpChanged = userScope ? installCopilotMcpServer() : false;
3496
+ if (!scopeDirExisted && fs7.existsSync(scopeDir)) recordCreatedConfig(scopeDir);
3497
+ const { hooks, instructionsChanged: scopeInstructionsChanged } = syncCopilotHooksAndInstructions(opts);
3498
+ let instructionsChanged = scopeInstructionsChanged;
3499
+ if (userScope) {
3500
+ const projectRoot = path7.resolve(opts.projectRoot ?? process.cwd());
3501
+ const projectInstructions = copilotCliInstructionsPath({ local: true, projectRoot });
3502
+ if (fs7.existsSync(projectInstructions)) {
3503
+ const projectChanged = writeCopilotInstructionsBlock(projectInstructions);
3504
+ if (projectChanged) instructionsChanged = true;
3505
+ }
3506
+ }
3507
+ return {
3508
+ configPath: hooks.configPath,
3509
+ scriptPath: hooks.scriptPath,
3510
+ instructionsPath,
3511
+ ...userScope ? { mcpConfigPath: copilotMcpConfigPath() } : {},
3512
+ alreadyInstalled: !hooks.changed && !instructionsChanged && !mcpChanged
3513
+ };
3514
+ }
3515
+ function uninstallCopilotCliScope(opts) {
3516
+ return withInstallScope(projectScopeRoot(opts), () => uninstallCopilotCliScopeInner(opts));
3517
+ }
3518
+ function uninstallCopilotCliScopeInner(opts) {
3519
+ let removedAny = releaseCopilotHooksFile(copilotCliHooksDir(opts), "copilot");
3520
+ const instructionsPath = copilotCliInstructionsPath(opts);
3521
+ if (stripCopilotInstructionsBlock(instructionsPath)) {
3522
+ removedAny = true;
3523
+ }
3524
+ removeCreatedIfEmpty(instructionsPath);
3525
+ if (opts.local !== true && uninstallCopilotMcpServer()) {
3526
+ removedAny = true;
3527
+ }
3528
+ removeCreatedIfEmpty(path7.dirname(instructionsPath));
3529
+ return removedAny;
3530
+ }
3531
+ function uninstallCopilotCli(opts = {}) {
3532
+ if (opts.local === true) {
3533
+ return uninstallCopilotCliScope({ local: true });
3534
+ }
3535
+ const userRemoved = uninstallCopilotCliScope({ local: false });
3536
+ const localRemoved = uninstallCopilotCliScope({ local: true });
3537
+ return userRemoved || localRemoved;
3538
+ }
3539
+ function isCopilotCliInstalled(opts = {}) {
3540
+ const scriptPresent = fs7.existsSync(copilotCliScriptPath(opts)) || fs7.existsSync(path7.join(copilotCliHooksDir(opts), LEGACY_HOOKS_SCRIPT_FILE));
3541
+ if (!fs7.existsSync(copilotCliConfigPath(opts)) || !scriptPresent) {
3542
+ return false;
3543
+ }
3544
+ if (!readCopilotHooksOwners(copilotCliHooksDir(opts)).has("copilot")) return false;
3545
+ let instructions;
3546
+ try {
3547
+ instructions = fs7.readFileSync(copilotCliInstructionsPath(opts), "utf8");
3548
+ } catch {
3549
+ return false;
3550
+ }
3551
+ return instructions.includes(COPILOT_INSTRUCTIONS_BEGIN) && instructions.includes(COPILOT_INSTRUCTIONS_END);
3552
+ }
3553
+
3554
+ export {
3555
+ SHIM_TRY_SERVER,
3556
+ SHIM_FILE,
3557
+ LEGACY_SHIM_FILE,
3558
+ legacyShimForwarder,
3559
+ SHIM_REQUIRES,
3560
+ SHIM_VALID_HOOK_EVENTS,
3561
+ SHIM_TRY_IN_PROCESS,
3562
+ SHIM_SPAWN_LADDER,
3563
+ shimFallbackTimeoutConst,
3564
+ SHIM_MAX_BUFFER_CONST,
3565
+ CLAUDECODE_HOOK_SCRIPT,
3566
+ buildGuidanceBody,
3567
+ skillDescriptionLine,
3568
+ buildGuidanceBlock,
3569
+ packagedNativeBinary,
3570
+ nativeCopyPath,
3571
+ nativeCopyCurrent,
3572
+ nativeSelftest,
3573
+ nativeHookBinary,
3574
+ nativeHookCommandLine,
3575
+ splitHookCommand,
3576
+ HookCommandRewriteError,
3577
+ hookCommandRewrittenSpan,
3578
+ parseNativeInvocation,
3579
+ anchoredMarkerPattern,
3580
+ claudeHookScriptPath,
3581
+ settingsPath,
3582
+ wiredClaudeHookWords,
3583
+ installHooks,
3584
+ uninstallHooks,
3585
+ finishHookUninstall,
3586
+ isInstalled,
3587
+ claudeHooksInstalledAnyScope,
3588
+ hookEventGaps,
3589
+ claudeMdPath,
3590
+ installedClaudeMdBlock,
3591
+ installClaudeMd,
3592
+ uninstallClaudeMd,
3593
+ findStrayClaudeMdBlocks,
3594
+ skillPath,
3595
+ installedSkillDescription,
3596
+ installSkill,
3597
+ uninstallSkill,
3598
+ CODEX_HOOK_SCRIPT,
3599
+ groupHasTokenGoat2 as groupHasTokenGoat,
3600
+ codexConfigPath,
3601
+ codexAgentsPath,
3602
+ codexHookScriptPath,
3603
+ installCodex,
3604
+ uninstallCodex,
3605
+ wiredCodexHookWords,
3606
+ isCodexInstalled,
3607
+ bundledCliPath,
3608
+ managedServer,
3609
+ isManagedServer,
3610
+ readServersJson,
3611
+ serversOf,
3612
+ editAt,
3613
+ setTokenGoatServer,
3614
+ isResidueServersJson,
3615
+ noteRootKeyCreation,
3616
+ dropEmptyServers,
3617
+ managedServerEntry,
3618
+ hasManagedServer,
3619
+ MATERIALIZE_SHRUNK_IMAGE_JS,
3620
+ COPILOT_CLI_HOOK_SCRIPT,
3621
+ copilotMcpConfigPath,
3622
+ visualStudioUserMcpPath,
3623
+ visualStudioProjectMcpPath,
3624
+ visualStudioOtherScopeHasManagedServer,
3625
+ isVisualStudioInstalled,
3626
+ visualStudioSolutionVscodeMcpPath,
3627
+ visualStudioDuplicateNote,
3628
+ visualStudioMcpStatus,
3629
+ visualStudioManagedEntry,
3630
+ syncVisualStudioProjectGuidance,
3631
+ installVisualStudio,
3632
+ uninstallVisualStudio,
3633
+ canonicalProjectRoot,
3634
+ copilotCliConfigPath,
3635
+ copilotCliScriptPath,
3636
+ copilotCliInstructionsPath,
3637
+ wiredCopilotHookWords,
3638
+ HOOKS_SCRIPT_FILE,
3639
+ LEGACY_HOOKS_SCRIPT_FILE,
3640
+ copilotHooksFilePaths,
3641
+ installCopilotHooksFile,
3642
+ releaseCopilotHooksFile,
3643
+ installCopilotCli,
3644
+ repairCopilotCliHooks,
3645
+ uninstallCopilotCli,
3646
+ isCopilotCliInstalled
3647
+ };