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
@@ -1,1429 +0,0 @@
1
- import { createRequire as __cjsRequire } from 'node:module';
2
- const require = __cjsRequire(import.meta.url);
3
- import {
4
- estimateTokensFromLength
5
- } from "./token-goat-chunk-52NSDWYY.mjs";
6
- import {
7
- loadConfig
8
- } from "./token-goat-chunk-TGOXNAHW.mjs";
9
- import {
10
- atomicWriteText,
11
- backupFile,
12
- ensureDirSync,
13
- hookCommandFor,
14
- hookPowershellCommand,
15
- projectScopeRoot,
16
- recordCreatedConfig,
17
- removeCreatedBackups,
18
- removeFileInScope,
19
- stripDelimitedBlock,
20
- takeCreatedConfig,
21
- upsertDelimitedBlock,
22
- withInstallScope,
23
- writeIfDifferent
24
- } from "./token-goat-chunk-XYWPOSCN.mjs";
25
- import {
26
- init_define_import_meta_env
27
- } from "./token-goat-chunk-A37V4PBF.mjs";
28
-
29
- // src/bridges/visualstudio_install.ts
30
- init_define_import_meta_env();
31
- import * as fs2 from "node:fs";
32
- import * as os from "node:os";
33
- import * as path2 from "node:path";
34
-
35
- // src/bridges/guidance_block.ts
36
- init_define_import_meta_env();
37
- function buildGuidanceBody(fallbackToolClause, opts = {}) {
38
- const gdrive = opts.gdrive !== false;
39
- return [
40
- "## token-goat",
41
- "",
42
- "**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.",
43
- "",
44
- `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.`,
45
- "",
46
- "Fallback clauses may name your harness's own native read, search, and edit tools, or its shell helpers. Shell binaries and editor programs are commands invoked through the shell tool, never tool identifiers, and must never appear in an agent's tools frontmatter or an allowed-tools list. This paragraph deliberately names no specific tool or binary: instruction-file loaders harvest such names into a tool allowlist and then warn that every one of them is unknown.",
47
- "",
48
- "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).",
49
- "",
50
- "Failure shapes to catch yourself in, and the command that replaces each:",
51
- '- not knowing which command answers a question you can already state in words \u2192 `answer "<question>"`, which resolves the subject in the index, runs the right command, prints `via: <command>`, and refuses rather than guess',
52
- '- a shell text search with context flags to find a function body \u2192 `read "file::symbol"`',
53
- '- paging one function with view/view_range \u2192 `read "file::symbol"`',
54
- "- a read refused by a `[tg]` deny, retried with a shifted range or a different read tool \u2192 the command the deny names",
55
- '- reading a symbol plus chasing its callers and containing doc section as separate reads \u2192 `brief "file::symbol"`',
56
- '- reading one heading of a large doc \u2192 `section "file::Heading"`',
57
- "- searching for a symbol's callers \u2192 `refs file::symbol --callers`",
58
- '- searching for a *concept* rather than a literal string \u2192 `semantic "description"`',
59
- "- re-reading output you already captured \u2192 `bash-output`/`web-output`/`mcp-output` by ID",
60
- "- a directory listing or recursive wildcard walk to orient in an unfamiliar repo \u2192 `map --compact`",
61
- "- 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'`",
62
- "- opening an image to check its dimensions, format, or size \u2192 `image-meta file`",
63
- "- opening a screenshot, diagram, or scan to read the text in it \u2192 `image-text file`",
64
- "- opening a PDF or Office document \u2192 inspect its format first, then read a narrow slice: PDF `pdf-meta`/`pdf-outline` then `pdf-locate` to find the pages and `pdf-extract` only those; Word `docx-outline` then `docx-tables`/`docx-text`; PowerPoint `pptx-outline` then `pptx-slide`/`pptx-notes`; Excel `xlsx-sheets` then `xlsx-columns`/`xlsx-head`/`xlsx-range`/`xlsx-query`",
65
- "",
66
- 'Commands: `answer "<question>"`, `symbol NAME`, `read "file::symbol"`, `brief "file::symbol"`, `section "file::Heading"`, `semantic "description"`, `outline file`/`skeleton file`, `map --compact`, `refs file::symbol --callers`, `changed --symbol`, `config-get file KEY`, `json-query file \'a.b.c\'`/`yaml-query`/`xml-query`, `json-outline file`/`yaml-outline`/`xml-outline`, `bash-output`/`web-output`/`mcp-output`, ' + (gdrive ? "`gdrive-sections <file-id>`, " : "") + "`image-meta file`/`image-text file`, `pdf-meta`/`pdf-outline`/`pdf-locate`/`pdf-extract`, `docx-outline`/`docx-tables`/`docx-text`, `pptx-outline`/`pptx-slide`/`pptx-notes`/`pptx-text`, `xlsx-sheets`/`xlsx-columns`/`xlsx-head`/`xlsx-range`/`xlsx-query`.",
67
- "",
68
- "Sub-agent briefs must carry this gate verbatim: a sub-agent inherits none of this context and its reads spend the same token budget.",
69
- "",
70
- "`token-goat stats` \u2014 self-check. Flat counts during code work mean the gate is being skipped."
71
- ].join("\n");
72
- }
73
- function skillDescriptionLine(gdrive) {
74
- 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.`;
75
- }
76
- function buildGuidanceBlock(h) {
77
- return [h.beginMarker, buildGuidanceBody(h.fallbackToolClause, h.gdrive === void 0 ? {} : { gdrive: h.gdrive }), h.endMarker].join("\n");
78
- }
79
-
80
- // src/bridges/mcp_servers_json.ts
81
- init_define_import_meta_env();
82
- import * as fs from "node:fs";
83
- import * as path from "node:path";
84
- import { createRequire } from "node:module";
85
- import { fileURLToPath } from "node:url";
86
- var jsoncParser;
87
- function jsonc() {
88
- jsoncParser ??= createRequire(import.meta.url)("jsonc-parser");
89
- return jsoncParser;
90
- }
91
- function bundledCliPath() {
92
- const here = path.dirname(fileURLToPath(import.meta.url));
93
- const bundled = path.join(here, "token-goat.mjs");
94
- if (fs.existsSync(bundled)) return bundled;
95
- return path.resolve(here, "..", "..", "dist", "token-goat.mjs");
96
- }
97
- function managedServer() {
98
- return {
99
- type: "stdio",
100
- command: process.execPath,
101
- args: [bundledCliPath(), "mcp-serve"]
102
- };
103
- }
104
- function isManagedServer(value) {
105
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
106
- const entry = value;
107
- const args = entry["args"];
108
- return entry["type"] === "stdio" && Array.isArray(args) && args.length === 2 && typeof args[0] === "string" && path.basename(args[0]).toLowerCase() === "token-goat.mjs" && args[1] === "mcp-serve";
109
- }
110
- function readServersJson(filePath, label) {
111
- const text = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "{}\n";
112
- const errors = [];
113
- const parsed = jsonc().parse(text, errors, { allowTrailingComma: true, disallowComments: false });
114
- if (errors.length > 0) {
115
- throw new Error(`malformed ${label} MCP JSON at ${filePath}`);
116
- }
117
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
118
- throw new Error(`malformed ${label} MCP JSON at ${filePath}: expected a JSON object`);
119
- }
120
- return { text, value: parsed };
121
- }
122
- function serversOf(config, filePath, label, rootKey = "servers") {
123
- const servers = config.value[rootKey];
124
- if (servers === void 0) return {};
125
- if (servers === null || typeof servers !== "object" || Array.isArray(servers)) {
126
- throw new Error(`malformed ${label} MCP JSON at ${filePath}: ${rootKey} must be an object`);
127
- }
128
- return servers;
129
- }
130
- var FORMAT = { insertSpaces: true, tabSize: 2, eol: "\n" };
131
- function editAt(text, jsonPath, value) {
132
- let out = text;
133
- for (const e of [...jsonc().modify(text, jsonPath, value, {})].sort((a, b) => b.offset - a.offset)) {
134
- out = out.slice(0, e.offset) + e.content + out.slice(e.offset + e.length);
135
- if (e.content.length > 0) out = jsonc().applyEdits(out, jsonc().format(out, { offset: e.offset, length: e.content.length }, FORMAT));
136
- }
137
- return out;
138
- }
139
- function setTokenGoatServer(text, value, rootKey = "servers") {
140
- if (value !== void 0 && /^\s*\{\s*\}\s*$/.test(text)) return `${JSON.stringify({ [rootKey]: { "token-goat": value } }, null, 2)}
141
- `;
142
- return editAt(text, [rootKey, "token-goat"], value);
143
- }
144
- function holdsOnlyManagedServer(value) {
145
- const keys = Object.keys(value);
146
- if (keys.length !== 1) return false;
147
- const root = keys[0];
148
- if (root !== "servers" && root !== "mcpServers") return false;
149
- const servers = value[root];
150
- if (servers === null || typeof servers !== "object" || Array.isArray(servers)) return false;
151
- const names = Object.keys(servers);
152
- return names.length === 1 && names[0] === "token-goat" && isManagedServer(servers["token-goat"]);
153
- }
154
- function isResidueServersJson(text, rootKey = "servers") {
155
- const trimmed = text.trim();
156
- if (trimmed === "") return true;
157
- const parsed = parseObject(trimmed);
158
- if (parsed === null) return false;
159
- const keys = Object.keys(parsed);
160
- if (keys.length !== 1 || keys[0] !== rootKey) return false;
161
- const servers = parsed[rootKey];
162
- return servers !== null && typeof servers === "object" && !Array.isArray(servers) && Object.keys(servers).length === 0;
163
- }
164
- function dropEmptyServers(text, rootKey = "servers") {
165
- const parsed = parseObject(text);
166
- if (parsed === null) return text;
167
- const servers = parsed[rootKey];
168
- if (servers === null || typeof servers !== "object" || Array.isArray(servers) || Object.keys(servers).length > 0) return text;
169
- return editAt(text, [rootKey], void 0);
170
- }
171
- function parseObject(text) {
172
- const parsed = jsonc().parse(text, [], { allowTrailingComma: true, disallowComments: false });
173
- return parsed === null || typeof parsed !== "object" || Array.isArray(parsed) ? null : parsed;
174
- }
175
- function ensureMcpServersKey(text) {
176
- const parsed = parseObject(text);
177
- if (parsed === null || "mcpServers" in parsed) return text;
178
- return editAt(text, ["mcpServers"], {});
179
- }
180
- function dropLoneEmptyMcpServers(text) {
181
- const parsed = parseObject(text);
182
- if (parsed === null || Object.keys(parsed).length !== 1) return text;
183
- const mcpServers = parsed["mcpServers"];
184
- if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers) || Object.keys(mcpServers).length > 0) return text;
185
- return editAt(text, ["mcpServers"], void 0);
186
- }
187
- function hasManagedServer(filePath, label, rootKey = "servers", isManaged = isManagedServer) {
188
- if (!fs.existsSync(filePath)) return false;
189
- try {
190
- const config = readServersJson(filePath, label);
191
- return isManaged(serversOf(config, filePath, label, rootKey)["token-goat"]);
192
- } catch {
193
- return false;
194
- }
195
- }
196
-
197
- // src/bridges/visualstudio_install.ts
198
- var LABEL = "Visual Studio";
199
- var VISUALSTUDIO_GUIDANCE_BEGIN = "<!-- token-goat-visualstudio-begin -->";
200
- var VISUALSTUDIO_GUIDANCE_END = "<!-- token-goat-visualstudio-end -->";
201
- var SIBLING_GATE_MARKERS = [
202
- ["<!-- token-goat-vscode-begin -->", "<!-- token-goat-vscode-end -->"],
203
- ["<!-- token-goat-begin -->", "<!-- token-goat-end -->"]
204
- ];
205
- function visualStudioUserMcpPath() {
206
- return path2.join(os.homedir(), ".mcp.json");
207
- }
208
- function visualStudioProjectMcpPath(projectRoot = process.cwd()) {
209
- return path2.join(path2.resolve(projectRoot), ".mcp.json");
210
- }
211
- function visualStudioMcpPath(opts = {}) {
212
- return opts.project === true ? visualStudioProjectMcpPath(opts.projectRoot) : visualStudioUserMcpPath();
213
- }
214
- function otherScopeMcpPath(opts) {
215
- return opts.project === true ? visualStudioUserMcpPath() : visualStudioProjectMcpPath(opts.projectRoot);
216
- }
217
- function visualStudioInstructionsPath(opts = {}) {
218
- return opts.project === true ? path2.join(path2.resolve(opts.projectRoot ?? process.cwd()), ".github", "copilot-instructions.md") : path2.join(os.homedir(), "copilot-instructions.md");
219
- }
220
- function visualStudioOtherScopeHasManagedServer(opts = {}) {
221
- return hasManagedServer(otherScopeMcpPath(opts), LABEL);
222
- }
223
- function isVisualStudioInstalled(opts = {}) {
224
- return hasManagedServer(visualStudioMcpPath(opts), LABEL);
225
- }
226
- function visualStudioSolutionVscodeMcpPath(projectRoot = process.cwd()) {
227
- return path2.join(path2.resolve(projectRoot), ".vscode", "mcp.json");
228
- }
229
- function visualStudioManagedRegistrations(projectRoot = process.cwd()) {
230
- return [visualStudioUserMcpPath(), visualStudioProjectMcpPath(projectRoot), visualStudioSolutionVscodeMcpPath(projectRoot)].filter((p) => hasManagedServer(p, LABEL));
231
- }
232
- function visualStudioDuplicateNote(projectRoot = process.cwd()) {
233
- const found = visualStudioManagedRegistrations(projectRoot);
234
- if (found.length < 2) return null;
235
- 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.`;
236
- if (!found.includes(visualStudioSolutionVscodeMcpPath(projectRoot))) return `${lead} Keep one: run "token-goat uninstall --visualstudio" or "token-goat uninstall --visualstudio -p".`;
237
- return `${lead} Keep one: if you do not use VS Code in this folder, run "token-goat uninstall --vscode -p"; otherwise run "token-goat uninstall --visualstudio${found.includes(visualStudioUserMcpPath()) ? "" : " -p"}".`;
238
- }
239
- function visualStudioMcpStatus(opts = {}) {
240
- const checkedPaths = [visualStudioUserMcpPath()];
241
- if (opts.projectRoot !== void 0) checkedPaths.push(visualStudioProjectMcpPath(opts.projectRoot));
242
- return { configured: checkedPaths.some((p) => hasManagedServer(p, LABEL)), checkedPaths };
243
- }
244
- function visualStudioManagedEntry(filePath) {
245
- if (!fs2.existsSync(filePath)) return null;
246
- try {
247
- const entry = serversOf(readServersJson(filePath, LABEL), filePath, LABEL)["token-goat"];
248
- if (!isManagedServer(entry)) return null;
249
- const { command, args } = entry;
250
- return { command: typeof command === "string" ? command : "", bundlePath: args[0] ?? "" };
251
- } catch {
252
- return null;
253
- }
254
- }
255
- var FALLBACK_TOOL_CLAUSE = "Visual Studio\u2019s built-in readfile, code_search, and find_references tools";
256
- 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.";
257
- 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.";
258
- function buildVisualStudioGuidanceBlock(sharesFileWithGate) {
259
- 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 })];
260
- return [VISUALSTUDIO_GUIDANCE_BEGIN, ...lead, "", RETRIEVE_NOTE, "", SUPPORT_NOTE, VISUALSTUDIO_GUIDANCE_END].join("\n");
261
- }
262
- function hasBlock(text, begin, end) {
263
- const b = text.indexOf(begin);
264
- const e = text.indexOf(end);
265
- return b !== -1 && e > b;
266
- }
267
- function readText(filePath) {
268
- try {
269
- return fs2.readFileSync(filePath, "utf8");
270
- } catch {
271
- return "";
272
- }
273
- }
274
- function writeGuidance(filePath) {
275
- const text = readText(filePath);
276
- const shares = SIBLING_GATE_MARKERS.some(([begin, end]) => hasBlock(text, begin, end));
277
- return upsertDelimitedBlock(filePath, VISUALSTUDIO_GUIDANCE_BEGIN, VISUALSTUDIO_GUIDANCE_END, buildVisualStudioGuidanceBlock(shares));
278
- }
279
- function syncVisualStudioProjectGuidance(filePath) {
280
- if (!hasBlock(readText(filePath), VISUALSTUDIO_GUIDANCE_BEGIN, VISUALSTUDIO_GUIDANCE_END)) return false;
281
- return writeGuidance(filePath);
282
- }
283
- function installVisualStudio(opts = {}) {
284
- return withInstallScope(projectScopeRoot(opts), () => installVisualStudioScoped(opts));
285
- }
286
- function installVisualStudioScoped(opts) {
287
- const scope = opts.project === true ? "project" : "user";
288
- const mcpPath = visualStudioMcpPath(opts);
289
- const instructionsPath = visualStudioInstructionsPath(opts);
290
- if (visualStudioOtherScopeHasManagedServer(opts)) {
291
- const otherScope = scope === "project" ? "user" : "project";
292
- throw new Error(
293
- `token-goat is already registered in Visual Studio ${otherScope} scope (${otherScopeMcpPath(opts)}). Visual Studio reads both files, so installing into ${scope} scope too would register it twice. Run "token-goat uninstall --visualstudio${otherScope === "project" ? " --project" : ""}" first if you want to move it, or drop --visualstudio from this run.`
294
- );
295
- }
296
- const mcpExisted = fs2.existsSync(mcpPath);
297
- const config = readServersJson(mcpPath, LABEL);
298
- const ownedAlready = mcpExisted && holdsOnlyManagedServer(config.value);
299
- const current = serversOf(config, mcpPath, LABEL)["token-goat"];
300
- if (current !== void 0 && !isManagedServer(current)) {
301
- throw new Error(`Visual Studio MCP JSON at ${mcpPath} already has a non-token-goat-managed server named "token-goat"`);
302
- }
303
- const next = ensureMcpServersKey(setTokenGoatServer(config.text, managedServer()));
304
- if (config.text !== next) {
305
- ensureDirSync(path2.dirname(mcpPath));
306
- backupFile(mcpPath);
307
- atomicWriteText(mcpPath, next);
308
- if (!mcpExisted || ownedAlready) recordCreatedConfig(mcpPath);
309
- }
310
- const guidanceChanged = writeGuidance(instructionsPath);
311
- return { mcpPath, instructionsPath, alreadyInstalled: config.text === next && !guidanceChanged, scope };
312
- }
313
- function uninstallVisualStudio(opts = {}) {
314
- return withInstallScope(projectScopeRoot(opts), () => uninstallVisualStudioScoped(opts));
315
- }
316
- function uninstallVisualStudioScoped(opts) {
317
- const mcpPath = visualStudioMcpPath(opts);
318
- let removed = false;
319
- if (fs2.existsSync(mcpPath)) {
320
- const config = readServersJson(mcpPath, LABEL);
321
- if (isManagedServer(serversOf(config, mcpPath, LABEL)["token-goat"])) {
322
- const next = dropEmptyServers(setTokenGoatServer(config.text, void 0));
323
- if (/^\s*\{\s*\}\s*$/.test(dropLoneEmptyMcpServers(next)) && takeCreatedConfig(mcpPath)) removeFileInScope(mcpPath);
324
- else {
325
- backupFile(mcpPath);
326
- atomicWriteText(mcpPath, ensureMcpServersKey(next));
327
- }
328
- removeCreatedBackups(mcpPath);
329
- removed = true;
330
- }
331
- }
332
- const instructionsPath = visualStudioInstructionsPath(opts);
333
- if (stripDelimitedBlock(instructionsPath, VISUALSTUDIO_GUIDANCE_BEGIN, VISUALSTUDIO_GUIDANCE_END)) {
334
- removed = true;
335
- if (readText(instructionsPath).trim() === "") removeFileInScope(instructionsPath);
336
- }
337
- return removed;
338
- }
339
-
340
- // src/bridges/copilot_cli_install.ts
341
- init_define_import_meta_env();
342
- import * as fs3 from "node:fs";
343
- import * as os2 from "node:os";
344
- import * as path3 from "node:path";
345
-
346
- // src/bridges/copilot_cli.ts
347
- init_define_import_meta_env();
348
-
349
- // src/copilot_tool_names.ts
350
- init_define_import_meta_env();
351
- var COPILOT_CLI_TOOL_NAME_MAP = {
352
- bash: "Bash",
353
- powershell: "Bash",
354
- read_bash: "BashOutput",
355
- read_powershell: "BashOutput",
356
- view: "Read",
357
- create: "Write",
358
- edit: "Edit",
359
- web_fetch: "WebFetch",
360
- web_search: "WebSearch",
361
- grep: "Grep",
362
- glob: "Glob",
363
- skill: "Skill",
364
- exit_plan_mode: "ExitPlanMode"
365
- };
366
-
367
- // src/tool_name_fold.ts
368
- init_define_import_meta_env();
369
- function foldToolName(name) {
370
- return typeof name === "string" ? name.toLowerCase().replace(/[_-]/g, "") : "";
371
- }
372
-
373
- // src/bridges/shim_try_server.ts
374
- init_define_import_meta_env();
375
- var SHIM_TRY_SERVER = `// Attempts the resident hook server (src/hook_server.ts): the sibling hook client sends this
376
- // call to an already-running token-goat process, so neither Node's module loading nor the hook
377
- // graph is paid for again. The CommonJS client comes first: this shim is CommonJS, and loading
378
- // the ES module one starts Node's ESM loader, about 5ms of a call the server answers. The ES
379
- // module one is for an install built before the CommonJS file existed. It returns undefined
380
- // whenever nothing was dispatched -- no server yet (it starts one in the background), all busy,
381
- // replaced by a newer build, turned off, or an install predating the client -- and the caller
382
- // then runs the call itself.
383
- async function tryServer(entryPath, eventName, input, harnessWaitMs) {
384
- if (!entryPath) return undefined
385
- try {
386
- const fs = require('node:fs')
387
- const cjsPath = path.join(path.dirname(entryPath), 'token-goat-hook-client.cjs')
388
- const mjsPath = path.join(path.dirname(entryPath), 'token-goat-hook-client.mjs')
389
- const client = fs.existsSync(cjsPath) ? require(cjsPath) : fs.existsSync(mjsPath) ? await import(pathToFileURL(mjsPath).href) : undefined
390
- if (client === undefined) return undefined
391
- return await client.relayViaServer(eventName, input, harnessWaitMs)
392
- } catch {
393
- return undefined
394
- }
395
- }`;
396
-
397
- // src/bridges/shrink_block.ts
398
- init_define_import_meta_env();
399
- 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.
400
- const MATERIALIZED_SHRINK_MAX_AGE_MS = 60 * 60 * 1000
401
- let lastMaterializedShrinkSweepAtMs = 0
402
- function pruneMaterializedShrinks() {
403
- const now = Date.now()
404
- if (now - lastMaterializedShrinkSweepAtMs < MATERIALIZED_SHRINK_MAX_AGE_MS) return
405
- lastMaterializedShrinkSweepAtMs = now
406
- try {
407
- const dir = os.tmpdir()
408
- for (const file of fs.readdirSync(dir)) {
409
- if (!file.startsWith("token-goat-shrink-")) continue
410
- const full = path.join(dir, file)
411
- try {
412
- const st = fs.statSync(full)
413
- if (st.isFile() && now - st.mtimeMs > MATERIALIZED_SHRINK_MAX_AGE_MS) fs.unlinkSync(full)
414
- } catch {
415
- // Best-effort per-file cleanup; one bad stat/unlink must not abort the sweep.
416
- }
417
- }
418
- } catch {
419
- // Best-effort; a readdir failure must never break the materialization below.
420
- }
421
- }
422
-
423
- // 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.
424
- function materializeShrunkImage(context) {
425
- if (typeof context !== "string") return undefined
426
- const idx = context.indexOf("data:image/")
427
- if (idx === -1) return undefined
428
- const match = /^data:image\\/([a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)$/.exec(context.slice(idx).trim())
429
- if (!match) return undefined
430
- try {
431
- pruneMaterializedShrinks()
432
- const buf = Buffer.from(match[2], "base64")
433
- const name = \`token-goat-shrink-\${process.pid}-\${Date.now()}-\${globalThis.crypto.randomUUID()}.\${match[1]}\`
434
- const file = path.join(os.tmpdir(), name)
435
- // Owner-only: the copy is of a workspace image and sits in the shared temp dir.
436
- fs.writeFileSync(file, buf, { mode: 0o600 })
437
- return file
438
- } catch {
439
- return undefined
440
- }
441
- }`;
442
-
443
- // src/bridges/copilot_cli.ts
444
- var COPILOT_CLI_HOOK_SCRIPT = `#!/usr/bin/env node
445
- // token-goat Copilot CLI hook shim. Translates Copilot's hook event names and
446
- // request/response schema to/from token-goat's internal hook protocol.
447
- 'use strict'
448
- const { spawnSync } = require('node:child_process')
449
- const fs = require('node:fs')
450
- const os = require('node:os')
451
- const path = require('node:path')
452
- const { pathToFileURL } = require('node:url')
453
-
454
- // Copilot event name -> token-goat internal HookEventName (src/types.ts's
455
- // HOOK_EVENTS). Only these eight have a token-goat handler; every other real
456
- // Copilot event (sessionEnd, subagentStart, errorOccurred, notification,
457
- // permissionRequest) is left unimplemented
458
- // rather than guessed at, and falls through to the default no-op below.
459
- // postToolUseFailure is the newest of the eight and the only one whose channel
460
- // costs tokens instead of saving them: it fires instead of postToolUse when a
461
- // tool result is a failure, and accepts only additionalContext back --
462
- // modifiedResult and suppressOutput are not honored there, so a failed result
463
- // still cannot be fenced, compressed or shrunk. It is wired anyway because
464
- // additionalContext demonstrably reaches the model (see the postToolUseFailure
465
- // branch in translate() for the bundle offset), and hooks_tool_failure.ts
466
- // spends that channel only on an exact repeat failure, where staying silent
467
- // costs a whole wasted retry.
468
- // 'sessionStart' was previously a permanent no-op on the stated grounds that
469
- // token-goat has no internal session_start handler. That was simply wrong --
470
- // hooks_session_start.ts has long emitted the command-routing reminder that
471
- // every other harness receives -- and the no-op was the reason Copilot CLI
472
- // sessions alone never got told token-goat exists. It is wired now: verified
473
- // against Copilot CLI 1.0.77 that a hooks.json sessionStart entry returning
474
- // {additionalContext} does reach the model. The github/copilot-cli#2142
475
- // fire-and-forget bug that would have made this dead wiring was fixed in a
476
- // pre-release months before that version, and its companion multi-extension
477
- // hook-overwrite bug never applied here: it hit runtime *extension* hooks,
478
- // while this config-file hooks.json path goes through Copilot's own merge.
479
- const COPILOT_TO_TG_EVENT = {
480
- sessionStart: 'session_start',
481
- preToolUse: 'pre_tool_use',
482
- postToolUse: 'post_tool_use',
483
- preCompact: 'pre_compact',
484
- agentStop: 'stop',
485
- subagentStop: 'subagent_stop',
486
- userPromptSubmitted: 'user_prompt_submit',
487
- postToolUseFailure: 'post_tool_use_failure',
488
- }
489
-
490
- // Copilot built-in tool name -> token-goat internal tool name. Confirmed via
491
- // @github/copilot-sdk type definitions and multiple real GitHub issue payload
492
- // dumps -- supersedes an earlier docs-based guess (shell/read/write/url) that
493
- // didn't hold up in practice; 'write' in particular was never a real
494
- // toolName value, only a Copilot permission-pattern keyword. 'powershell'
495
- // maps to the same 'Bash' handler as 'bash' since both are shell-command
496
- // execution from token-goat's perspective (mirrors how hooks_bash.ts's own
497
- // filters already treat powershell-wrapped commands as part of the Bash
498
- // pipeline, not a separate tool). 'task', 'ask_user', 'memory', and
499
- // MCP-server tool invocations (<server-name>-<tool-name>) have no
500
- // token-goat equivalent and are passed through unmapped (safe no-op for
501
- // handlers that don't recognize the name).
502
- // 'read_bash'/'read_powershell' are the background-shell output pollers, and
503
- // they are the exact shape of Claude Code's BashOutput: Copilot's shell tool
504
- // is async, so a long-running command is started once and then re-read over
505
- // and over within one turn, each read returning the accumulated output again.
506
- // hooks_bashoutput.ts already collapses that into a delta (or a short
507
- // "unchanged" marker) for Claude Code, and now does the same here. Both the
508
- // names and the argument key were read out of the shipping 1.0.80 bundle
509
- // rather than guessed: the builtin tool-name table in
510
- // prebuilds/win32-x64/runtime.node lists read_bash/stop_bash/list_bash and
511
- // read_powershell/stop_powershell/list_powershell, and the poller's own input
512
- // schema in app.js is {shellId, delay} -- so the shell id needs the
513
- // POLL_ID_ARG_KEY remap below to reach postBashOutputHandler, which reads
514
- // 'bash_id'. read_shell/stop_shell/list_shells, which an earlier static sweep
515
- // of the same binary suggested were the real names, are internal Rust
516
- // identifiers (tool_read_shell_prepare_input, tool_list_shells_descriptor,
517
- // PreparedStopShellInput, and the serde field names of the shell config
518
- // struct), never wire tool names, so they are deliberately absent here.
519
- // stop_bash/list_bash and their powershell twins stay unmapped because
520
- // token-goat has no KillShell-equivalent handler for them to reach; a mapping
521
- // would be pure decoration.
522
- // 'bash'/'powershell' stay exactly as they were. The same sweep suggested the
523
- // executor had been renamed to write_bash/write_powershell and that this
524
- // mapping was dead, and that is not what the bundle says: app.js resolves the
525
- // executor as shellConfig?.shellToolName ?? "bash" (with "powershell" as the
526
- // Windows default of the same config), and its command lives under 'command'
527
- // exactly as hooks_bash.ts expects. write_bash/write_powershell are real tool
528
- // names, but they are a different tool -- their schema is {shellId, input,
529
- // delay} and the bundle files them under the subtype "write_shell", i.e. send
530
- // stdin to an already-running shell, not run a command. Mapping them to Bash
531
- // would label a stdin write as a shell execution, so they are left unmapped.
532
- // 'task', 'read_agent' and 'memory'-family tools are likewise left alone:
533
- // task's result is assembled in the native addon and its shape is unknown,
534
- // read_agent is an incremental poll that postAgentHandler is not written for,
535
- // and the real tool names behind memory were never confirmed. Each would put
536
- // a handler that rewrites model-visible output in front of a payload shape
537
- // nobody has seen, which is worse than leaving the compression on the table.
538
- // Own properties only, for every map in this shim -- see the event map in main() for why a
539
- // bare lookup on a plain object literal is not a gate. A tool named 'constructor' resolved to
540
- // a Function, which JSON.stringify drops, so the forwarded payload carried no tool_name and
541
- // every hook keyed on one no-opped; '__proto__' forwarded a whole prototype instead. The key
542
- // is third-party text: Copilot namespaces MCP tools as server:tool and the stripped half is
543
- // whatever that server called its tool.
544
- function ownGet(map, key) {
545
- return typeof key === 'string' && Object.prototype.hasOwnProperty.call(map, key) ? map[key] : undefined
546
- }
547
-
548
- const TOOL_TO_TG = {
549
- bash: 'Bash',
550
- powershell: 'Bash',
551
- read_bash: 'BashOutput',
552
- read_powershell: 'BashOutput',
553
- view: 'Read',
554
- create: 'Write',
555
- edit: 'Edit',
556
- web_fetch: 'WebFetch',
557
- web_search: 'WebSearch',
558
- grep: 'Grep',
559
- glob: 'Glob',
560
- skill: 'Skill',
561
- exit_plan_mode: 'ExitPlanMode',
562
- }
563
-
564
- function foldToolName(name) {
565
- return typeof name === 'string' ? name.toLowerCase().replace(/[_-]/g, '') : ''
566
- }
567
-
568
- const FOLDED_TOOL_TO_TG = {}
569
- for (const [k, v] of Object.entries(TOOL_TO_TG)) {
570
- FOLDED_TOOL_TO_TG[foldToolName(k)] = v
571
- }
572
- // Also register folded entries for canonical TG tool names directly (e.g. WebSearch -> websearch)
573
- for (const v of Object.values(TOOL_TO_TG)) {
574
- FOLDED_TOOL_TO_TG[foldToolName(v)] = v
575
- }
576
-
577
- function resolveCanonicalToolName(name) {
578
- if (!name || typeof name !== 'string') return name
579
- const direct = ownGet(TOOL_TO_TG, name)
580
- if (direct !== undefined) return direct
581
- const stripped = name.indexOf(':') !== -1 ? name.split(':').pop() : name
582
- const strippedDirect = ownGet(TOOL_TO_TG, stripped)
583
- if (strippedDirect !== undefined) return strippedDirect
584
- const folded = foldToolName(name)
585
- const foldedStripped = foldToolName(stripped)
586
- return ownGet(FOLDED_TOOL_TO_TG, folded) || ownGet(FOLDED_TOOL_TO_TG, foldedStripped) || name
587
- }
588
-
589
- // Confirmed via github/copilot-cli#3349 (open, unresolved as of writing): some
590
- // real Copilot CLI invocations send toolArgs as a JSON-*encoded string*
591
- // rather than a parsed object, contradicting the documented schema. Left
592
- // unhandled, canonical.tool_input would become a raw string and every
593
- // downstream event.toolInput[key] lookup in token-goat's handlers would
594
- // silently return undefined -- the deny/dedup mechanism would no-op with no
595
- // error surfaced. Parse it defensively; on a non-string, absent, or
596
- // unparsable value, fall back to {} rather than throwing (this shim's
597
- // convention throughout is fail-open, never crash on a malformed payload).
598
- function parseMaybeJsonObject(value) {
599
- if (value && typeof value === 'object') return value
600
- if (typeof value === 'string') {
601
- try {
602
- const parsed = JSON.parse(value)
603
- if (parsed && typeof parsed === 'object') return parsed
604
- } catch {
605
- // fall through to {}
606
- }
607
- }
608
- return {}
609
- }
610
-
611
- // view/edit/create send the file path under 'path'; every token-goat handler these
612
- // three tools reach only ever looks for 'file_path' (getFilePath in hooks_common.ts).
613
- // Keyed by the ORIGINAL Copilot tool name (before TOOL_TO_TG renames it) since that's
614
- // the name toolArgs' shape is keyed to, not token-goat's internal tool name.
615
- const FILE_PATH_ARG_KEY = {
616
- view: 'path',
617
- edit: 'path',
618
- create: 'path',
619
- }
620
-
621
- // read_bash/read_powershell send the background shell's id under 'shellId' (confirmed
622
- // against the poller's input schema in the shipping 1.0.80 app.js: {shellId, delay});
623
- // postBashOutputHandler reads 'bash_id'. Without this the tool-name mapping above would
624
- // be inert -- the handler bails on a missing bash_id and every poll would keep costing
625
- // the full accumulated output. Same keying convention as FILE_PATH_ARG_KEY: the ORIGINAL
626
- // Copilot tool name, since that is what the argument shape belongs to.
627
- const POLL_ID_ARG_KEY = {
628
- read_bash: 'shellId',
629
- read_powershell: 'shellId',
630
- }
631
-
632
- // Copilot spawns a brand-new process for every single hook invocation (no long-lived plugin
633
- // process the way OpenClaw's is -- OPENCLAW_HOOK_SCRIPT's own \`copilot-\${process.pid}-\${Date.now()}\`
634
- // fallback is safe there specifically because that process lives for the whole session, so the
635
- // pid stays constant across calls). If Copilot ever omits \`sessionId\` from a payload, falling
636
- // back to \`process.pid\` here would mint a DIFFERENT id on every single call for what's really
637
- // the same session, since process.pid varies per invocation -- breaking token-goat's
638
- // session-based dedup/state ledger, which never accumulates across calls as a result. Derive a
639
- // stable id instead from the one thing that's actually constant across calls for the same
640
- // session: the working directory Copilot reports. That field is \`workingDirectory\`, declared
641
- // required on BaseHookInput in copilot-sdk/types.d.ts since 1.0.76, so it is present on EVERY
642
- // hook event. This previously read \`payload.cwd\`, which Copilot has never sent under any name in
643
- // any version -- the key simply did not exist, so this derived every fallback id from
644
- // process.cwd() instead and \`canonical.cwd\` below was undefined on every single call. It went
645
- // unnoticed because process.cwd() happens to be the project directory Copilot spawns the hook in,
646
- // so the fallback was accidentally right; nothing about that was by design. \`cwd\` is still read
647
- // as a secondary in case a future version adds it under the shorter name.
648
- function stableFallbackSessionId(cwd) {
649
- const key = typeof cwd === 'string' && cwd ? cwd : process.cwd()
650
- const hash = require('node:crypto').createHash('sha256').update(key).digest('hex').slice(0, 16)
651
- return 'copilot-' + hash
652
- }
653
-
654
- function remapToolInput(copilotToolName, input) {
655
- if (!input || typeof input !== 'object') return input
656
- let out = input
657
- // Add the canonical key alongside the original rather than renaming it, so nothing that
658
- // might read the original 'path'/'shellId' key elsewhere (e.g. a future handler) loses it.
659
- const stripped = typeof copilotToolName === 'string' && copilotToolName.indexOf(':') !== -1
660
- ? copilotToolName.split(':').pop()
661
- : copilotToolName
662
- const folded = foldToolName(stripped)
663
- const pathKey = ownGet(FILE_PATH_ARG_KEY, copilotToolName) || ownGet(FILE_PATH_ARG_KEY, stripped) || (folded === 'view' || folded === 'edit' || folded === 'create' ? 'path' : undefined)
664
- if (pathKey !== undefined && pathKey in out) {
665
- out = Object.assign({}, out, { file_path: out[pathKey] })
666
- }
667
- const idKey = ownGet(POLL_ID_ARG_KEY, copilotToolName) || ownGet(POLL_ID_ARG_KEY, stripped) || (folded === 'readbash' || folded === 'readpowershell' ? 'shellId' : undefined)
668
- if (idKey !== undefined && idKey in out) {
669
- out = Object.assign({}, out, { bash_id: out[idKey] })
670
- }
671
- if (Array.isArray(out.view_range) && out.view_range.length >= 1) {
672
- const rawStart = Number(out.view_range[0])
673
- if (Number.isFinite(rawStart) && rawStart >= 1) {
674
- const mapped = { offset: Math.floor(rawStart) }
675
- if (out.view_range.length >= 2) {
676
- const rawEnd = Number(out.view_range[1])
677
- if (Number.isFinite(rawEnd) && rawEnd >= rawStart) {
678
- mapped.limit = Math.floor(rawEnd - rawStart + 1)
679
- }
680
- }
681
- out = Object.assign({}, out, mapped)
682
- }
683
- }
684
- if ('old_str' in out && !('old_string' in out)) {
685
- out = Object.assign({}, out, { old_string: out.old_str })
686
- }
687
- if ('new_str' in out && !('new_string' in out)) {
688
- out = Object.assign({}, out, { new_string: out.new_str })
689
- }
690
- return out
691
- }
692
-
693
- ${SHIM_TRY_SERVER}
694
-
695
- // Attempts the resident server (tryServer above), then the in-process hook call: import()s dist/token-goat-hook.mjs (a sibling of
696
- // the baked token-goat entry path, built with zero load-time side effects -- unlike
697
- // the CLI entry, which runs the full argv-parsing CLI as a side effect of being
698
- // loaded) and calls its exported relayInProcess() directly, avoiding a second node
699
- // process spawn entirely. Returns undefined (triggering the spawnSync fallback below)
700
- // when entryPath is absent, the sibling file doesn't exist (an older install predating
701
- // this file), or anything else goes wrong -- this must never throw.
702
- async function tryInProcess(entryPath, tgEvent, canonical, harness) {
703
- if (!entryPath) return undefined
704
- try {
705
- // Set before either path: the resident server runs the call under this process's environment.
706
- process.env.TOKEN_GOAT_HARNESS_OVERRIDE = harness
707
- // Which hooks directory VS Code loaded this shim from is the only thing separating a
708
- // user-scope copy from a project-scope one -- the two files are byte-identical and so are
709
- // their payloads. vscode_duplicate.ts needs it to stand the redundant copy down.
710
- if (harness === 'vscode') process.env.TOKEN_GOAT_VSCODE_HOOKS_DIR = __dirname
711
- const served = await tryServer(entryPath, tgEvent, canonical)
712
- if (served !== undefined) return served
713
- const hookLibPath = path.join(path.dirname(entryPath), 'token-goat-hook.mjs')
714
- if (!require('node:fs').existsSync(hookLibPath)) return undefined
715
- const mod = await import(pathToFileURL(hookLibPath).href)
716
- return await mod.relayInProcess(tgEvent, canonical)
717
- } catch {
718
- return undefined
719
- }
720
- }
721
-
722
- // VS Code's built-in agent reads this same hooks file (~/.copilot/hooks and .github/hooks are both
723
- // on its hook-source list) and runs these same entries, but it sends Claude Code's payload shape:
724
- // hook_event_name, session_id, tool_name, tool_input, tool_response. Copilot CLI's payload never
725
- // carries hook_event_name and always names its tool in toolName, so that pair tells them apart.
726
- // The payload goes to token-goat as-is under the vscode harness, which does its own tool-name
727
- // mapping and writes a response VS Code reads directly, so nothing here translates it.
728
- function isVscodePayload(payload) {
729
- return payload !== null && typeof payload === 'object' && typeof payload.hook_event_name === 'string' && payload.toolName === undefined
730
- }
731
-
732
- async function relayVscode(entryPath, tgEvent, payload) {
733
- // 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.
734
- const input = typeof payload.session_id === 'string' && payload.session_id !== ''
735
- ? payload
736
- : Object.assign({}, payload, { session_id: stableFallbackSessionId(process.cwd()) })
737
- let stdout = await tryInProcess(entryPath, tgEvent, input, 'vscode')
738
- if (stdout === undefined) {
739
- // 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.
740
- const spawnOpts = {
741
- input: JSON.stringify(input),
742
- encoding: 'utf8',
743
- windowsHide: true,
744
- timeout: 3000,
745
- killSignal: 'SIGKILL',
746
- maxBuffer: 32 * 1024 * 1024,
747
- env: Object.assign({}, process.env, { TOKEN_GOAT_HARNESS_OVERRIDE: 'vscode', TOKEN_GOAT_VSCODE_HOOKS_DIR: __dirname }),
748
- }
749
- const res = entryPath
750
- ? spawnSync(process.execPath, [entryPath, 'hook', tgEvent, '--harness', 'vscode'], spawnOpts)
751
- : spawnSync('token-goat hook ' + tgEvent + ' --harness vscode', Object.assign({ shell: true }, spawnOpts))
752
- if (res.status !== 0 || !res.stdout) return '{}'
753
- stdout = res.stdout
754
- }
755
- try {
756
- const parsed = JSON.parse(stdout)
757
- return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? JSON.stringify(parsed) : '{}'
758
- } catch {
759
- return '{}'
760
- }
761
- }
762
-
763
- async function main() {
764
- const copilotEvent = process.argv[2] || ''
765
-
766
- // 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.
767
- const tgEvent = ownGet(COPILOT_TO_TG_EVENT, copilotEvent)
768
- if (!tgEvent) {
769
- process.stdout.write('{}')
770
- return
771
- }
772
-
773
- let raw = ''
774
- try {
775
- raw = require('node:fs').readFileSync(0, 'utf8')
776
- } catch {
777
- process.stdout.write('{}')
778
- return
779
- }
780
-
781
- let payload
782
- try {
783
- payload = JSON.parse(raw)
784
- } catch {
785
- process.stdout.write('{}')
786
- return
787
- }
788
-
789
- if (isVscodePayload(payload)) {
790
- process.stdout.write(await relayVscode(process.argv[3], tgEvent, payload))
791
- return
792
- }
793
-
794
- const toolName = payload && payload.toolName
795
- const canonical = {
796
- session_id:
797
- (payload && payload.sessionId) ||
798
- stableFallbackSessionId(payload && (payload.workingDirectory || payload.cwd)),
799
- cwd: payload && (payload.workingDirectory || payload.cwd),
800
- }
801
-
802
- // Subagent correlation and W3C Trace Context propagation from Copilot CLI payloads
803
- const agentId = payload && (payload.agent_id || payload.agentId)
804
- if (typeof agentId === 'string' && agentId !== '') {
805
- canonical.agent_id = agentId
806
- }
807
- const traceparent = payload && (payload.traceparent || payload.traceParent)
808
- if (typeof traceparent === 'string' && traceparent !== '') {
809
- canonical.traceparent = traceparent
810
- }
811
- const tracestate = payload && (payload.tracestate || payload.traceState)
812
- if (typeof tracestate === 'string' && tracestate !== '') {
813
- canonical.tracestate = tracestate
814
- }
815
-
816
- // userPromptSubmitted only: Copilot declares \`prompt\` required on UserPromptSubmittedHookInput.
817
- // hooks_session.ts's userPromptSubmitHandler reads it as \`event.raw['prompt']\` and gates every
818
- // branch it has on the text, so without this it saw '' on every Copilot prompt and the
819
- // embedded-skill dedup hint could never fire. Same shape as the postToolUseFailure \`error\`
820
- // drop: a required field the canonical builder simply did not list.
821
- if (typeof (payload && payload.prompt) === 'string' && payload.prompt !== '') {
822
- canonical.prompt = payload.prompt
823
- }
824
- let originalToolArgs = {}
825
- if (toolName) {
826
- originalToolArgs = parseMaybeJsonObject(payload && payload.toolArgs)
827
- canonical.tool_name = resolveCanonicalToolName(toolName)
828
- canonical.tool_input = remapToolInput(toolName, originalToolArgs)
829
- }
830
-
831
- // postToolUse only: confirmed via https://docs.github.com/en/copilot/reference/hooks-reference
832
- // that Copilot's toolResult is an object ({resultType, textResultForLlm}), not a bare string
833
- // or array. Without this, token-goat's tool_response consumers (hooks_read.ts's
834
- // extractReadOutput, hooks_bash.ts's extractBashOutput, etc. -- all of which check for a
835
- // string or an object keyed by output/content/text/body) never see any content, so
836
- // post-read/post-bash stats stay empty no matter how many tool calls happen. Extract the
837
- // LLM-facing text directly rather than forwarding the raw object, since textResultForLlm
838
- // isn't one of those recognized object keys.
839
- // postToolUseFailure only: Copilot's PostToolUseFailureHookInput (copilot-sdk/types.d.ts:1042)
840
- // carries {toolName, toolArgs, error} and no toolResult at all -- the failure text lives in
841
- // \`error\`, a plain string. Without forwarding it, hooks_tool_failure.ts's extractFailureText
842
- // finds nothing to key on and the repeat-failure brake returns pass on every single call: wired,
843
- // green, and doing nothing. Found by driving the installed shim rather than by a test, because
844
- // the handler's own tests hand it a raw payload that already has the field.
845
- if (typeof (payload && payload.error) === 'string' && payload.error !== '') {
846
- canonical.error = payload.error
847
- }
848
-
849
- const rawResult = payload && payload.toolResult
850
- if (rawResult && typeof rawResult === 'object') {
851
- const tr = rawResult
852
- // text_result_for_llm: Copilot's docs also describe a "VS Code compatible" snake_case
853
- // wire format (tool_result.text_result_for_llm) alongside the camelCase one above; try
854
- // both rather than assuming only the camelCase shape ever reaches this shim.
855
- const text = typeof tr.textResultForLlm === 'string' ? tr.textResultForLlm : tr.text_result_for_llm
856
- if (typeof text === 'string') canonical.tool_response = text
857
- }
858
-
859
- // process.argv[3], when present, is the absolute path to the token-goat CLI entry that ran
860
- // \`token-goat install --copilot\` (baked in by hookCommandFor in copilot_cli_install.ts).
861
- // Invoking it directly via process.execPath sidesteps PATH/cmd.exe resolution for this
862
- // inner call too -- confirmed live-production root cause of an *intermittent* (not the
863
- // original, already-fixed github/copilot-cli#4001) "(hook errored)" deny-all: even once the
864
- // outer command launches via its own baked process.execPath, this inner call still shelled
865
- // out to a bare \`token-goat\`, which depends on the npm global bin being on whatever PATH
866
- // Copilot spawns this hook subprocess with. Falls back to the old PATH-based shell:true
867
- // invocation when argv[3] is absent (an older cached hook config still pointing at a
868
- // freshly-reinstalled shim, or a direct dev/test invocation), so this is a pure hardening,
869
- // never a behavior break for configs installed before this fix. An args array (not a
870
- // template string) is safe here specifically because entryPath and tgEvent never touch a
871
- // shell -- no DEP0190 concern, unlike the shell:true fallback below.
872
- const entryPath = process.argv[3]
873
- // Try the in-process hook lib first (tryInProcess above), which avoids spawning a
874
- // second node process altogether. If that's unavailable, invoking the entry directly
875
- // via process.execPath sidesteps PATH/cmd.exe resolution for this inner call, per the
876
- // note above this function's original single-spawn form documented. A 3000ms
877
- // timeout/killSignal on both spawnSync fallbacks keeps them well under Copilot CLI's
878
- // own hook timeout budget (~30000ms), so token-goat degrades to its own fail-open
879
- // '{}' rather than being force-killed by Copilot first.
880
- let stdout = await tryInProcess(entryPath, tgEvent, canonical, 'copilot_cli')
881
- if (stdout === undefined) {
882
- const res = entryPath
883
- ? spawnSync(process.execPath, [entryPath, 'hook', tgEvent], {
884
- input: JSON.stringify(canonical),
885
- encoding: 'utf8',
886
- windowsHide: true,
887
- timeout: 3000,
888
- killSignal: 'SIGKILL',
889
- maxBuffer: 32 * 1024 * 1024,
890
- env: Object.assign({}, process.env, { TOKEN_GOAT_HARNESS_OVERRIDE: 'copilot_cli' }),
891
- })
892
- : spawnSync('token-goat hook ' + tgEvent, {
893
- input: JSON.stringify(canonical),
894
- encoding: 'utf8',
895
- shell: true,
896
- windowsHide: true,
897
- timeout: 3000,
898
- killSignal: 'SIGKILL',
899
- maxBuffer: 32 * 1024 * 1024,
900
- env: Object.assign({}, process.env, { TOKEN_GOAT_HARNESS_OVERRIDE: 'copilot_cli' }),
901
- })
902
- if (res.status !== 0 || !res.stdout) {
903
- process.stdout.write('{}')
904
- return
905
- }
906
- stdout = res.stdout
907
- }
908
-
909
- let resp
910
- try {
911
- resp = JSON.parse(stdout)
912
- } catch {
913
- process.stdout.write('{}')
914
- return
915
- }
916
-
917
- process.stdout.write(JSON.stringify(translate(copilotEvent, resp, toolName, originalToolArgs, payload)))
918
- }
919
-
920
- ${MATERIALIZE_SHRUNK_IMAGE_JS}
921
-
922
- function translate(copilotEvent, resp, toolName, originalToolArgs, payload) {
923
- if (copilotEvent === 'preToolUse') {
924
- const hso = resp && resp.hookSpecificOutput
925
- const denied = resp && (resp.decision === 'block' || (hso && hso.permissionDecision === 'deny'))
926
- if (denied) {
927
- const reason =
928
- (resp && resp.reason) || (hso && hso.permissionDecisionReason) || 'blocked by token-goat'
929
- return { permissionDecision: 'deny', permissionDecisionReason: reason }
930
- }
931
- const context = extractContext(resp)
932
- // 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.
933
- const shrinkPayload = typeof context === 'string' && context.indexOf('data:image/') !== -1
934
- const out = {}
935
- const updated = hso && hso.updatedInput
936
- if (updated && typeof updated === 'object') {
937
- out.modifiedArgs = updated
938
- } else if (shrinkPayload && (toolName === 'view' || foldToolName(toolName) === 'view')) {
939
- // 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.
940
- const shrunkPath = materializeShrunkImage(context)
941
- if (shrunkPath) out.modifiedArgs = Object.assign({}, originalToolArgs, { path: shrunkPath })
942
- }
943
- // 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.
944
- if (context && !shrinkPayload) out.additionalContext = context
945
- return out
946
- }
947
-
948
- if (copilotEvent === 'postToolUse') {
949
- // modifiedResult is honored on success: rewriteOutput (compression, fencing, image shrink) reaches LLM.
950
- // additionalContext is dropped on Copilot CLI JS path, so the ONLY channel that reaches the model here is
951
- // modifiedResult.textResultForLlm, and the body that goes into it is chosen before the fold rather than
952
- // instead of it. The two used to be alternatives -- a rewritten output took the first branch and skipped the
953
- // fold entirely -- so any response carrying both a compressed body and a hint delivered the body and dropped
954
- // the hint on the floor, silently, on the one harness this whole fold exists for. Pick the body (rewritten if
955
- // there is one, else the original), then append the hint to whichever it was.
956
- const hso = resp && resp.hookSpecificOutput
957
- const updatedToolOutput = hso && hso.updatedToolOutput
958
- const context = extractContext(resp)
959
- const out = {}
960
- const rawResult = payload && payload.toolResult
961
- // Copilot's own ToolResult type is a union of a bare string and ToolResultObject (copilot-sdk/types.d.ts), so a string is a shape the harness itself admits even though PostToolUseHookInput narrows this field to the object form. Reading it as a body costs one typeof and removes the only route by which body could come back undefined on a well-formed payload: in every version this bridge has been checked against (1.0.80, 1.0.82, 1.0.86, 1.0.87, 1.0.88) ToolResultObject declares textResultForLlm as a required string rather than an optional one, so the object form always carries a body and the hint always has something to be folded into.
962
- const originalText =
963
- typeof rawResult === 'string'
964
- ? rawResult
965
- : rawResult && (typeof rawResult.textResultForLlm === 'string' ? rawResult.textResultForLlm : typeof rawResult.text_result_for_llm === 'string' ? rawResult.text_result_for_llm : undefined)
966
- const rewritten = typeof updatedToolOutput === 'string'
967
- const body = rewritten ? updatedToolOutput : typeof originalText === 'string' ? originalText : undefined
968
- // Never claim a modification we did not make: with no rewrite and no hint, the body would be the tool's own
969
- // text handed back verbatim, and a pass-through modifiedResult is a lie about authorship on every call.
970
- if (typeof body === 'string' && (rewritten || context)) {
971
- out.modifiedResult = { resultType: 'success', textResultForLlm: context ? body + '\\n\\n[token-goat: ' + context + ']' : body }
972
- }
973
- // Still set alongside the fold rather than instead of it: the field is dropped on the JS path read at 1.0.80,
974
- // but it is the documented channel and costs nothing if a later release starts honoring it. The fold above is
975
- // what the delivery guarantee rests on.
976
- if (context) out.additionalContext = context
977
- return out
978
- }
979
-
980
- if (copilotEvent === 'postToolUseFailure') {
981
- // The failed-tool twin of postToolUse, and the only response field it accepts is
982
- // additionalContext: the shipped copilot-sdk/types.d.ts says "modifiedResult or suppressOutput
983
- // are not honored for failure hooks", so nothing here can fence, compress or shrink the failed
984
- // output -- that gap is real and stays open. What is NOT open, and was the reason this event
985
- // went unwired for so long, is whether additionalContext reaches the model at all. It does:
986
- // app.js 1.0.80 at offset 2043380 either folds it into textResultForLlm (when
987
- // appendFailureContextToToolResult is set) or has the native
988
- // hookAppendPostToolUseFailureContext push {content, source:'system'} onto
989
- // toolResult.newMessages. That is the exact opposite of postToolUse, whose additionalContext
990
- // the JS side drops on the floor, so neither event's behaviour generalises to the other.
991
- //
992
- // Because this channel spends tokens rather than saving them, the handler behind it
993
- // (hooks_tool_failure.ts) is silent on a first failure and speaks only on an exact repeat.
994
- const context = extractContext(resp)
995
- if (context) return { additionalContext: context }
996
- return {}
997
- }
998
-
999
- if (copilotEvent === 'sessionStart') {
1000
- // sessionStart has no tool result to modify -- only additionalContext applies, and it's the
1001
- // one channel that reaches the model before it picks its first read tool, so this is where
1002
- // the routing reminder has to land.
1003
- const context = extractContext(resp)
1004
- if (context) return { additionalContext: context }
1005
- return {}
1006
- }
1007
-
1008
- if (copilotEvent === 'agentStop' || copilotEvent === 'subagentStop') {
1009
- // Confirmed against the hooks reference doc: the only accepted response
1010
- // shape for these two events is {decision, reason} -- additionalContext
1011
- // is not part of their schema and Copilot silently ignores it there.
1012
- // token-goat's internal 'stop'/'subagent_stop' handlers never return a
1013
- // real deny today (subagentStopHandler only ever logs and passes), but a
1014
- // future deny is mapped through here rather than silently dropped.
1015
- if (resp && resp.decision === 'block') {
1016
- const reason = (resp && resp.reason) || 'blocked by token-goat'
1017
- return { decision: 'block', reason: reason }
1018
- }
1019
- return { decision: 'allow' }
1020
- }
1021
-
1022
- if (copilotEvent === 'userPromptSubmitted') {
1023
- // Copilot's own hooks reference says command-hook output here "is dropped, including
1024
- // modifiedPrompt", and this branch used to believe it and return nothing. That is wrong for
1025
- // additionalContext on 1.0.80, established by experiment rather than by reading: a config-file
1026
- // command hook (at <cwd>/.github/hooks/, the project scope -- the user scope ~/.copilot/hooks/
1027
- // was never exercised) returned {"additionalContext":"<marker>"} and the marker turned up
1028
- // verbatim inside the session's user.message.transformedContent, wrapped in a
1029
- // <system_reminder> block. What settles that it reached the model rather than only the on-disk
1030
- // record is the billing: the provider's returned usage charged ~140 input tokens for a turn
1031
- // whose raw content field is 35 bytes, so the marker's bytes were paid for whichever field
1032
- // carried them. (The weaker argument first offered for this -- that transformedContent carries
1033
- // an envelope the content field lacks, 29 B vs 195 B -- was measured on the control turn that
1034
- // had no marker in it, and proves nothing about the marker.) That the model sees it is the
1035
- // whole point: hook.start/hook.end records also persist and reach nothing.
1036
- //
1037
- // Scope of the finding, stated honestly: demonstrated ONCE on 1.0.80, not shown to be
1038
- // reliable. Of two turns in that experiment, one delivered the marker and one fired a
1039
- // userPromptSubmitted hook that produced no output and never ran the script; no explanation
1040
- // was established and the rate is unknown. A hint that silently fails to arrive costs nothing
1041
- // and breaks nothing here, which is why the direct return is still the right default.
1042
- //
1043
- // modifiedPrompt is NOT claimed to work and is not wanted: rewriting a user's prompt is far
1044
- // more invasive than anything token-goat does, so only additionalContext is forwarded.
1045
- //
1046
- // This is also the write end of any post-compaction channel. Copilot has no postCompact hook.
1047
- // Whether the summary is recoverable from events.jsonl is NOT settled -- the emit()/
1048
- // emitEphemeral() distinction does not gate the writer, and the real decision is in native
1049
- // code; see COPILOT_NO_POST_COMPACT_REASON in ../bridges_status.ts for what was and was not
1050
- // established. The read end that IS confirmed is preCompact, which fires as a notification, so
1051
- // a manifest can be built there and drained here without reading the event log at all.
1052
- const context = extractContext(resp)
1053
- if (context) return { additionalContext: context }
1054
- return {}
1055
- }
1056
-
1057
- // preCompact is the genuine notification-only case, and the contrast with userPromptSubmitted
1058
- // above is why this fallthrough is worth a comment at all. The hooks reference marks it
1059
- // "No -- notification only" for output processing, and unlike the additionalContext claim that
1060
- // turned out to be false, this one is confirmed in the shipping bundle: both preCompact call
1061
- // sites in app.js (1.0.79 and re-checked in 1.0.80) await the hook and never assign its result.
1062
- // There is no field to aim at here, so nothing to reconsider on the next version bump.
1063
- return {}
1064
- }
1065
-
1066
- function extractContext(resp) {
1067
- const hso = resp && resp.hookSpecificOutput
1068
- if (hso && typeof hso.additionalContext === 'string') return hso.additionalContext
1069
- if (resp && typeof resp.systemMessage === 'string') return resp.systemMessage
1070
- return undefined
1071
- }
1072
-
1073
- // Hard outer safety net, on top of main()'s own per-step try/catch fallbacks
1074
- // (JSON.parse, readFileSync, spawnSync): a hook error must never itself cause
1075
- // Copilot's fail-closed "(hook errored)" behavior, which denies EVERY tool
1076
- // call unconditionally (the exact live-production failure mode behind
1077
- // github/copilot-cli#4001). Any uncaught exception anywhere in this script --
1078
- // including one a future code path adds that the existing per-step guards
1079
- // don't anticipate -- still guarantees stdout gets valid JSON and the
1080
- // process exits 0. process.exitCode is set explicitly and unconditionally at
1081
- // the very end of every path so nothing upstream (e.g. an unhandled-rejection
1082
- // warning in some Node versions nudging exit-code inference) can flip it.
1083
- main()
1084
- .catch(() => {
1085
- try {
1086
- process.stdout.write('{}')
1087
- } catch {
1088
- // stdout itself is broken; nothing more can be done here.
1089
- }
1090
- })
1091
- .finally(() => {
1092
- process.exitCode = 0
1093
- })
1094
- `;
1095
- var TOOL_TO_TG_MAPPING = COPILOT_CLI_TOOL_NAME_MAP;
1096
- var FOLDED_MAPPING = {};
1097
- for (const [k, v] of Object.entries(TOOL_TO_TG_MAPPING)) {
1098
- FOLDED_MAPPING[foldToolName(k)] = v;
1099
- FOLDED_MAPPING[foldToolName(v)] = v;
1100
- }
1101
-
1102
- // src/bridges/copilot_cli_install.ts
1103
- var COPILOT_CLI_HOOK_EVENTS = [
1104
- "sessionStart",
1105
- "preToolUse",
1106
- "postToolUse",
1107
- "preCompact",
1108
- "agentStop",
1109
- "subagentStop",
1110
- "userPromptSubmitted",
1111
- "postToolUseFailure"
1112
- ];
1113
- function copilotCliUserRoot() {
1114
- const override = process.env["COPILOT_HOME"];
1115
- if (override !== void 0 && override.trim() !== "") return path3.resolve(override);
1116
- return path3.join(os2.homedir(), ".copilot");
1117
- }
1118
- function copilotCliUserHooksDir() {
1119
- return path3.join(copilotCliUserRoot(), "hooks");
1120
- }
1121
- function copilotCliProjectHooksDir() {
1122
- return path3.join(process.cwd(), ".github", "hooks");
1123
- }
1124
- function copilotCliCacheRoot() {
1125
- const override = process.env["COPILOT_CACHE_HOME"];
1126
- if (override !== void 0 && override.trim() !== "") return path3.resolve(override);
1127
- const home = os2.homedir();
1128
- if (process.platform === "darwin") return path3.join(home, "Library", "Caches", "copilot");
1129
- if (process.platform === "win32") {
1130
- const local = process.env["LOCALAPPDATA"];
1131
- const base2 = local !== void 0 && local.trim() !== "" ? local : path3.join(home, ".cache");
1132
- return path3.join(base2, "copilot");
1133
- }
1134
- const xdg = process.env["XDG_CACHE_HOME"];
1135
- const base = xdg !== void 0 && xdg.trim() !== "" ? xdg : path3.join(home, ".cache");
1136
- return path3.join(base, "copilot");
1137
- }
1138
- function copilotCliMcpToolsDir() {
1139
- return path3.join(copilotCliCacheRoot(), "mcp-tools");
1140
- }
1141
- function copilotCliHooksDir(opts = {}) {
1142
- return opts.local === true ? copilotCliProjectHooksDir() : copilotCliUserHooksDir();
1143
- }
1144
- function copilotCliConfigPath(opts = {}) {
1145
- return path3.join(copilotCliHooksDir(opts), HOOKS_CONFIG_FILE);
1146
- }
1147
- function copilotCliScriptPath(opts = {}) {
1148
- return path3.join(copilotCliHooksDir(opts), HOOKS_SCRIPT_FILE);
1149
- }
1150
- function copilotCliInstructionsPath(opts = {}) {
1151
- return path3.join(path3.dirname(copilotCliHooksDir(opts)), "copilot-instructions.md");
1152
- }
1153
- var COPILOT_INSTRUCTIONS_BEGIN = "<!-- token-goat-begin -->";
1154
- var COPILOT_INSTRUCTIONS_END = "<!-- token-goat-end -->";
1155
- function buildCopilotInstructionsBlock() {
1156
- return stripInlineCodeSpans(
1157
- buildGuidanceBlock({
1158
- beginMarker: COPILOT_INSTRUCTIONS_BEGIN,
1159
- endMarker: COPILOT_INSTRUCTIONS_END,
1160
- fallbackToolClause: "Copilot CLI's native `view`, `grep`, and `glob` tools (with PowerShell commands `Get-Content`/`Select-String` as search fallbacks)",
1161
- gdrive: loadConfig().gdrive.enabled
1162
- })
1163
- );
1164
- }
1165
- function stripInlineCodeSpans(text) {
1166
- return text.replace(/`([^`]+)`/g, "$1");
1167
- }
1168
- function writeCopilotInstructionsBlock(p) {
1169
- const changed = upsertDelimitedBlock(p, COPILOT_INSTRUCTIONS_BEGIN, COPILOT_INSTRUCTIONS_END, buildCopilotInstructionsBlock());
1170
- const visualStudioChanged = syncVisualStudioProjectGuidance(p);
1171
- return changed || visualStudioChanged;
1172
- }
1173
- function stripCopilotInstructionsBlock(p) {
1174
- const removed = stripDelimitedBlock(p, COPILOT_INSTRUCTIONS_BEGIN, COPILOT_INSTRUCTIONS_END);
1175
- if (removed) syncVisualStudioProjectGuidance(p);
1176
- return removed;
1177
- }
1178
- function hookPowershellCommandFor(scriptPath, event) {
1179
- return `& ${hookPowershellCommand(scriptPath, event)}`;
1180
- }
1181
- var HOOK_TIMEOUT_SEC = 60;
1182
- var ALLOWED_ENV_VARS = [
1183
- "TRACEPARENT",
1184
- "TRACESTATE",
1185
- "COPILOT_HOME",
1186
- "COPILOT_CACHE_HOME",
1187
- "TOKEN_GOAT_LOG"
1188
- ];
1189
- function buildConfig(scriptPath) {
1190
- const hooks = {};
1191
- for (const event of COPILOT_CLI_HOOK_EVENTS) {
1192
- hooks[event] = [
1193
- {
1194
- type: "command",
1195
- command: hookCommandFor(scriptPath, event),
1196
- bash: hookCommandFor(scriptPath, event),
1197
- powershell: hookPowershellCommandFor(scriptPath, event),
1198
- timeoutSec: HOOK_TIMEOUT_SEC,
1199
- allowedEnvVars: [...ALLOWED_ENV_VARS]
1200
- }
1201
- ];
1202
- }
1203
- return { version: 1, hooks };
1204
- }
1205
- var HOOKS_CONFIG_FILE = "token-goat.json";
1206
- var HOOKS_SCRIPT_FILE = "token-goat-shim.cjs";
1207
- var LEGACY_HOOKS_SCRIPT_FILE = "token-goat-shim.js";
1208
- var HOOKS_OWNERS_FILE = "token-goat.owners";
1209
- var HOOKS_SCRIPT_FORWARDER = `#!/usr/bin/env node
1210
- // Forwarder for compatibility with cached hook definitions in running Copilot CLI sessions.
1211
- import('./token-goat-shim.cjs').catch(() => {
1212
- try { process.stdout.write('{}') } catch {}
1213
- process.exitCode = 0
1214
- });
1215
- `;
1216
- function copilotHooksOwnersPath(hooksDir) {
1217
- return path3.join(hooksDir, HOOKS_OWNERS_FILE);
1218
- }
1219
- function copilotHooksFilePaths(hooksDir) {
1220
- return [
1221
- path3.join(hooksDir, HOOKS_CONFIG_FILE),
1222
- path3.join(hooksDir, HOOKS_SCRIPT_FILE),
1223
- path3.join(hooksDir, LEGACY_HOOKS_SCRIPT_FILE),
1224
- copilotHooksOwnersPath(hooksDir)
1225
- ];
1226
- }
1227
- function readCopilotHooksOwners(hooksDir) {
1228
- const owners = /* @__PURE__ */ new Set();
1229
- let text;
1230
- try {
1231
- text = fs3.readFileSync(copilotHooksOwnersPath(hooksDir), "utf8");
1232
- } catch {
1233
- if (fs3.existsSync(path3.join(hooksDir, HOOKS_CONFIG_FILE))) owners.add("copilot");
1234
- return owners;
1235
- }
1236
- for (const line of text.split(/\r?\n/)) {
1237
- const name = line.trim();
1238
- if (name === "copilot" || name === "vscode") owners.add(name);
1239
- }
1240
- return owners;
1241
- }
1242
- function installCopilotHooksFile(hooksDir, owner) {
1243
- const configPath = path3.join(hooksDir, HOOKS_CONFIG_FILE);
1244
- const scriptPath = path3.join(hooksDir, HOOKS_SCRIPT_FILE);
1245
- const legacyScriptPath = path3.join(hooksDir, LEGACY_HOOKS_SCRIPT_FILE);
1246
- const owners = readCopilotHooksOwners(hooksDir);
1247
- const scriptChanged = writeIfDifferent(scriptPath, COPILOT_CLI_HOOK_SCRIPT);
1248
- const legacyScriptChanged = writeIfDifferent(legacyScriptPath, HOOKS_SCRIPT_FORWARDER);
1249
- const desiredText = JSON.stringify(buildConfig(scriptPath), null, 2) + "\n";
1250
- const configChanged = writeIfDifferent(configPath, desiredText, true);
1251
- recordCreatedConfig(configPath);
1252
- owners.add(owner);
1253
- const ownersChanged = writeIfDifferent(copilotHooksOwnersPath(hooksDir), [...owners].sort().join("\n") + "\n");
1254
- return { configPath, scriptPath, changed: scriptChanged || legacyScriptChanged || configChanged || ownersChanged };
1255
- }
1256
- function releaseCopilotHooksFile(hooksDir, owner, keepBackups = false) {
1257
- const ownersPath = copilotHooksOwnersPath(hooksDir);
1258
- const hadSidecar = fs3.existsSync(ownersPath);
1259
- const owners = readCopilotHooksOwners(hooksDir);
1260
- if (!owners.has(owner) && (hadSidecar || owner !== "copilot")) return false;
1261
- owners.delete(owner);
1262
- if (owners.size > 0) {
1263
- writeIfDifferent(ownersPath, [...owners].sort().join("\n") + "\n");
1264
- return true;
1265
- }
1266
- const configRemoved = removeFileInScope(path3.join(hooksDir, HOOKS_CONFIG_FILE));
1267
- if (!keepBackups) removeCreatedBackups(path3.join(hooksDir, HOOKS_CONFIG_FILE));
1268
- takeCreatedConfig(path3.join(hooksDir, HOOKS_CONFIG_FILE));
1269
- const scriptRemoved = removeFileInScope(path3.join(hooksDir, HOOKS_SCRIPT_FILE));
1270
- const legacyScriptRemoved = removeFileInScope(path3.join(hooksDir, LEGACY_HOOKS_SCRIPT_FILE));
1271
- const ownersRemoved = removeFileInScope(ownersPath);
1272
- return configRemoved || scriptRemoved || legacyScriptRemoved || ownersRemoved;
1273
- }
1274
- function installCopilotCli(opts = {}) {
1275
- return withInstallScope(projectScopeRoot(opts), () => installCopilotCliScoped(opts));
1276
- }
1277
- function installCopilotCliScoped(opts) {
1278
- const instructionsPath = copilotCliInstructionsPath(opts);
1279
- const hooks = installCopilotHooksFile(copilotCliHooksDir(opts), "copilot");
1280
- const instructionsChanged = writeCopilotInstructionsBlock(instructionsPath);
1281
- return {
1282
- configPath: hooks.configPath,
1283
- scriptPath: hooks.scriptPath,
1284
- instructionsPath,
1285
- alreadyInstalled: !hooks.changed && !instructionsChanged
1286
- };
1287
- }
1288
- function uninstallCopilotCliScope(opts) {
1289
- return withInstallScope(projectScopeRoot(opts), () => uninstallCopilotCliScopeInner(opts));
1290
- }
1291
- function uninstallCopilotCliScopeInner(opts) {
1292
- let removedAny = releaseCopilotHooksFile(copilotCliHooksDir(opts), "copilot");
1293
- if (stripCopilotInstructionsBlock(copilotCliInstructionsPath(opts))) {
1294
- removedAny = true;
1295
- }
1296
- return removedAny;
1297
- }
1298
- function uninstallCopilotCli(opts = {}) {
1299
- if (opts.local === true) {
1300
- return uninstallCopilotCliScope({ local: true });
1301
- }
1302
- const userRemoved = uninstallCopilotCliScope({ local: false });
1303
- const localRemoved = uninstallCopilotCliScope({ local: true });
1304
- return userRemoved || localRemoved;
1305
- }
1306
- function isCopilotCliInstalled(opts = {}) {
1307
- const scriptPresent = fs3.existsSync(copilotCliScriptPath(opts)) || fs3.existsSync(path3.join(copilotCliHooksDir(opts), LEGACY_HOOKS_SCRIPT_FILE));
1308
- if (!fs3.existsSync(copilotCliConfigPath(opts)) || !scriptPresent) {
1309
- return false;
1310
- }
1311
- if (!readCopilotHooksOwners(copilotCliHooksDir(opts)).has("copilot")) return false;
1312
- let instructions;
1313
- try {
1314
- instructions = fs3.readFileSync(copilotCliInstructionsPath(opts), "utf8");
1315
- } catch {
1316
- return false;
1317
- }
1318
- return instructions.includes(COPILOT_INSTRUCTIONS_BEGIN) && instructions.includes(COPILOT_INSTRUCTIONS_END);
1319
- }
1320
-
1321
- // src/copilot_mcp_tools.ts
1322
- init_define_import_meta_env();
1323
- import fs4 from "node:fs";
1324
- import path4 from "node:path";
1325
- function wireShapeLength(tools) {
1326
- const wire = tools.map((tool) => {
1327
- const out = {};
1328
- if (typeof tool.name === "string") out["name"] = tool.name;
1329
- if (typeof tool.description === "string") out["description"] = tool.description;
1330
- if (tool.inputSchema !== void 0) out["inputSchema"] = tool.inputSchema;
1331
- return out;
1332
- });
1333
- return JSON.stringify(wire).length;
1334
- }
1335
- function readCopilotMcpTools(dir = copilotCliMcpToolsDir()) {
1336
- let entries;
1337
- try {
1338
- entries = fs4.readdirSync(dir);
1339
- } catch {
1340
- return { cacheFound: false, servers: [], unreadable: 0 };
1341
- }
1342
- const newest = /* @__PURE__ */ new Map();
1343
- let unreadable = 0;
1344
- for (const entry of entries) {
1345
- if (!entry.toLowerCase().endsWith(".json")) continue;
1346
- let parsed;
1347
- try {
1348
- parsed = JSON.parse(fs4.readFileSync(path4.join(dir, entry), "utf8"));
1349
- } catch {
1350
- unreadable += 1;
1351
- continue;
1352
- }
1353
- if (typeof parsed !== "object" || parsed === null) {
1354
- unreadable += 1;
1355
- continue;
1356
- }
1357
- const record = parsed;
1358
- const serverName = typeof record.serverName === "string" ? record.serverName.trim() : "";
1359
- if (serverName === "") {
1360
- unreadable += 1;
1361
- continue;
1362
- }
1363
- const tools = Array.isArray(record.tools) ? record.tools : [];
1364
- const updatedAt = typeof record.updatedAt === "string" ? record.updatedAt : "";
1365
- const definitionBytes = wireShapeLength(tools);
1366
- const candidate = {
1367
- serverName,
1368
- toolCount: tools.length,
1369
- definitionBytes,
1370
- estimatedTokens: estimateTokensFromLength(definitionBytes),
1371
- updatedAt,
1372
- // `tools` is an unchecked cast off a JSON array, so every element is
1373
- // treated as untrusted here rather than trusted to match CachedTool.
1374
- toolNames: tools.map((t) => t?.name).filter((n) => typeof n === "string" && n !== "")
1375
- };
1376
- const existing = newest.get(serverName);
1377
- if (existing === void 0 || candidate.updatedAt > existing.updatedAt) newest.set(serverName, candidate);
1378
- }
1379
- const servers = [...newest.values()].sort(
1380
- (a, b) => b.definitionBytes - a.definitionBytes || a.serverName.localeCompare(b.serverName)
1381
- );
1382
- return { cacheFound: true, servers, unreadable };
1383
- }
1384
-
1385
- export {
1386
- foldToolName,
1387
- COPILOT_CLI_TOOL_NAME_MAP,
1388
- SHIM_TRY_SERVER,
1389
- MATERIALIZE_SHRUNK_IMAGE_JS,
1390
- COPILOT_CLI_HOOK_SCRIPT,
1391
- buildGuidanceBody,
1392
- skillDescriptionLine,
1393
- buildGuidanceBlock,
1394
- jsonc,
1395
- bundledCliPath,
1396
- managedServer,
1397
- isManagedServer,
1398
- readServersJson,
1399
- serversOf,
1400
- setTokenGoatServer,
1401
- isResidueServersJson,
1402
- dropEmptyServers,
1403
- hasManagedServer,
1404
- visualStudioUserMcpPath,
1405
- visualStudioProjectMcpPath,
1406
- visualStudioOtherScopeHasManagedServer,
1407
- isVisualStudioInstalled,
1408
- visualStudioSolutionVscodeMcpPath,
1409
- visualStudioDuplicateNote,
1410
- visualStudioMcpStatus,
1411
- visualStudioManagedEntry,
1412
- syncVisualStudioProjectGuidance,
1413
- installVisualStudio,
1414
- uninstallVisualStudio,
1415
- copilotCliUserRoot,
1416
- copilotCliMcpToolsDir,
1417
- copilotCliConfigPath,
1418
- copilotCliScriptPath,
1419
- HOOKS_SCRIPT_FILE,
1420
- LEGACY_HOOKS_SCRIPT_FILE,
1421
- copilotHooksFilePaths,
1422
- readCopilotHooksOwners,
1423
- installCopilotHooksFile,
1424
- releaseCopilotHooksFile,
1425
- installCopilotCli,
1426
- uninstallCopilotCli,
1427
- isCopilotCliInstalled,
1428
- readCopilotMcpTools
1429
- };