@cleocode/cleo 2026.9.7 → 2026.9.10

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 (691) hide show
  1. package/dist/cli/index.js +3873 -2234
  2. package/package.json +14 -13
  3. package/scripts/check-cleo-tarball-size.mjs +105 -0
  4. package/scripts/copy-studio-dist.mjs +33 -9
  5. package/studio-dist/client/_app/immutable/chunks/{wI_OOyoh.js → BB7zPpV2.js} +1 -1
  6. package/studio-dist/client/_app/immutable/chunks/{D8NsLz6i.js → BOnrCZaR.js} +1 -1
  7. package/studio-dist/client/_app/immutable/chunks/{eK8hf4s7.js → BSRBuVG3.js} +1 -1
  8. package/studio-dist/client/_app/immutable/chunks/C6jhAnil.js +1 -0
  9. package/studio-dist/client/_app/immutable/chunks/C87Q3uxm.js +1 -0
  10. package/studio-dist/client/_app/immutable/chunks/{DVHIxkgs.js → CLZBEpLY.js} +1 -1
  11. package/studio-dist/client/_app/immutable/chunks/{CqF5Lomf.js → CTLGzgdN.js} +1 -1
  12. package/studio-dist/client/_app/immutable/chunks/{o1PZ6Kzq.js → CY3heNub.js} +1 -1
  13. package/studio-dist/client/_app/immutable/chunks/{RptFUp2P.js → Cge6qiwc.js} +1 -1
  14. package/studio-dist/client/_app/immutable/chunks/{BRgWBQlV.js → CnwXLaZJ.js} +1 -1
  15. package/studio-dist/client/_app/immutable/chunks/{Bxz2A0ZC.js → DEH43YwF.js} +1 -1
  16. package/studio-dist/client/_app/immutable/chunks/DGftG5oL.js +3 -0
  17. package/studio-dist/client/_app/immutable/chunks/DX_JOBSO.js +1 -0
  18. package/studio-dist/client/_app/immutable/chunks/{CEqOYim7.js → DZrdToAH.js} +1 -1
  19. package/studio-dist/client/_app/immutable/chunks/EZwHPWle.js +2 -0
  20. package/studio-dist/client/_app/immutable/chunks/cWVw9W0E.js +3 -0
  21. package/studio-dist/client/_app/immutable/chunks/{B9psmq5-.js → rB_71s8h.js} +1 -1
  22. package/studio-dist/client/_app/immutable/chunks/{JPJd4fEI.js → rUtW7Hx6.js} +1 -1
  23. package/studio-dist/client/_app/immutable/entry/app.tvu3-wbp.js +2 -0
  24. package/studio-dist/client/_app/immutable/entry/start.BS-rpdjH.js +1 -0
  25. package/studio-dist/client/_app/immutable/nodes/{0.CZzEEyVh.js → 0.DMP7swbx.js} +1 -1
  26. package/studio-dist/client/_app/immutable/nodes/{1.Bj3Opbua.js → 1.CYZMFpIp.js} +1 -1
  27. package/studio-dist/client/_app/immutable/nodes/{10.xe_Mc2Ko.js → 10.3rxHNnSx.js} +1 -1
  28. package/studio-dist/client/_app/immutable/nodes/{11.DuKPA7JD.js → 11.B6jWv3Kz.js} +1 -1
  29. package/studio-dist/client/_app/immutable/nodes/{12.BoZ_QrUS.js → 12.BrDHeTEq.js} +1 -1
  30. package/studio-dist/client/_app/immutable/nodes/{13.DFJiUiv8.js → 13.CE5bd4JV.js} +1 -1
  31. package/studio-dist/client/_app/immutable/nodes/{14.eaAlFU4w.js → 14.DgXD2LRx.js} +1 -1
  32. package/studio-dist/client/_app/immutable/nodes/{15.BfXgiwMC.js → 15.Cz1KI9Fv.js} +1 -1
  33. package/studio-dist/client/_app/immutable/nodes/{16.mEwy9cqh.js → 16.K9wYQPhb.js} +1 -1
  34. package/studio-dist/client/_app/immutable/nodes/{17.DP2QtQGn.js → 17.CtUmdfMg.js} +1 -1
  35. package/studio-dist/client/_app/immutable/nodes/{18.Bu0054sX.js → 18.mtosd0sL.js} +1 -1
  36. package/studio-dist/client/_app/immutable/nodes/{19.UWt6QbCm.js → 19.su55K8y_.js} +1 -1
  37. package/studio-dist/client/_app/immutable/nodes/{2.y5jKpHiR.js → 2.mqgFVWFQ.js} +1 -1
  38. package/studio-dist/client/_app/immutable/nodes/{20.B1AIHdkP.js → 20.NJuHLxuS.js} +1 -1
  39. package/studio-dist/client/_app/immutable/nodes/{21.CpAN71H8.js → 21.BhwhdQCq.js} +1 -1
  40. package/studio-dist/client/_app/immutable/nodes/{22.CrYKrN4c.js → 22.BmJEWwER.js} +1 -1
  41. package/studio-dist/client/_app/immutable/nodes/{23.pGRaHQjq.js → 23.CAVIJEH-.js} +1 -1
  42. package/studio-dist/client/_app/immutable/nodes/{24.BUoUu37z.js → 24.CMuXs5Sy.js} +1 -1
  43. package/studio-dist/client/_app/immutable/nodes/25.DhMBJYAo.js +39 -0
  44. package/studio-dist/client/_app/immutable/nodes/26.CJ4FA5FS.js +1 -0
  45. package/studio-dist/client/_app/immutable/nodes/{28.DcmYM7lb.js → 28.D64RBcfs.js} +1 -1
  46. package/studio-dist/client/_app/immutable/nodes/{29.lbHbMqNO.js → 29.yfuBeH1x.js} +1 -1
  47. package/studio-dist/client/_app/immutable/nodes/{3.Dyir4xXj.js → 3.y115j1d5.js} +1 -1
  48. package/studio-dist/client/_app/immutable/nodes/{30.DXj0Wm2A.js → 30.1WUv5bh9.js} +1 -1
  49. package/studio-dist/client/_app/immutable/nodes/4.D6vXYBoQ.js +1 -0
  50. package/studio-dist/client/_app/immutable/nodes/{5.VWSdP7oc.js → 5.hjw2O-4e.js} +1 -1
  51. package/studio-dist/client/_app/immutable/nodes/{6.Dl-XLwOj.js → 6.CAoDfDt6.js} +1 -1
  52. package/studio-dist/client/_app/immutable/nodes/{7.CFuC5L-y.js → 7.CLgPSQ8Z.js} +1 -1
  53. package/studio-dist/client/_app/immutable/nodes/{8.BCJ0DhOb.js → 8.zuaO1Hik.js} +1 -1
  54. package/studio-dist/client/_app/immutable/nodes/{9.Bq4SUzL9.js → 9.Cdsd85m2.js} +1 -1
  55. package/studio-dist/client/_app/version.json +1 -1
  56. package/studio-dist/server/chunks/0-BQvtFOrC.js +41 -0
  57. package/studio-dist/server/chunks/1-Bx2_yoiK.js +9 -0
  58. package/studio-dist/server/chunks/{10-DvOD4ksa.js → 10-CYLc3d5x.js} +3 -3
  59. package/studio-dist/server/chunks/{11-DMnBZ02H.js → 11-DcIYR5vm.js} +3 -3
  60. package/studio-dist/server/chunks/{12-CCyRyRWX.js → 12-WE1u104t.js} +3 -3
  61. package/studio-dist/server/chunks/{13-Dx1c3a_X.js → 13-joBDoxhK.js} +3 -3
  62. package/studio-dist/server/chunks/{14-Bo-dTCaZ.js → 14-Doj5F4uJ.js} +3 -3
  63. package/studio-dist/server/chunks/{15-BvUNl3KL.js → 15-nQTeHGD1.js} +3 -3
  64. package/studio-dist/server/chunks/{16-De6EzPSa.js → 16-CcTN0e8k.js} +5 -4
  65. package/studio-dist/server/chunks/{17-CZNsiZhi.js → 17-DWiFnzES.js} +10 -93
  66. package/studio-dist/server/chunks/{18-CbxxjZox.js → 18-Dcc9GnPo.js} +5 -4
  67. package/studio-dist/server/chunks/{19-DqHyladS.js → 19-BAQHPrMB.js} +3 -3
  68. package/studio-dist/server/chunks/{2-COvmA-ZA.js → 2-DXSVVaej.js} +10 -93
  69. package/studio-dist/server/chunks/20-DfQlGHPW.js +57 -0
  70. package/studio-dist/server/chunks/{21-DXK9ptAC.js → 21-qhGjkW41.js} +4 -31
  71. package/studio-dist/server/chunks/{22-DD5CLWJC.js → 22-Byy16hFA.js} +3 -3
  72. package/studio-dist/server/chunks/23-CZpFrGp4.js +97 -0
  73. package/studio-dist/server/chunks/24-EEysaT9T.js +67 -0
  74. package/studio-dist/server/chunks/{25-CAg6Xcmn.js → 25-CgiYn70L.js} +15 -96
  75. package/studio-dist/server/chunks/{26-_nfVTvNi.js → 26-Cfd6BaUb.js} +12 -93
  76. package/studio-dist/server/chunks/{27-C9edghG4.js → 27-Cqzlh9gZ.js} +3 -2
  77. package/studio-dist/server/chunks/{28-VemTJGyc.js → 28-DeYe2rpm.js} +8 -62
  78. package/studio-dist/server/chunks/{29-CXP-m29t.js → 29-6pjLM0Z1.js} +10 -93
  79. package/studio-dist/server/chunks/{3-B8KhfvHp.js → 3-E_fWDAkq.js} +9 -25
  80. package/studio-dist/server/chunks/{30-QiM1S4cJ.js → 30-Dw317sSW.js} +10 -93
  81. package/studio-dist/server/chunks/{31-BwdY9NnB.js → 31-Q4kHbp8j.js} +3 -2
  82. package/studio-dist/server/chunks/{4-CSaG4HjO.js → 4-C9BMSh4K.js} +4 -3
  83. package/studio-dist/server/chunks/{5-i5ry4y3C.js → 5-C0CF4WKG.js} +3 -3
  84. package/studio-dist/server/chunks/{6-DR1GWBMT.js → 6-BWyOJust.js} +3 -3
  85. package/studio-dist/server/chunks/{7-DSNwn3aQ.js → 7-CK4MK5gy.js} +3 -3
  86. package/studio-dist/server/chunks/{8-Bj0bx07K.js → 8-CiX7Xk9k.js} +3 -3
  87. package/studio-dist/server/chunks/{9-B3Tz0mZq.js → 9-Be5118w4.js} +3 -3
  88. package/studio-dist/server/chunks/{HeroHeader-v4Q9vaGr.js → HeroHeader-BrGN5WPw.js} +2 -2
  89. package/studio-dist/server/chunks/{Sparkline-BJ-GuhRd.js → Sparkline-DU_NVxz9.js} +2 -2
  90. package/studio-dist/server/chunks/{StatBlock-DHJMmOCx.js → StatBlock-jNsxycS1.js} +2 -2
  91. package/studio-dist/server/chunks/{Tabs-DzG6Bn9a.js → Tabs-DQKDflnT.js} +2 -2
  92. package/studio-dist/server/chunks/_dispatch-DTWViLGL.js +145 -0
  93. package/studio-dist/server/chunks/{_layout.svelte-BEIGQcIv.js → _layout.svelte-CL6GfET2.js} +7 -9
  94. package/studio-dist/server/chunks/{_page.svelte-al08ovKm.js → _page.svelte-6iMiGR11.js} +5 -5
  95. package/studio-dist/server/chunks/{_page.svelte-Byz2djto.js → _page.svelte-B-YXGvAW.js} +5 -5
  96. package/studio-dist/server/chunks/{_page.svelte-tYN7_LXI.js → _page.svelte-B5E6RqVb.js} +9 -11
  97. package/studio-dist/server/chunks/{_page.svelte-UYiF7YBK.js → _page.svelte-BCRyjlk2.js} +5 -5
  98. package/studio-dist/server/chunks/{_page.svelte-CCoTp89s.js → _page.svelte-BoYafqft.js} +3 -3
  99. package/studio-dist/server/chunks/{_page.svelte-nzffIbBP.js → _page.svelte-C7Wm6_HB.js} +3 -3
  100. package/studio-dist/server/chunks/{_page.svelte-C_VSL8NW.js → _page.svelte-Cgqt7ucn.js} +13 -71
  101. package/studio-dist/server/chunks/{_page.svelte-CophUar3.js → _page.svelte-CqI6hqMm.js} +5 -5
  102. package/studio-dist/server/chunks/{_page.svelte-BgwL2Gyu.js → _page.svelte-CvQ2rZZ1.js} +11 -13
  103. package/studio-dist/server/chunks/{_page.svelte-l4Z_xjl9.js → _page.svelte-CvTyJEyS.js} +9 -11
  104. package/studio-dist/server/chunks/{_page.svelte-DOWw416I.js → _page.svelte-CvzAttO9.js} +11 -13
  105. package/studio-dist/server/chunks/{_page.svelte-DYNorA0S.js → _page.svelte-Cx9Ptecp.js} +10 -67
  106. package/studio-dist/server/chunks/{_page.svelte-Wfx7ww_h.js → _page.svelte-Cx9dPjsT.js} +11 -13
  107. package/studio-dist/server/chunks/{_page.svelte-2cgtvHwW.js → _page.svelte-DGN5j-At.js} +13 -71
  108. package/studio-dist/server/chunks/{_page.svelte-BVVTrzTL.js → _page.svelte-DaheDIRs.js} +5 -5
  109. package/studio-dist/server/chunks/{_page.svelte-M5D2QnMS.js → _page.svelte-Dm8y5a8S.js} +10 -67
  110. package/studio-dist/server/chunks/{_page.svelte-B_pq-Z98.js → _page.svelte-DqXxyNsF.js} +7 -7
  111. package/studio-dist/server/chunks/{_page.svelte-C3aIui4o.js → _page.svelte-DqsbnAhk.js} +4 -4
  112. package/studio-dist/server/chunks/{_page.svelte-ksTJUXce.js → _page.svelte-Dt0_c1T-.js} +3 -3
  113. package/studio-dist/server/chunks/{_page.svelte-bGhsrIjS.js → _page.svelte-Dv6iwhu7.js} +3 -3
  114. package/studio-dist/server/chunks/{_page.svelte-BXyoMME1.js → _page.svelte-GPiHej3P.js} +5 -5
  115. package/studio-dist/server/chunks/{_page.svelte-CYDjW1R0.js → _page.svelte-LuRm8XxE.js} +5 -5
  116. package/studio-dist/server/chunks/{_page.svelte-Clbkk1gm.js → _page.svelte-Z7nL9SC-.js} +8 -9
  117. package/studio-dist/server/chunks/{_page.svelte-DUWbZjC7.js → _page.svelte-_kntHS6v.js} +4 -4
  118. package/studio-dist/server/chunks/{_page.svelte-BIlhmugm.js → _page.svelte-eiomv6Pa.js} +11 -13
  119. package/studio-dist/server/chunks/{_page.svelte-9OQphlnm.js → _page.svelte-jXJbd8Mx.js} +14 -69
  120. package/studio-dist/server/chunks/{_page.svelte-CHzXuw2Q.js → _page.svelte-me5SUosG.js} +3 -3
  121. package/studio-dist/server/chunks/{_server.ts-DSzRdt1U.js → _server.ts--2m_U4SL.js} +11 -94
  122. package/studio-dist/server/chunks/{_server.ts-ujoDR2_g.js → _server.ts--sGLmBsz.js} +10 -92
  123. package/studio-dist/server/chunks/{_server.ts-k2fApwOp.js → _server.ts-0ukyuE0I.js} +5 -60
  124. package/studio-dist/server/chunks/_server.ts-1eW2mTeh.js +40 -0
  125. package/studio-dist/server/chunks/{_server.ts-Bfr4u3EP.js → _server.ts-3LzqLzt0.js} +10 -92
  126. package/studio-dist/server/chunks/_server.ts-40h9Y46Y.js +122 -0
  127. package/studio-dist/server/chunks/_server.ts-5euivmMj.js +38 -0
  128. package/studio-dist/server/chunks/{_server.ts-CShsBkeE.js → _server.ts-B0uoay97.js} +7 -23
  129. package/studio-dist/server/chunks/{_server.ts-hL92cV7Z.js → _server.ts-B5vYKkI6.js} +9 -63
  130. package/studio-dist/server/chunks/{_server.ts-_RJXtmfi.js → _server.ts-B8dTbPHd.js} +8 -23
  131. package/studio-dist/server/chunks/{_server.ts-C3Jo-ju8.js → _server.ts-BA8Xdegq.js} +10 -92
  132. package/studio-dist/server/chunks/_server.ts-BBClE0n9.js +54 -0
  133. package/studio-dist/server/chunks/{_server.ts-ZS1_yMwR.js → _server.ts-BDwMoyZ_.js} +6 -32
  134. package/studio-dist/server/chunks/{_server.ts-sv4pA808.js → _server.ts-BE-Uhx4N.js} +5 -46
  135. package/studio-dist/server/chunks/{_server.ts-DV6pzF-M.js → _server.ts-BSHADOUJ.js} +4 -3
  136. package/studio-dist/server/chunks/_server.ts-BcsqAqXt.js +67 -0
  137. package/studio-dist/server/chunks/_server.ts-Bl9tMqwZ.js +159 -0
  138. package/studio-dist/server/chunks/_server.ts-BlzDnRI0.js +51 -0
  139. package/studio-dist/server/chunks/{_server.ts-DnE0f-iC.js → _server.ts-BtuemCsf.js} +4 -3
  140. package/studio-dist/server/chunks/{_server.ts-BPcErnS0.js → _server.ts-C2nYqbd9.js} +10 -92
  141. package/studio-dist/server/chunks/{_server.ts-4p6cp9ke.js → _server.ts-C6IDM_3I.js} +10 -92
  142. package/studio-dist/server/chunks/_server.ts-C8Z32vuL.js +45 -0
  143. package/studio-dist/server/chunks/_server.ts-CBXyadsy.js +26 -0
  144. package/studio-dist/server/chunks/{_server.ts-Do9RIPRo.js → _server.ts-CE6h2ATE.js} +5 -32
  145. package/studio-dist/server/chunks/_server.ts-CH06JVqZ.js +65 -0
  146. package/studio-dist/server/chunks/_server.ts-CNqcws-M.js +29 -0
  147. package/studio-dist/server/chunks/{_server.ts-CJA1djzJ.js → _server.ts-CPf8necd.js} +8 -92
  148. package/studio-dist/server/chunks/{_server.ts-uUx7uT2Y.js → _server.ts-CYv5YuHd.js} +6 -43
  149. package/studio-dist/server/chunks/{_server.ts-B2E7fxo9.js → _server.ts-CZ5GZRWX.js} +6 -91
  150. package/studio-dist/server/chunks/{_server.ts-DSAv66wD.js → _server.ts-CbThsyhY.js} +4 -3
  151. package/studio-dist/server/chunks/{_server.ts-DgbY5EB3.js → _server.ts-CeQolQOK.js} +10 -92
  152. package/studio-dist/server/chunks/_server.ts-ChoGyX5C.js +61 -0
  153. package/studio-dist/server/chunks/_server.ts-CjJYkLJH.js +61 -0
  154. package/studio-dist/server/chunks/{_server.ts-lPlBUIJX.js → _server.ts-CtjArzxf.js} +5 -60
  155. package/studio-dist/server/chunks/{_server.ts-Cq60m8Pw.js → _server.ts-D3QWZwt_.js} +4 -3
  156. package/studio-dist/server/chunks/{_server.ts-CkoJ2HCW.js → _server.ts-D6wP5bCh.js} +10 -92
  157. package/studio-dist/server/chunks/{_server.ts-DwTRjS4y.js → _server.ts-D7JwQTun.js} +10 -93
  158. package/studio-dist/server/chunks/_server.ts-DKFJdXPK.js +97 -0
  159. package/studio-dist/server/chunks/_server.ts-DNAPGKEF.js +55 -0
  160. package/studio-dist/server/chunks/_server.ts-DQiTiY3E.js +78 -0
  161. package/studio-dist/server/chunks/{_server.ts-BT0Uq2yq.js → _server.ts-DUyeSXbV.js} +10 -92
  162. package/studio-dist/server/chunks/{_server.ts-DRVz9D-J.js → _server.ts-DY7BwZCv.js} +9 -24
  163. package/studio-dist/server/chunks/{_server.ts-Cz0zgaUV.js → _server.ts-D_E276ms.js} +3 -2
  164. package/studio-dist/server/chunks/{_server.ts-B5g3eScF.js → _server.ts-DgtmTAp5.js} +5 -60
  165. package/studio-dist/server/chunks/{_server.ts-CboRJY0Q.js → _server.ts-DhTKaw7h.js} +10 -63
  166. package/studio-dist/server/chunks/_server.ts-DqKBSCPT.js +31 -0
  167. package/studio-dist/server/chunks/{_server.ts-CpmMp5D8.js → _server.ts-DyXBNnRD.js} +4 -3
  168. package/studio-dist/server/chunks/_server.ts-DzjUpduu.js +25 -0
  169. package/studio-dist/server/chunks/{_server.ts-CGiAXy5i.js → _server.ts-Dznqw6Dg.js} +8 -23
  170. package/studio-dist/server/chunks/{_server.ts-tKMjUFUP.js → _server.ts-EVwS7UXb.js} +4 -3
  171. package/studio-dist/server/chunks/_server.ts-J17j25VG.js +62 -0
  172. package/studio-dist/server/chunks/{_server.ts-BdN4AJ6o.js → _server.ts-VQchZxXq.js} +10 -91
  173. package/studio-dist/server/chunks/{_server.ts-C4BcDGqS.js → _server.ts-XLV5SvjH.js} +7 -63
  174. package/studio-dist/server/chunks/_server.ts-jZgqFIJz.js +45 -0
  175. package/studio-dist/server/chunks/{_server.ts-Bw7A4zgb.js → _server.ts-lyiGGv7w.js} +5 -3
  176. package/studio-dist/server/chunks/{brain-CXR7ZT7S.js → brain-xt_NeshR.js} +2 -2
  177. package/studio-dist/server/chunks/cleo-home-_XIqkZaL.js +109 -0
  178. package/studio-dist/server/chunks/{cli-action-DQcaqcfn.js → cli-action-VJPEn1pY.js} +2 -2
  179. package/studio-dist/server/chunks/{client-CgX38JBu.js → client-D71MZ7Vi.js} +5 -6
  180. package/studio-dist/server/chunks/cluster-label-layer-CysRMvhi.js +6721 -0
  181. package/studio-dist/server/chunks/connections-CWLPjwBP.js +15 -0
  182. package/studio-dist/server/chunks/{connections2-mi7mZqJM.js → connections2-DHVs5ytW.js} +32 -19
  183. package/studio-dist/server/chunks/{dev-BNpEUZI3.js → dev-Br5zieNA.js} +2 -2
  184. package/studio-dist/server/chunks/dist-BzgWhfUR.js +18748 -0
  185. package/studio-dist/server/chunks/{error.svelte-CplNulnS.js → error.svelte-BIToUowC.js} +6 -8
  186. package/studio-dist/server/chunks/{explorer-loader-BN_mkt3_.js → explorer-loader-B_uCSs9_.js} +2 -2
  187. package/studio-dist/server/chunks/exports-Dg-gdPPq.js +124 -0
  188. package/studio-dist/server/chunks/{hooks.server-DBsyphiX.js → hooks.server-CWBRp8NI.js} +10 -93
  189. package/studio-dist/server/chunks/{http-CRTZe306.js → http-DPU3HHl2.js} +3 -4
  190. package/studio-dist/server/chunks/{index-server-DocJvixA.js → index-server-BgfA6C0S.js} +17 -4
  191. package/studio-dist/server/chunks/{internal-D6eWgsHJ.js → internal-DPnIJYMs.js} +56 -92
  192. package/studio-dist/server/chunks/{memory2-Cgc_4D5q.js → memory-BZHnDZzu.js} +4 -4
  193. package/studio-dist/server/chunks/{nexus-adapter-9EL_VlGI.js → nexus-adapter-D1ryYG1Z.js} +5 -5
  194. package/studio-dist/server/chunks/{no-face-up-DcejJLy-.js → no-face-up-B_3h6Hgs.js} +2 -2
  195. package/studio-dist/server/chunks/project-context-BAXQmgch.js +188 -0
  196. package/studio-dist/server/chunks/{src-BUqGet2c.js → src-D2rW9bIF.js} +422 -5
  197. package/studio-dist/server/chunks/{stores-5DmdjPCH.js → stores-BZdYTyl7.js} +3 -3
  198. package/studio-dist/server/chunks/{tasks2-Bf9VHrv-.js → tasks-sWuEmiGW.js} +2349 -9
  199. package/studio-dist/server/chunks/{ui-DR8zOSpC.js → ui-CkQ-pCs3.js} +2 -2
  200. package/studio-dist/server/chunks/utils-g564cUMV.js +151 -0
  201. package/studio-dist/server/index.js +5 -16
  202. package/studio-dist/server/manifest.js +88 -88
  203. package/dist/cli/index.js.map +0 -6
  204. package/studio-dist/client/_app/immutable/assets/parse-worker.CSEr67Qi.js +0 -1127
  205. package/studio-dist/client/_app/immutable/chunks/2mEksghu2.js +0 -1
  206. package/studio-dist/client/_app/immutable/chunks/3KebaI-z.js +0 -53
  207. package/studio-dist/client/_app/immutable/chunks/3y2yNYEk2.js +0 -3
  208. package/studio-dist/client/_app/immutable/chunks/7UXYlaI1.js +0 -1
  209. package/studio-dist/client/_app/immutable/chunks/83VLmCvk.js +0 -4
  210. package/studio-dist/client/_app/immutable/chunks/84Rhmi6r.js +0 -2
  211. package/studio-dist/client/_app/immutable/chunks/9CV9TXTg2.js +0 -92
  212. package/studio-dist/client/_app/immutable/chunks/9LFtcz24.js +0 -1
  213. package/studio-dist/client/_app/immutable/chunks/9bLMgKwu2.js +0 -1
  214. package/studio-dist/client/_app/immutable/chunks/9kKqohVR2.js +0 -37
  215. package/studio-dist/client/_app/immutable/chunks/B-Ye28EY.js +0 -7
  216. package/studio-dist/client/_app/immutable/chunks/B-jnUl7o.js +0 -1
  217. package/studio-dist/client/_app/immutable/chunks/B0ORuy8x2.js +0 -1
  218. package/studio-dist/client/_app/immutable/chunks/B0xCM1wN2.js +0 -1
  219. package/studio-dist/client/_app/immutable/chunks/B0zCN4sX.js +0 -1
  220. package/studio-dist/client/_app/immutable/chunks/B1wt-iGW.js +0 -7
  221. package/studio-dist/client/_app/immutable/chunks/B2F3KUIU.js +0 -1
  222. package/studio-dist/client/_app/immutable/chunks/B2_6RrlL2.js +0 -1
  223. package/studio-dist/client/_app/immutable/chunks/B2qKEYxA2.js +0 -1
  224. package/studio-dist/client/_app/immutable/chunks/B2u2rNyI2.js +0 -8
  225. package/studio-dist/client/_app/immutable/chunks/B3_X3Lhd2.js +0 -1
  226. package/studio-dist/client/_app/immutable/chunks/B3sNtbqf.js +0 -1
  227. package/studio-dist/client/_app/immutable/chunks/B4NQP1Ax2.js +0 -6
  228. package/studio-dist/client/_app/immutable/chunks/BAtZg5v5.js +0 -2
  229. package/studio-dist/client/_app/immutable/chunks/BB6WKLm82.js +0 -1
  230. package/studio-dist/client/_app/immutable/chunks/BBX8UDtn.js +0 -1
  231. package/studio-dist/client/_app/immutable/chunks/BBs1U1iU.js +0 -1
  232. package/studio-dist/client/_app/immutable/chunks/BEli_4Jp2.js +0 -1
  233. package/studio-dist/client/_app/immutable/chunks/BEt2wIUt.js +0 -1
  234. package/studio-dist/client/_app/immutable/chunks/BEyM8qSW2.js +0 -1
  235. package/studio-dist/client/_app/immutable/chunks/BFZA3Dsk2.js +0 -1
  236. package/studio-dist/client/_app/immutable/chunks/BGdPweG1.js +0 -2
  237. package/studio-dist/client/_app/immutable/chunks/BGiusSqE.js +0 -1
  238. package/studio-dist/client/_app/immutable/chunks/BGmpyfzD.js +0 -1
  239. package/studio-dist/client/_app/immutable/chunks/BH5rhSIR.js +0 -1
  240. package/studio-dist/client/_app/immutable/chunks/BHciFt922.js +0 -2
  241. package/studio-dist/client/_app/immutable/chunks/BHnFwTQY.js +0 -1
  242. package/studio-dist/client/_app/immutable/chunks/BIvye3XP2.js +0 -59
  243. package/studio-dist/client/_app/immutable/chunks/BJ8mcpQA2.js +0 -1
  244. package/studio-dist/client/_app/immutable/chunks/BK17qSHw.js +0 -2
  245. package/studio-dist/client/_app/immutable/chunks/BLkgUtyb.js +0 -1
  246. package/studio-dist/client/_app/immutable/chunks/BMUko3r62.js +0 -5
  247. package/studio-dist/client/_app/immutable/chunks/BMwzdEaa.js +0 -2
  248. package/studio-dist/client/_app/immutable/chunks/BQZx6brg.js +0 -2
  249. package/studio-dist/client/_app/immutable/chunks/BUPJTTTd.js +0 -2
  250. package/studio-dist/client/_app/immutable/chunks/BVNGAeZe2.js +0 -146
  251. package/studio-dist/client/_app/immutable/chunks/BWfLryGm.js +0 -1
  252. package/studio-dist/client/_app/immutable/chunks/BXIfeogw.js +0 -1
  253. package/studio-dist/client/_app/immutable/chunks/BXTfFyUU.js +0 -1
  254. package/studio-dist/client/_app/immutable/chunks/BYJC6_MM.js +0 -2
  255. package/studio-dist/client/_app/immutable/chunks/BZV1ZJzH.js +0 -2
  256. package/studio-dist/client/_app/immutable/chunks/BZu9rehd.js +0 -1
  257. package/studio-dist/client/_app/immutable/chunks/Bbzm48C52.js +0 -1
  258. package/studio-dist/client/_app/immutable/chunks/BfGnlwjH.js +0 -1
  259. package/studio-dist/client/_app/immutable/chunks/Bfjb9LPp.js +0 -2
  260. package/studio-dist/client/_app/immutable/chunks/BieFfu-R.js +0 -1
  261. package/studio-dist/client/_app/immutable/chunks/Bmv63uXe2.js +0 -1
  262. package/studio-dist/client/_app/immutable/chunks/BtBW1A0u.js +0 -1
  263. package/studio-dist/client/_app/immutable/chunks/BxISGQCl2.js +0 -1
  264. package/studio-dist/client/_app/immutable/chunks/ByNR_KvT.js +0 -2
  265. package/studio-dist/client/_app/immutable/chunks/BzjhD6Hr2.js +0 -2
  266. package/studio-dist/client/_app/immutable/chunks/BzpWnq0c2.js +0 -1
  267. package/studio-dist/client/_app/immutable/chunks/C-yJVcyI2.js +0 -1
  268. package/studio-dist/client/_app/immutable/chunks/C0cQ-8kS2.js +0 -1
  269. package/studio-dist/client/_app/immutable/chunks/C1vWmhYe.js +0 -48
  270. package/studio-dist/client/_app/immutable/chunks/C1w6bBje.js +0 -1
  271. package/studio-dist/client/_app/immutable/chunks/C243sn7r.js +0 -4
  272. package/studio-dist/client/_app/immutable/chunks/C2oZaDvt.js +0 -67
  273. package/studio-dist/client/_app/immutable/chunks/C3OziSQx.js +0 -1
  274. package/studio-dist/client/_app/immutable/chunks/C3hFPbMK.js +0 -2
  275. package/studio-dist/client/_app/immutable/chunks/C4P9oFvU2.js +0 -6
  276. package/studio-dist/client/_app/immutable/chunks/C5Nlanei.js +0 -8
  277. package/studio-dist/client/_app/immutable/chunks/C6KBAXPl.js +0 -15
  278. package/studio-dist/client/_app/immutable/chunks/C8MZnWta.js +0 -1
  279. package/studio-dist/client/_app/immutable/chunks/CAoan3K12.js +0 -1
  280. package/studio-dist/client/_app/immutable/chunks/CBpcINYH.js +0 -29
  281. package/studio-dist/client/_app/immutable/chunks/CE7phgWq.js +0 -1
  282. package/studio-dist/client/_app/immutable/chunks/CFb4Co_K.js +0 -2
  283. package/studio-dist/client/_app/immutable/chunks/CGWoRK3t2.js +0 -1
  284. package/studio-dist/client/_app/immutable/chunks/CH3t4xo1.js +0 -1
  285. package/studio-dist/client/_app/immutable/chunks/CHJgk6g-.js +0 -1
  286. package/studio-dist/client/_app/immutable/chunks/CHgPjuDQ.js +0 -17
  287. package/studio-dist/client/_app/immutable/chunks/CHkCZrnr.js +0 -1
  288. package/studio-dist/client/_app/immutable/chunks/CI_91WUN.js +0 -4
  289. package/studio-dist/client/_app/immutable/chunks/CJ7-hP0H.js +0 -1
  290. package/studio-dist/client/_app/immutable/chunks/CKIrNnkY2.js +0 -1
  291. package/studio-dist/client/_app/immutable/chunks/CKoQ7Ko5.js +0 -1
  292. package/studio-dist/client/_app/immutable/chunks/CM8O4zqs.js +0 -56
  293. package/studio-dist/client/_app/immutable/chunks/CMz3AVRO.js +0 -1
  294. package/studio-dist/client/_app/immutable/chunks/CNToUR3P.js +0 -13
  295. package/studio-dist/client/_app/immutable/chunks/CNuQG1yl2.js +0 -1
  296. package/studio-dist/client/_app/immutable/chunks/CQLIZtxj.js +0 -2
  297. package/studio-dist/client/_app/immutable/chunks/CQYVNgM0.js +0 -20
  298. package/studio-dist/client/_app/immutable/chunks/CQeVOmFO2.js +0 -1
  299. package/studio-dist/client/_app/immutable/chunks/CSPHHq09.js +0 -1
  300. package/studio-dist/client/_app/immutable/chunks/CTJEdbIn.js +0 -1
  301. package/studio-dist/client/_app/immutable/chunks/CTTT3wYw.js +0 -35
  302. package/studio-dist/client/_app/immutable/chunks/CV4ck7fz.js +0 -2
  303. package/studio-dist/client/_app/immutable/chunks/CXQrVnHf.js +0 -11
  304. package/studio-dist/client/_app/immutable/chunks/CXTCtOUQ.js +0 -1
  305. package/studio-dist/client/_app/immutable/chunks/C_CsU7Qq.js +0 -1
  306. package/studio-dist/client/_app/immutable/chunks/C_IZx3682.js +0 -2
  307. package/studio-dist/client/_app/immutable/chunks/CbTnu2Po2.js +0 -1
  308. package/studio-dist/client/_app/immutable/chunks/Cba8RZ1a2.js +0 -8
  309. package/studio-dist/client/_app/immutable/chunks/Cc8xxDr5.js +0 -1
  310. package/studio-dist/client/_app/immutable/chunks/CdPxoldu.js +0 -1
  311. package/studio-dist/client/_app/immutable/chunks/CdqDTozW.js +0 -1
  312. package/studio-dist/client/_app/immutable/chunks/CdsmcCfC.js +0 -25
  313. package/studio-dist/client/_app/immutable/chunks/CeYPAoY9.js +0 -1
  314. package/studio-dist/client/_app/immutable/chunks/Cf4kGS9y.js +0 -1
  315. package/studio-dist/client/_app/immutable/chunks/CfWEqXtJ2.js +0 -1
  316. package/studio-dist/client/_app/immutable/chunks/Cg7Wj2b9.js +0 -2
  317. package/studio-dist/client/_app/immutable/chunks/CgTpKZfi2.js +0 -1
  318. package/studio-dist/client/_app/immutable/chunks/Chpy9ca7.js +0 -2
  319. package/studio-dist/client/_app/immutable/chunks/CiLKuLrQ.js +0 -4
  320. package/studio-dist/client/_app/immutable/chunks/CjNDJjwu.js +0 -1
  321. package/studio-dist/client/_app/immutable/chunks/Cjde0ZKr2.js +0 -1
  322. package/studio-dist/client/_app/immutable/chunks/Cjgnade02.js +0 -3
  323. package/studio-dist/client/_app/immutable/chunks/Ckp90om52.js +0 -2
  324. package/studio-dist/client/_app/immutable/chunks/ClAPVhCD.js +0 -1
  325. package/studio-dist/client/_app/immutable/chunks/ClT89UdO.js +0 -11
  326. package/studio-dist/client/_app/immutable/chunks/CnJf5tML.js +0 -1
  327. package/studio-dist/client/_app/immutable/chunks/CpF4Rrcc2.js +0 -1
  328. package/studio-dist/client/_app/immutable/chunks/CqCka5yX2.js +0 -1
  329. package/studio-dist/client/_app/immutable/chunks/CqtqZrOl.js +0 -1
  330. package/studio-dist/client/_app/immutable/chunks/CrJdywQt.js +0 -1
  331. package/studio-dist/client/_app/immutable/chunks/Cs0zC2l2.js +0 -1
  332. package/studio-dist/client/_app/immutable/chunks/CsMn5fBs.js +0 -141
  333. package/studio-dist/client/_app/immutable/chunks/CsvKLHQQ2.js +0 -2
  334. package/studio-dist/client/_app/immutable/chunks/Ctaresvj.js +0 -1
  335. package/studio-dist/client/_app/immutable/chunks/Ctetf8vc2.js +0 -2
  336. package/studio-dist/client/_app/immutable/chunks/Cv7yW6MG.js +0 -1
  337. package/studio-dist/client/_app/immutable/chunks/CxRIz-Zd.js +0 -1
  338. package/studio-dist/client/_app/immutable/chunks/CxbvtjIH.js +0 -3
  339. package/studio-dist/client/_app/immutable/chunks/CxfeoXkZ.js +0 -2
  340. package/studio-dist/client/_app/immutable/chunks/CySLscL2.js +0 -2
  341. package/studio-dist/client/_app/immutable/chunks/D0PKQAbM.js +0 -87
  342. package/studio-dist/client/_app/immutable/chunks/D0WQsLTu.js +0 -1
  343. package/studio-dist/client/_app/immutable/chunks/D1LBVx6r2.js +0 -1
  344. package/studio-dist/client/_app/immutable/chunks/D1NlFXu9.js +0 -1
  345. package/studio-dist/client/_app/immutable/chunks/D1QfC4hu2.js +0 -1
  346. package/studio-dist/client/_app/immutable/chunks/D1yrHAn72.js +0 -1
  347. package/studio-dist/client/_app/immutable/chunks/D2pa4reh.js +0 -7
  348. package/studio-dist/client/_app/immutable/chunks/D3QsWcIw2.js +0 -2
  349. package/studio-dist/client/_app/immutable/chunks/D4OkQuPl2.js +0 -1
  350. package/studio-dist/client/_app/immutable/chunks/D6UDnATk2.js +0 -1
  351. package/studio-dist/client/_app/immutable/chunks/D7w7HllH.js +0 -38
  352. package/studio-dist/client/_app/immutable/chunks/D93KpglN.js +0 -1
  353. package/studio-dist/client/_app/immutable/chunks/D99fQj2G2.js +0 -1
  354. package/studio-dist/client/_app/immutable/chunks/DB5ifLM-.js +0 -3
  355. package/studio-dist/client/_app/immutable/chunks/DBhP8ifO2.js +0 -15
  356. package/studio-dist/client/_app/immutable/chunks/DF_4OWhj.js +0 -2
  357. package/studio-dist/client/_app/immutable/chunks/DFpNK1vs2.js +0 -1
  358. package/studio-dist/client/_app/immutable/chunks/DGRYMk-W.js +0 -142
  359. package/studio-dist/client/_app/immutable/chunks/DH3CcQwq.js +0 -1
  360. package/studio-dist/client/_app/immutable/chunks/DHQfJeLU.js +0 -4
  361. package/studio-dist/client/_app/immutable/chunks/DL89yEDy2.js +0 -1
  362. package/studio-dist/client/_app/immutable/chunks/DLRku_0p2.js +0 -2
  363. package/studio-dist/client/_app/immutable/chunks/DMZkjPYT2.js +0 -1
  364. package/studio-dist/client/_app/immutable/chunks/DNP72foU.js +0 -1
  365. package/studio-dist/client/_app/immutable/chunks/DO2GztVm.js +0 -1
  366. package/studio-dist/client/_app/immutable/chunks/DOVNXmCK2.js +0 -1
  367. package/studio-dist/client/_app/immutable/chunks/DOqzs8kl2.js +0 -1
  368. package/studio-dist/client/_app/immutable/chunks/DQnR63hh2.js +0 -7
  369. package/studio-dist/client/_app/immutable/chunks/DTxtWPkw.js +0 -1
  370. package/studio-dist/client/_app/immutable/chunks/DVDgCC9A2.js +0 -20
  371. package/studio-dist/client/_app/immutable/chunks/DVVCJYjz.js +0 -1
  372. package/studio-dist/client/_app/immutable/chunks/DWRhYUMj2.js +0 -1
  373. package/studio-dist/client/_app/immutable/chunks/DWavBJBI.js +0 -4
  374. package/studio-dist/client/_app/immutable/chunks/DXDSWRw-.js +0 -1
  375. package/studio-dist/client/_app/immutable/chunks/DXoHaH4a.js +0 -2
  376. package/studio-dist/client/_app/immutable/chunks/DXpCX2Hi.js +0 -1
  377. package/studio-dist/client/_app/immutable/chunks/DYP3EJMY.js +0 -1
  378. package/studio-dist/client/_app/immutable/chunks/DYQphR_V.js +0 -1
  379. package/studio-dist/client/_app/immutable/chunks/DYUlIivP.js +0 -1
  380. package/studio-dist/client/_app/immutable/chunks/DYo2ye8X2.js +0 -1
  381. package/studio-dist/client/_app/immutable/chunks/DYxFu8yV.js +0 -1
  382. package/studio-dist/client/_app/immutable/chunks/DZ66sRb6.js +0 -1
  383. package/studio-dist/client/_app/immutable/chunks/D_XTiylm2.js +0 -1
  384. package/studio-dist/client/_app/immutable/chunks/DaRv9jn22.js +0 -1
  385. package/studio-dist/client/_app/immutable/chunks/DaXb9i_E.js +0 -1
  386. package/studio-dist/client/_app/immutable/chunks/Dccf4VP2.js +0 -24
  387. package/studio-dist/client/_app/immutable/chunks/DdODs-Sb.js +0 -1
  388. package/studio-dist/client/_app/immutable/chunks/DdekYWer.js +0 -2
  389. package/studio-dist/client/_app/immutable/chunks/DeHaovsF.js +0 -1
  390. package/studio-dist/client/_app/immutable/chunks/DeRRkRO52.js +0 -1
  391. package/studio-dist/client/_app/immutable/chunks/DeWI5qCn.js +0 -1
  392. package/studio-dist/client/_app/immutable/chunks/DfivR-Md.js +0 -1
  393. package/studio-dist/client/_app/immutable/chunks/DgmxpSo7.js +0 -1
  394. package/studio-dist/client/_app/immutable/chunks/DhPLWrun2.js +0 -1
  395. package/studio-dist/client/_app/immutable/chunks/DhhrVdfi.js +0 -2
  396. package/studio-dist/client/_app/immutable/chunks/Djr2dbcc2.js +0 -37
  397. package/studio-dist/client/_app/immutable/chunks/DjuRo5vv.js +0 -1
  398. package/studio-dist/client/_app/immutable/chunks/Djz4EDzO.js +0 -1
  399. package/studio-dist/client/_app/immutable/chunks/DkRfaw-K2.js +0 -1
  400. package/studio-dist/client/_app/immutable/chunks/DksB5g4W2.js +0 -1
  401. package/studio-dist/client/_app/immutable/chunks/Dmqenb5z2.js +0 -1
  402. package/studio-dist/client/_app/immutable/chunks/DnbToVyf.js +0 -2
  403. package/studio-dist/client/_app/immutable/chunks/DpGZwlmj.js +0 -1
  404. package/studio-dist/client/_app/immutable/chunks/Dq0lE5C32.js +0 -1
  405. package/studio-dist/client/_app/immutable/chunks/Dq54XqHT2.js +0 -1
  406. package/studio-dist/client/_app/immutable/chunks/DqK84mbb.js +0 -1
  407. package/studio-dist/client/_app/immutable/chunks/DqjFtSJD.js +0 -2
  408. package/studio-dist/client/_app/immutable/chunks/DrKWWDpL.js +0 -1
  409. package/studio-dist/client/_app/immutable/chunks/DsQIgu-4.js +0 -1
  410. package/studio-dist/client/_app/immutable/chunks/DsqdgJHS2.js +0 -1
  411. package/studio-dist/client/_app/immutable/chunks/DtNkZUkq.js +0 -17
  412. package/studio-dist/client/_app/immutable/chunks/DutIls4V2.js +0 -1
  413. package/studio-dist/client/_app/immutable/chunks/DvxYFW4C.js +0 -1
  414. package/studio-dist/client/_app/immutable/chunks/Dwp-76GQ.js +0 -2
  415. package/studio-dist/client/_app/immutable/chunks/DxSZd548.js +0 -1
  416. package/studio-dist/client/_app/immutable/chunks/DxiUMhk0.js +0 -5
  417. package/studio-dist/client/_app/immutable/chunks/DxslLswf.js +0 -23
  418. package/studio-dist/client/_app/immutable/chunks/Dy7EjGgb2.js +0 -9
  419. package/studio-dist/client/_app/immutable/chunks/DyGQ_VXC.js +0 -3
  420. package/studio-dist/client/_app/immutable/chunks/DySe3kTV2.js +0 -1
  421. package/studio-dist/client/_app/immutable/chunks/DyzE2Uxw.js +0 -1
  422. package/studio-dist/client/_app/immutable/chunks/DzhuJaxE2.js +0 -2
  423. package/studio-dist/client/_app/immutable/chunks/FmMHyrh6.js +0 -1
  424. package/studio-dist/client/_app/immutable/chunks/Gxs0J274.js +0 -5
  425. package/studio-dist/client/_app/immutable/chunks/HwtzDYxN.js +0 -2
  426. package/studio-dist/client/_app/immutable/chunks/ITRz_Q7H2.js +0 -15
  427. package/studio-dist/client/_app/immutable/chunks/KDNP98XY2.js +0 -1
  428. package/studio-dist/client/_app/immutable/chunks/LmVjFhpk2.js +0 -1
  429. package/studio-dist/client/_app/immutable/chunks/MiAejGV_.js +0 -1
  430. package/studio-dist/client/_app/immutable/chunks/Ml4JR6Jz2.js +0 -2
  431. package/studio-dist/client/_app/immutable/chunks/NU5S3nNs2.js +0 -2
  432. package/studio-dist/client/_app/immutable/chunks/Oh5zPjv3.js +0 -2
  433. package/studio-dist/client/_app/immutable/chunks/QZkvJ9vi.js +0 -1
  434. package/studio-dist/client/_app/immutable/chunks/Rbn8UpBm.js +0 -1
  435. package/studio-dist/client/_app/immutable/chunks/SPX3gYwn2.js +0 -1
  436. package/studio-dist/client/_app/immutable/chunks/TX-23QW3.js +0 -3
  437. package/studio-dist/client/_app/immutable/chunks/UyeQuL132.js +0 -12
  438. package/studio-dist/client/_app/immutable/chunks/VQszoXcV.js +0 -1
  439. package/studio-dist/client/_app/immutable/chunks/VtZsEMxD.js +0 -1
  440. package/studio-dist/client/_app/immutable/chunks/W2IeK61p2.js +0 -1
  441. package/studio-dist/client/_app/immutable/chunks/Wadww7ZG.js +0 -1
  442. package/studio-dist/client/_app/immutable/chunks/WgfA1zWk2.js +0 -1
  443. package/studio-dist/client/_app/immutable/chunks/WxIgAS1f.js +0 -21
  444. package/studio-dist/client/_app/immutable/chunks/XrsBoOj9.js +0 -2
  445. package/studio-dist/client/_app/immutable/chunks/Y3PX4poR.js +0 -2
  446. package/studio-dist/client/_app/immutable/chunks/YxfwqiF32.js +0 -1
  447. package/studio-dist/client/_app/immutable/chunks/Z7uO1YaO.js +0 -1
  448. package/studio-dist/client/_app/immutable/chunks/_4ykQk_8.js +0 -1
  449. package/studio-dist/client/_app/immutable/chunks/_kRcZFVn.js +0 -1
  450. package/studio-dist/client/_app/immutable/chunks/aQNM7eVA2.js +0 -1
  451. package/studio-dist/client/_app/immutable/chunks/bHKz_Fb2.js +0 -45
  452. package/studio-dist/client/_app/immutable/chunks/bJRyRGSZ.js +0 -1
  453. package/studio-dist/client/_app/immutable/chunks/dlzF6lnR.js +0 -1
  454. package/studio-dist/client/_app/immutable/chunks/fjRbpfEr2.js +0 -26
  455. package/studio-dist/client/_app/immutable/chunks/imLk4if5.js +0 -3
  456. package/studio-dist/client/_app/immutable/chunks/lgRG0R-32.js +0 -1
  457. package/studio-dist/client/_app/immutable/chunks/mFhCjxQJ2.js +0 -1
  458. package/studio-dist/client/_app/immutable/chunks/mgnH6qYg.js +0 -1
  459. package/studio-dist/client/_app/immutable/chunks/n6M_5LoW.js +0 -1
  460. package/studio-dist/client/_app/immutable/chunks/nLIOiDZp2.js +0 -1
  461. package/studio-dist/client/_app/immutable/chunks/nab7WB3u2.js +0 -1
  462. package/studio-dist/client/_app/immutable/chunks/orUmAKoW2.js +0 -1
  463. package/studio-dist/client/_app/immutable/chunks/p-fLQDfY2.js +0 -63
  464. package/studio-dist/client/_app/immutable/chunks/s0-WkOGk2.js +0 -1
  465. package/studio-dist/client/_app/immutable/chunks/sPuXfbUF.js +0 -1
  466. package/studio-dist/client/_app/immutable/chunks/t4h0vrqR2.js +0 -2
  467. package/studio-dist/client/_app/immutable/chunks/uE83FKL-.js +0 -1
  468. package/studio-dist/client/_app/immutable/chunks/uzhXlm0g2.js +0 -1
  469. package/studio-dist/client/_app/immutable/chunks/vTrEsGlD2.js +0 -1
  470. package/studio-dist/client/_app/immutable/chunks/xJgCk3dx2.js +0 -28
  471. package/studio-dist/client/_app/immutable/chunks/z0Mt579s2.js +0 -13
  472. package/studio-dist/client/_app/immutable/entry/app.BoQ7cgT2.js +0 -2
  473. package/studio-dist/client/_app/immutable/entry/start.Cg1v8DU4.js +0 -1
  474. package/studio-dist/client/_app/immutable/nodes/25.ddZ1_EFk.js +0 -1
  475. package/studio-dist/client/_app/immutable/nodes/26.6OcmQbLV.js +0 -1
  476. package/studio-dist/client/_app/immutable/nodes/4.CCjOEwVU.js +0 -1
  477. package/studio-dist/server/chunks/0-CvsKDxQ6.js +0 -126
  478. package/studio-dist/server/chunks/1-ck_x2Hf0.js +0 -9
  479. package/studio-dist/server/chunks/20-DVU22np7.js +0 -142
  480. package/studio-dist/server/chunks/23-D_tUnnoc.js +0 -181
  481. package/studio-dist/server/chunks/24-CVlymvLF.js +0 -151
  482. package/studio-dist/server/chunks/AwsRestJsonProtocol-BZuoNBmL.js +0 -416
  483. package/studio-dist/server/chunks/UnionSerde-BflIkhKj.js +0 -2653
  484. package/studio-dist/server/chunks/_dispatch-ncuRztUQ.js +0 -5597
  485. package/studio-dist/server/chunks/_server.ts-4NyKAneX.js +0 -116
  486. package/studio-dist/server/chunks/_server.ts-7hGdxdEI.js +0 -136
  487. package/studio-dist/server/chunks/_server.ts-B6b3b-yA.js +0 -131
  488. package/studio-dist/server/chunks/_server.ts-BOgGqaZp.js +0 -144
  489. package/studio-dist/server/chunks/_server.ts-BV1HiKZ0.js +0 -120
  490. package/studio-dist/server/chunks/_server.ts-BWp3g6Cr.js +0 -110
  491. package/studio-dist/server/chunks/_server.ts-C5NF-UgC.js +0 -122
  492. package/studio-dist/server/chunks/_server.ts-CDMurH5m.js +0 -2301
  493. package/studio-dist/server/chunks/_server.ts-COJHCgbq.js +0 -127
  494. package/studio-dist/server/chunks/_server.ts-CUl-Utkt.js +0 -148
  495. package/studio-dist/server/chunks/_server.ts-D0GAFJRb.js +0 -156
  496. package/studio-dist/server/chunks/_server.ts-DA6p3pmi.js +0 -117
  497. package/studio-dist/server/chunks/_server.ts-DThYFaZj.js +0 -160
  498. package/studio-dist/server/chunks/_server.ts-D_3Pm0gr.js +0 -179
  499. package/studio-dist/server/chunks/_server.ts-DcHxdMsm.js +0 -144
  500. package/studio-dist/server/chunks/_server.ts-Dlfadh2I.js +0 -144
  501. package/studio-dist/server/chunks/_server.ts-JallJnps.js +0 -240
  502. package/studio-dist/server/chunks/_server.ts-NEFTbJpM.js +0 -142
  503. package/studio-dist/server/chunks/_server.ts-d-2NvRwX.js +0 -122
  504. package/studio-dist/server/chunks/_server.ts-rUWMLkIw.js +0 -146
  505. package/studio-dist/server/chunks/abort-events-CDRAJSqb.js +0 -30
  506. package/studio-dist/server/chunks/adapters-BkMgPNUM.js +0 -359
  507. package/studio-dist/server/chunks/adapters2-CQ3PzoaK.js +0 -28
  508. package/studio-dist/server/chunks/api-mode2-3W-I1-XO.js +0 -36
  509. package/studio-dist/server/chunks/api2-CmNQzb0b.js +0 -96
  510. package/studio-dist/server/chunks/attention-consolidate-CidG1jfH.js +0 -485
  511. package/studio-dist/server/chunks/audit3-DMO_fUGp.js +0 -52
  512. package/studio-dist/server/chunks/auto-extract-B84La1Vs.js +0 -34
  513. package/studio-dist/server/chunks/brain-consolidator-JGm6mzFf.js +0 -373
  514. package/studio-dist/server/chunks/brain-doctor-Dc2WqVbs.js +0 -311
  515. package/studio-dist/server/chunks/brain-embedding-Djw9ekXu.js +0 -42
  516. package/studio-dist/server/chunks/brain-lifecycle-BOOa876K.js +0 -969
  517. package/studio-dist/server/chunks/brain-maintenance-U6VvsvmK.js +0 -133
  518. package/studio-dist/server/chunks/brain-maintenance2-CwVSmfDT.js +0 -43
  519. package/studio-dist/server/chunks/brain-retrieval2-exR8mdgh.js +0 -751
  520. package/studio-dist/server/chunks/brain-search-CpSh-SNi.js +0 -48
  521. package/studio-dist/server/chunks/brain-search2-CwI2cXgg.js +0 -716
  522. package/studio-dist/server/chunks/brain-similarity-DI4yzm9e.js +0 -113
  523. package/studio-dist/server/chunks/brain-stdp2-BxSBIlSr.js +0 -960
  524. package/studio-dist/server/chunks/brain-writer-handlers-Dwu0QCAA.js +0 -188
  525. package/studio-dist/server/chunks/brain-writer-thread-6n7wI7Gy.js +0 -360
  526. package/studio-dist/server/chunks/branch-lock-Cq7zzmFK.js +0 -1118
  527. package/studio-dist/server/chunks/branch-lock2-CmDc4CZW.js +0 -45
  528. package/studio-dist/server/chunks/cancelled-child-waiver-audit-DkesrN83.js +0 -54
  529. package/studio-dist/server/chunks/catalog-model-resolver-Bas9qnoP.js +0 -9
  530. package/studio-dist/server/chunks/catalog-model-resolver2-Cexk2TN0.js +0 -237
  531. package/studio-dist/server/chunks/catalog-resolver-CLot3XJO.js +0 -260
  532. package/studio-dist/server/chunks/catalog-resolver2-JmKb91kY.js +0 -33
  533. package/studio-dist/server/chunks/chunk-DDP5xfzh.js +0 -39
  534. package/studio-dist/server/chunks/cleo-home-CCfe6kAQ.js +0 -64
  535. package/studio-dist/server/chunks/concrete-session2-bb_vxJ2N.js +0 -43
  536. package/studio-dist/server/chunks/config-DHj9CcFs.js +0 -386
  537. package/studio-dist/server/chunks/config2-F6PPIn7D.js +0 -35
  538. package/studio-dist/server/chunks/connections-BMGJVYcS.js +0 -98
  539. package/studio-dist/server/chunks/credential-pool-vF6NdtT9.js +0 -4600
  540. package/studio-dist/server/chunks/credentials-C7EJFf5O.js +0 -35
  541. package/studio-dist/server/chunks/credentials-store-CFygIuB2.js +0 -35
  542. package/studio-dist/server/chunks/cross-db-cleanup-1LWTUKPi.js +0 -147
  543. package/studio-dist/server/chunks/data-accessor-CWgVndbR.js +0 -72
  544. package/studio-dist/server/chunks/data-safety-central-DaFxK27G.js +0 -683
  545. package/studio-dist/server/chunks/decisions-CmVXV2-H.js +0 -574
  546. package/studio-dist/server/chunks/decisions2-XQsSYdxa.js +0 -48
  547. package/studio-dist/server/chunks/depends-waiver-audit-CUmlBYHx.js +0 -56
  548. package/studio-dist/server/chunks/dialectic-evaluator2-BOyyXhdI.js +0 -96
  549. package/studio-dist/server/chunks/dist-es2-DQy2Mjn_.js +0 -43
  550. package/studio-dist/server/chunks/dist-es3-CBygU4n5.js +0 -365
  551. package/studio-dist/server/chunks/dist-es4-CavbNu4B.js +0 -162
  552. package/studio-dist/server/chunks/dist-es5-Crpm8Wsl.js +0 -465
  553. package/studio-dist/server/chunks/dist-es6-CgePHzh4.js +0 -99
  554. package/studio-dist/server/chunks/dist-es7-k6c5GEd7.js +0 -311
  555. package/studio-dist/server/chunks/dist-es8-Bd9v0_pE.js +0 -91
  556. package/studio-dist/server/chunks/dist3-23bsJhRw.js +0 -27
  557. package/studio-dist/server/chunks/dist4-DmC1zSXi.js +0 -35618
  558. package/studio-dist/server/chunks/dual-scope-db-DKnWu1KM.js +0 -5672
  559. package/studio-dist/server/chunks/dual-scope-db2-Dht3luRw.js +0 -31
  560. package/studio-dist/server/chunks/duplicate-bypass-audit-BN5w1H2y.js +0 -36
  561. package/studio-dist/server/chunks/duplicate-detector-CaVFMKdE.js +0 -692
  562. package/studio-dist/server/chunks/edge-types-CgBvb0w1.js +0 -28
  563. package/studio-dist/server/chunks/embedding-local-xEnym8Ho.js +0 -92
  564. package/studio-dist/server/chunks/event-streams-Bi2p_fVo.js +0 -232
  565. package/studio-dist/server/chunks/execution-learning-BVCw7Plq.js +0 -278
  566. package/studio-dist/server/chunks/exodus-BBuuN6aA.js +0 -2594
  567. package/studio-dist/server/chunks/externalDataInterceptor-C3C4cObe.js +0 -21
  568. package/studio-dist/server/chunks/extraction-gate-DDNBq7gu.js +0 -48
  569. package/studio-dist/server/chunks/extraction-gate2-D77EZBQ6.js +0 -375
  570. package/studio-dist/server/chunks/getSSOTokenFromFile-yZAJxDci.js +0 -21
  571. package/studio-dist/server/chunks/governor2-CtfV3wfX.js +0 -923
  572. package/studio-dist/server/chunks/graph-auto-populate-Dmsr9ZJM.js +0 -42
  573. package/studio-dist/server/chunks/graph-auto-populate2-Cz0qeebM.js +0 -156
  574. package/studio-dist/server/chunks/graph-memory-bridge-CY8ft_d7.js +0 -43
  575. package/studio-dist/server/chunks/graph-memory-bridge2-Cd0XZwqc.js +0 -429
  576. package/studio-dist/server/chunks/handler-helpers-AANBGA_p.js +0 -49
  577. package/studio-dist/server/chunks/hash-Cm6yMET4.js +0 -14
  578. package/studio-dist/server/chunks/health-monitor-FepUZN1m.js +0 -106
  579. package/studio-dist/server/chunks/identity-DRpurt56.js +0 -30
  580. package/studio-dist/server/chunks/identity2-DeAB5RXE.js +0 -114
  581. package/studio-dist/server/chunks/index-61916rmV.js +0 -632
  582. package/studio-dist/server/chunks/index-DAgVDouG.js +0 -268
  583. package/studio-dist/server/chunks/index-Dtt_h58T.js +0 -2116
  584. package/studio-dist/server/chunks/indexes-DZ52grq6.js +0 -615
  585. package/studio-dist/server/chunks/json-Bfixam3g.js +0 -126
  586. package/studio-dist/server/chunks/learnings-D6Y3YGMU.js +0 -47
  587. package/studio-dist/server/chunks/learnings2-MwBdR9vb.js +0 -171
  588. package/studio-dist/server/chunks/lifecycle-ops2-V2CR5-7X.js +0 -121
  589. package/studio-dist/server/chunks/list-BhObczvn.js +0 -229
  590. package/studio-dist/server/chunks/living-brain2-j0B_Ir8j.js +0 -285
  591. package/studio-dist/server/chunks/llm-backend-resolver-D4ANvUPL.js +0 -295
  592. package/studio-dist/server/chunks/llm-extraction-DVCu4heR.js +0 -408
  593. package/studio-dist/server/chunks/loadSharedConfigFiles-umvW7kuz.js +0 -73
  594. package/studio-dist/server/chunks/loadSso-iidd5iKx.js +0 -599
  595. package/studio-dist/server/chunks/local-model-fit-B0TxifnX.js +0 -53
  596. package/studio-dist/server/chunks/local-model-fit2-BMqo6GJm.js +0 -2664
  597. package/studio-dist/server/chunks/lock-CA7d6lj7.js +0 -57
  598. package/studio-dist/server/chunks/logger-DQIoyZbq.js +0 -34
  599. package/studio-dist/server/chunks/memory-BUbicBbE.js +0 -618
  600. package/studio-dist/server/chunks/memory-accessor-i3P-RHSg.js +0 -42
  601. package/studio-dist/server/chunks/memory-accessor2-B981tHDR.js +0 -467
  602. package/studio-dist/server/chunks/memory-bridge-BOlEWg9w.js +0 -424
  603. package/studio-dist/server/chunks/memory-sqlite2-B7dq8doO.js +0 -42
  604. package/studio-dist/server/chunks/model-runner2-B71xjp4y.js +0 -43
  605. package/studio-dist/server/chunks/mvi-helpers-BNsJ8ZpQ.js +0 -59
  606. package/studio-dist/server/chunks/nexus-plasticity-BTfU8cW_.js +0 -269
  607. package/studio-dist/server/chunks/nexus-risk-audit-p379-TU5.js +0 -54
  608. package/studio-dist/server/chunks/nexus-schema-zeygY8OD.js +0 -11
  609. package/studio-dist/server/chunks/nexus-schema2-DP0Qd7CA.js +0 -444
  610. package/studio-dist/server/chunks/nexus-sqlite-BVNIpwc7.js +0 -36
  611. package/studio-dist/server/chunks/nexus-sqlite2-BBfY2xnY.js +0 -1103
  612. package/studio-dist/server/chunks/node-http-handler-CJI8kn7X.js +0 -386
  613. package/studio-dist/server/chunks/observe-Bo9veHGe.js +0 -47
  614. package/studio-dist/server/chunks/observe2-wzO0MaMY.js +0 -245
  615. package/studio-dist/server/chunks/observer-reflector-BwNOuq1d.js +0 -62
  616. package/studio-dist/server/chunks/observer-reflector2-ggtC1hmk.js +0 -478
  617. package/studio-dist/server/chunks/on-open-Wpcw7DV4.js +0 -740
  618. package/studio-dist/server/chunks/open-cleo-db--8sYNaYY.js +0 -70
  619. package/studio-dist/server/chunks/parseKnownFiles-Dg0pMFcT.js +0 -18
  620. package/studio-dist/server/chunks/parseUrl-qgGfl-Vh.js +0 -120
  621. package/studio-dist/server/chunks/paths-CV7S5-Zm.js +0 -30012
  622. package/studio-dist/server/chunks/patterns-lh5kn13I.js +0 -48
  623. package/studio-dist/server/chunks/patterns2-CzIcT-ai.js +0 -177
  624. package/studio-dist/server/chunks/pipeline-stage-IB_pOft-.js +0 -63
  625. package/studio-dist/server/chunks/prediction-D1lSGJ4c.js +0 -217
  626. package/studio-dist/server/chunks/premature-close-audit-VG09uAco.js +0 -53
  627. package/studio-dist/server/chunks/project-context-BjiqfEBi.js +0 -166
  628. package/studio-dist/server/chunks/provider-registry2-Cu7rAF0R.js +0 -35
  629. package/studio-dist/server/chunks/quality-feedback-CuCGEI6S.js +0 -371
  630. package/studio-dist/server/chunks/quality-scoring-CCPuRQmJ.js +0 -160
  631. package/studio-dist/server/chunks/query-ops-DCrjbZb9.js +0 -78
  632. package/studio-dist/server/chunks/query-promise-BkPmGPJy.js +0 -1257
  633. package/studio-dist/server/chunks/readFile-CxwXhymG.js +0 -31
  634. package/studio-dist/server/chunks/registry-CL2gN1zd.js +0 -27
  635. package/studio-dist/server/chunks/registry2-DoPYJnUW.js +0 -233
  636. package/studio-dist/server/chunks/registry3-BajEb-ll.js +0 -42
  637. package/studio-dist/server/chunks/registry4-DCqffZkT.js +0 -142
  638. package/studio-dist/server/chunks/retry-B3oEKYpp.js +0 -105
  639. package/studio-dist/server/chunks/role-executor-CC8BU54L.js +0 -25379
  640. package/studio-dist/server/chunks/role-resolver-D9QaDT0e.js +0 -6544
  641. package/studio-dist/server/chunks/role-resolver2-nkhzS1oy.js +0 -37
  642. package/studio-dist/server/chunks/rollup-CiefjN_Y.js +0 -194
  643. package/studio-dist/server/chunks/safety-data-accessor-CniDmGJO.js +0 -276
  644. package/studio-dist/server/chunks/service-connections-accessor-yBCdcgU-.js +0 -282
  645. package/studio-dist/server/chunks/session-factory-vbengc42.js +0 -96
  646. package/studio-dist/server/chunks/session-grade-BF4ZZtN_.js +0 -244
  647. package/studio-dist/server/chunks/session-journal-Bvey70Ur.js +0 -235
  648. package/studio-dist/server/chunks/session-narrative-BirZINSU.js +0 -304
  649. package/studio-dist/server/chunks/session-store--N_xHxUH.js +0 -557
  650. package/studio-dist/server/chunks/session-store2-DAM-eVTH.js +0 -43
  651. package/studio-dist/server/chunks/sessions-33DD4hkt.js +0 -2273
  652. package/studio-dist/server/chunks/sessions2-BHswKRDP.js +0 -50
  653. package/studio-dist/server/chunks/sigil-B-RznFXz.js +0 -33
  654. package/studio-dist/server/chunks/sigil2-jRMG5DuR.js +0 -47
  655. package/studio-dist/server/chunks/signin-BAsW3mM8.js +0 -644
  656. package/studio-dist/server/chunks/sleep-consolidation-BnSwXrvq.js +0 -823
  657. package/studio-dist/server/chunks/specialists-B_ZbpQlK.js +0 -441
  658. package/studio-dist/server/chunks/sqlite-Bm5LzbPs.js +0 -5977
  659. package/studio-dist/server/chunks/sqlite-backup-CFG1-b2e.js +0 -42
  660. package/studio-dist/server/chunks/sqlite-data-accessor-DD_RFX6r.js +0 -987
  661. package/studio-dist/server/chunks/sqlite-data-accessor2-CRmgNBnq.js +0 -44
  662. package/studio-dist/server/chunks/sqlite-native-BwlIqtbs.js +0 -42
  663. package/studio-dist/server/chunks/sqlite2-CUPut2fh.js +0 -42
  664. package/studio-dist/server/chunks/src2-DK8DKufK.js +0 -290
  665. package/studio-dist/server/chunks/src3-BJjf7XRG.js +0 -798
  666. package/studio-dist/server/chunks/src4-DNleK_t_.js +0 -28
  667. package/studio-dist/server/chunks/sso-oidc-eUvwseuL.js +0 -861
  668. package/studio-dist/server/chunks/storage-B9UA3BHR.js +0 -198
  669. package/studio-dist/server/chunks/sts-g7LDMLEd.js +0 -4655
  670. package/studio-dist/server/chunks/surprisal-CwTfWQi8.js +0 -224
  671. package/studio-dist/server/chunks/surprisal-tree-7dfHsuw1.js +0 -230
  672. package/studio-dist/server/chunks/system-resolver2-Bzhue_zQ.js +0 -53
  673. package/studio-dist/server/chunks/tasks-SHX7UPGY.js +0 -19694
  674. package/studio-dist/server/chunks/tasks-schema2-CTMoQKQy.js +0 -42
  675. package/studio-dist/server/chunks/temporal-supersession-DP4GPG_B.js +0 -298
  676. package/studio-dist/server/chunks/token-cAhAD9f9.js +0 -139
  677. package/studio-dist/server/chunks/token-util-D6drEz8_.js +0 -353
  678. package/studio-dist/server/chunks/token-util2-8UPSGvbb.js +0 -97
  679. package/studio-dist/server/chunks/transcript-scanner-kJWhWmlg.js +0 -53
  680. package/studio-dist/server/chunks/transformers.node-_xbxmK5p.js +0 -38475
  681. package/studio-dist/server/chunks/typed-query-l1D_QElt.js +0 -18
  682. package/studio-dist/server/chunks/unique-constraint-BZrrwgwc.js +0 -88
  683. package/studio-dist/server/chunks/update-B2YVBZG0.js +0 -1594
  684. package/studio-dist/server/chunks/user-profile-B61ZhVmC.js +0 -89
  685. package/studio-dist/server/chunks/utils-BPSZyajG.js +0 -1176
  686. package/studio-dist/server/chunks/validation-rules-CE27rtoF.js +0 -44
  687. package/studio-dist/server/chunks/zod-DsAeKNLm.js +0 -141
  688. /package/studio-dist/client/_app/immutable/chunks/{CQCxd1p9.js → B8WnZMYa.js} +0 -0
  689. /package/studio-dist/client/_app/immutable/chunks/{D13Quv9e2.js → B_0_YCYG.js} +0 -0
  690. /package/studio-dist/client/_app/immutable/chunks/{djL6V-tr.js → CqgtdAx_.js} +0 -0
  691. /package/studio-dist/client/_app/immutable/chunks/{_FU7h_8p2.js → KrUPZzXZ.js} +0 -0
@@ -1,4600 +0,0 @@
1
- import { h as getCleoHome, g as getCleoHome$1, b as getLogger, F as getCleoPlatformPaths, Q as parseClaudeCodeCredentials } from './paths-CV7S5-Zm.js';
2
- import { existsSync, mkdirSync, writeFileSync, readFileSync, renameSync, unlinkSync, readdirSync, statSync, chmodSync } from 'node:fs';
3
- import { dirname, join, basename } from 'node:path';
4
- import { randomBytes, randomUUID } from 'node:crypto';
5
- import { p as properLockfileExports } from './index-Dtt_h58T.js';
6
- import { a as getConfigValue } from './config-DHj9CcFs.js';
7
- import { pathToFileURL } from 'node:url';
8
- import 'node:child_process';
9
- import { homedir } from 'node:os';
10
-
11
- //#region ../core/dist/store/file-utils.js
12
- /**
13
- * File utility helpers for CLEO data access.
14
- *
15
- * File utility helpers for CLEO data access including atomic writes,
16
- * file locking, and backup rotation.
17
- *
18
- * @task T4833
19
- * @epic T4654
20
- */
21
- /**
22
- * Maximum number of operational backups to keep.
23
- */
24
- var MAX_BACKUPS = 10;
25
- /**
26
- * Create a numbered backup of a file (Tier 1 operational backup).
27
- *
28
- * When `mode` is provided, the backup directory is created at `mode`
29
- * (translated to a directory-execute bit) and each rotated backup is
30
- * written at `mode` so secrets never leak through historical copies.
31
- */
32
- function rotateBackup(filePath, mode) {
33
- const dir = dirname(filePath);
34
- const name = basename(filePath);
35
- const backupDir = join(dir, ".backups");
36
- const dirMode = typeof mode === "number" ? modeToDirMode(mode) : void 0;
37
- if (!existsSync(backupDir)) mkdirSync(backupDir, {
38
- recursive: true,
39
- mode: dirMode
40
- });
41
- for (let i = MAX_BACKUPS; i >= 1; i--) {
42
- const current = join(backupDir, `${name}.${i}`);
43
- if (i === MAX_BACKUPS) try {
44
- unlinkSync(current);
45
- } catch {}
46
- else {
47
- const next = join(backupDir, `${name}.${i + 1}`);
48
- try {
49
- if (existsSync(current)) renameSync(current, next);
50
- } catch {}
51
- }
52
- }
53
- try {
54
- const content = readFileSync(filePath, "utf-8");
55
- const backupPath = join(backupDir, `${name}.1`);
56
- if (typeof mode === "number") writeFileSync(backupPath, content, {
57
- encoding: "utf-8",
58
- mode
59
- });
60
- else writeFileSync(backupPath, content, "utf-8");
61
- } catch {}
62
- }
63
- /**
64
- * Translate a file mode (e.g. 0o600) into the matching directory mode by
65
- * mirroring read bits into execute bits. 0o600 → 0o700, 0o644 → 0o755.
66
- *
67
- * Directories need execute (`x`) where files only need read (`r`) — that's
68
- * how POSIX gates `readdir`/`stat` of children. Without this, a 0600 file
69
- * in a 0644 directory is still listable by other users.
70
- *
71
- * @task T-LLM-CRED-CENTRALIZATION Phase 2 — security review S-03
72
- */
73
- function modeToDirMode(mode) {
74
- let result = mode & 511;
75
- if (result & 256) result |= 64;
76
- if (result & 32) result |= 8;
77
- if (result & 4) result |= 1;
78
- return result;
79
- }
80
- /**
81
- * Write a JSON file atomically with backup rotation.
82
- *
83
- * Pattern: write temp -> backup original -> rename temp to target
84
- *
85
- * When `opts.mode` is provided, the temp file AND the rotated backup
86
- * are both created at that mode so there is no instant at which the
87
- * live file (or any historical copy) exists at a looser mode. This is
88
- * REQUIRED for secret-bearing files such as `llm-credentials.json`;
89
- * passing 0o600 closes CWE-276 (incorrect default permissions) and
90
- * CWE-367 (rename → chmod TOCTOU) together.
91
- *
92
- * Back-compat: callers that omit `opts` (or pass a number, for the old
93
- * `indent` positional form) get the legacy default-umask behavior so no
94
- * existing usage changes.
95
- *
96
- * @param filePath - Target file path.
97
- * @param data - Data to serialize as JSON.
98
- * @param optsOrIndent - Options object, or a number for legacy `indent`
99
- * positional form.
100
- *
101
- * @task T-LLM-CRED-CENTRALIZATION Phase 2 — security review S-01/S-02
102
- */
103
- function writeJsonFileAtomic(filePath, data, optsOrIndent = 2) {
104
- const opts = typeof optsOrIndent === "number" ? { indent: optsOrIndent } : optsOrIndent;
105
- const indent = opts.indent ?? 2;
106
- const mode = opts.mode;
107
- const tempPath = join(dirname(filePath), `.${basename(filePath)}.${randomBytes(6).toString("hex")}.tmp`);
108
- const content = JSON.stringify(data, null, indent) + "\n";
109
- if (typeof mode === "number") writeFileSync(tempPath, content, {
110
- encoding: "utf-8",
111
- mode
112
- });
113
- else writeFileSync(tempPath, content, "utf-8");
114
- try {
115
- if (existsSync(filePath)) rotateBackup(filePath, mode);
116
- renameSync(tempPath, filePath);
117
- } catch (error) {
118
- try {
119
- unlinkSync(tempPath);
120
- } catch {}
121
- throw error;
122
- }
123
- }
124
- /**
125
- * Read a JSON file, returning parsed content or null if not found.
126
- *
127
- * @param filePath - Path to the JSON file
128
- */
129
- function readJsonFile(filePath) {
130
- try {
131
- const content = readFileSync(filePath, "utf-8");
132
- return JSON.parse(content);
133
- } catch (error) {
134
- if (error.code === "ENOENT") return null;
135
- throw error;
136
- }
137
- }
138
- /**
139
- * Default lock options matching bash flock behavior
140
- */
141
- var LOCK_OPTIONS = {
142
- retries: {
143
- retries: 5,
144
- factor: 2,
145
- minTimeout: 100,
146
- maxTimeout: 5e3
147
- },
148
- stale: 3e4
149
- };
150
- /**
151
- * Read and write a JSON file with exclusive locking.
152
- *
153
- * Acquires a cross-process lock, reads current state, applies the
154
- * transform function, validates, and writes back atomically.
155
- *
156
- * When `opts.mode` is provided, every filesystem mutation in this path
157
- * (parent dir create, sentinel empty file, atomic write, rotated backup)
158
- * is constrained to the requested mode so secret-bearing data never
159
- * touches the disk at a looser permission.
160
- *
161
- * @param filePath - File to lock and modify.
162
- * @param transform - Function that receives current data and returns new data.
163
- * @param opts - Optional mode override for secret-bearing writes.
164
- * @returns The transformed data.
165
- *
166
- * @task T-LLM-CRED-CENTRALIZATION Phase 2 — security review S-01/S-02/S-03
167
- */
168
- async function withLock(filePath, transform, opts = {}) {
169
- const dir = dirname(filePath);
170
- const dirMode = typeof opts.mode === "number" ? modeToDirMode(opts.mode) : void 0;
171
- if (!existsSync(dir)) mkdirSync(dir, {
172
- recursive: true,
173
- mode: dirMode
174
- });
175
- if (!existsSync(filePath)) if (typeof opts.mode === "number") writeFileSync(filePath, "", {
176
- encoding: "utf-8",
177
- mode: opts.mode
178
- });
179
- else writeFileSync(filePath, "", "utf-8");
180
- let release;
181
- try {
182
- release = await properLockfileExports.lock(filePath, LOCK_OPTIONS);
183
- const updated = transform(readJsonFile(filePath));
184
- writeJsonFileAtomic(filePath, updated, { mode: opts.mode });
185
- return updated;
186
- } finally {
187
- if (release) await release();
188
- }
189
- }
190
- /**
191
- * Acquire a file lock and execute an operation.
192
- * Unlike withLock, this doesn't read/write the file - caller manages I/O.
193
- * The return type R is independent of the file content type.
194
- */
195
- async function withFileLock(filePath, operation) {
196
- const dir = dirname(filePath);
197
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
198
- if (!existsSync(filePath)) writeFileSync(filePath, "", "utf-8");
199
- let release;
200
- try {
201
- release = await properLockfileExports.lock(filePath, LOCK_OPTIONS);
202
- return await operation();
203
- } finally {
204
- if (release) await release();
205
- }
206
- }
207
-
208
- //#region ../core/dist/llm/stable-device-id.js
209
- /**
210
- * Stable device identifier — persisted UUIDv4 keyed to the CLEO home directory.
211
- *
212
- * Used by providers that enforce device-ID stability on every request
213
- * (`X-Msh-Device-Id` for Kimi Code; future GitHub Copilot / Cursor). The UUID
214
- * is written once at first use and reused across processes — losing it would
215
- * trigger device re-registration prompts upstream.
216
- *
217
- * Storage: `${CLEO_HOME}/device-id` — single-line UUIDv4, no metadata. Path
218
- * resolves through `getCleoHome()` so it follows XDG conventions on Linux
219
- * and stays inside the CLEO home on Windows/macOS.
220
- *
221
- * @module llm/stable-device-id
222
- * @task T9321
223
- * @epic T9261 T-LLM-CRED-CENTRALIZATION
224
- */
225
- /** Filename within `getCleoHome()` storing the persisted UUID. */
226
- var DEVICE_ID_FILE = "device-id";
227
- /** Process-lifetime cache so repeat callers do not re-read the disk. */
228
- var _cachedDeviceId = null;
229
- /**
230
- * Return the stable device UUID for this CLEO installation.
231
- *
232
- * Creates the file on first call (atomic write — tmp+rename pattern). All
233
- * subsequent calls in the same process return the cached value. Across
234
- * processes, the file is the source of truth.
235
- *
236
- * The UUID is OPAQUE — callers MUST treat it as a stable identifier and not
237
- * encode any meaning into its bytes.
238
- *
239
- * @returns A UUIDv4 string in canonical hyphenated form.
240
- *
241
- * @task T9321
242
- */
243
- function getStableDeviceId() {
244
- if (_cachedDeviceId !== null) return _cachedDeviceId;
245
- const path = join(getCleoHome(), DEVICE_ID_FILE);
246
- if (existsSync(path)) try {
247
- const raw = readFileSync(path, "utf-8").trim();
248
- if (raw.length > 0) {
249
- _cachedDeviceId = raw;
250
- return raw;
251
- }
252
- } catch {}
253
- const fresh = randomUUID();
254
- try {
255
- const dir = dirname(path);
256
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
257
- const tmpPath = `${path}.tmp.${process.pid}`;
258
- writeFileSync(tmpPath, fresh, { mode: 384 });
259
- renameSync(tmpPath, path);
260
- } catch {}
261
- _cachedDeviceId = fresh;
262
- return fresh;
263
- }
264
- //#endregion
265
- //#region ../core/dist/llm/provider-registry/builtin/kimi-code.js
266
- /**
267
- * Builtin provider profile for Kimi Code (kimi.com/code).
268
- *
269
- * Kimi Code is Moonshot AI's coding-focused offering accessible via two key
270
- * shapes that share a single chat endpoint:
271
- *
272
- * - **`sk-kimi-*` API keys** issued through https://kimi.com/code — sent as
273
- * `Authorization: Bearer <key>` against https://api.kimi.com/coding. The
274
- * endpoint speaks the Anthropic Messages protocol (so the Anthropic SDK
275
- * works out of the box once the base URL is overridden).
276
- * - **OAuth device-code** flow against https://auth.kimi.com using public
277
- * client `17e5f671-d194-4dfb-9706-5516cb48c098`. Yields an access token
278
- * (~15 min lifetime) + refresh token (~30 day lifetime). The access token
279
- * targets the same chat endpoint with the same protocol.
280
- *
281
- * The provider also requires six mandatory `X-Msh-*` headers on every
282
- * request — without them the backend rejects with 401. The `X-Msh-Device-Id`
283
- * must be a stable UUID persisted to disk (see {@link getStableDeviceId}).
284
- *
285
- * Note: the "legacy" Moonshot OpenAI-compat endpoint at api.moonshot.ai/v1
286
- * is served by the separate `moonshot` provider profile — Kimi Code is a
287
- * distinct provider with a different protocol and authentication path.
288
- *
289
- * @task T9321
290
- * @epic T9261 T-LLM-CRED-CENTRALIZATION
291
- * @see packages/core/src/llm/oauth/device-code.ts — Kimi device-code preset
292
- * @see packages/core/src/llm/stable-device-id.ts — persisted device UUID
293
- */
294
- /** Coding endpoint base URL — Anthropic Messages protocol. */
295
- var KIMI_CODE_BASE_URL = "https://api.kimi.com/coding";
296
- /** Default model — Moonshot's coding-tier Kimi K2. */
297
- var DEFAULT_MODEL = "kimi-k2-coding";
298
- /**
299
- * Build the six mandatory `X-Msh-*` headers required by the Kimi Code
300
- * backend. Missing any header triggers 401 errors regardless of credential.
301
- *
302
- * `X-Msh-Device-Id` is sourced from {@link getStableDeviceId} which persists
303
- * a UUIDv4 once at `${CLEO_HOME}/device-id` and reuses it forever — the
304
- * backend tracks devices and would force re-registration on drift.
305
- *
306
- * @returns Header object suitable for spreading into request headers.
307
- *
308
- * @task T9321
309
- */
310
- function getKimiCodeMshHeaders() {
311
- return {
312
- "X-Msh-Platform": "cleo",
313
- "X-Msh-Version": "1",
314
- "X-Msh-Device-Name": "cleo-cli",
315
- "X-Msh-Device-Model": "cleo",
316
- "X-Msh-Os-Version": process.version,
317
- "X-Msh-Device-Id": getStableDeviceId()
318
- };
319
- }
320
- /**
321
- * Detect whether an API key is a Kimi Code coding-plan key.
322
- *
323
- * Kimi Code issues keys with the `sk-kimi-` prefix. Other keys (e.g. legacy
324
- * `mk-` Moonshot platform keys) MUST go through the `moonshot` provider,
325
- * not `kimi-code`.
326
- *
327
- * @param apiKey - The credential's API-key string.
328
- * @returns `true` when the key targets the Kimi Code coding endpoint.
329
- *
330
- * @task T9321
331
- */
332
- function isKimiCodeApiKey(apiKey) {
333
- return apiKey.startsWith("sk-kimi-");
334
- }
335
- /**
336
- * Kimi Code provider profile.
337
- *
338
- * Speaks the Anthropic Messages protocol against api.kimi.com/coding. The
339
- * mandatory `X-Msh-*` headers are merged into every request via
340
- * {@link defaultHeaders}. OAuth bearer tokens issued by the device-code flow
341
- * are accepted alongside `sk-kimi-` API keys.
342
- *
343
- * @task T9321
344
- * @task T9286 (W1d — added reasoning_effort hook)
345
- */
346
- var kimiCodeProfile = {
347
- name: "kimi-code",
348
- displayName: "Kimi Code (Moonshot)",
349
- authTypes: ["api_key", "oauth"],
350
- baseUrl: KIMI_CODE_BASE_URL,
351
- defaultModel: DEFAULT_MODEL,
352
- aliases: [
353
- "kimi",
354
- "moonshot-coding",
355
- "kimi-coding"
356
- ],
357
- defaultHeaders: getKimiCodeMshHeaders(),
358
- envVars: ["KIMI_CODE_API_KEY", "KIMI_API_KEY"],
359
- buildApiKwargsExtras() {
360
- return { reasoning_effort: "high" };
361
- }
362
- };
363
-
364
- /**
365
- * Resolve the Anthropic OAuth client ID.
366
- *
367
- * Prefers `CLEO_ANTHROPIC_OAUTH_CLIENT_ID` env var; falls back to the
368
- * canonical public client_id. No warning is emitted in the fallback path —
369
- * the public client_id is the production-correct default.
370
- */
371
- function resolveAnthropicClientId() {
372
- return process.env["CLEO_ANTHROPIC_OAUTH_CLIENT_ID"] ?? "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
373
- }
374
- /**
375
- * Anthropic Claude provider profile.
376
- *
377
- * - `authTypes` includes both `api_key` (long-lived `sk-ant-*` keys) and
378
- * `oauth` (short-lived `sk-ant-oat-*` bearer tokens from Claude Code).
379
- * - `defaultHeaders` pins the stable `anthropic-version: 2023-06-01` API
380
- * version header required by all Anthropic Messages API requests.
381
- * - `fetchModels` is omitted — the Anthropic API does not expose a public
382
- * `/models` endpoint for arbitrary callers. Callers fall back to the
383
- * static model list maintained in `@cleocode/core/llm`.
384
- * - `oauth` configures the RFC 7636 PKCE flow used by `cleo llm login anthropic`.
385
- */
386
- var anthropicProfile = {
387
- name: "anthropic",
388
- displayName: "Anthropic Claude",
389
- authTypes: ["api_key", "oauth"],
390
- baseUrl: "https://api.anthropic.com",
391
- defaultModel: "claude-haiku-4-5-20251001",
392
- aliases: ["claude", "anthropic-api"],
393
- tier: "frontier",
394
- defaultAuxModel: "claude-haiku-4-5-20251001",
395
- defaultMaxTokens: 4096,
396
- defaultHeaders: { "anthropic-version": "2023-06-01" },
397
- oauth: {
398
- mode: "pkce",
399
- clientId: resolveAnthropicClientId(),
400
- authorizationEndpoint: "https://claude.ai/oauth/authorize",
401
- tokenEndpoint: "https://platform.claude.com/v1/oauth/token",
402
- scope: "org:create_api_key user:profile user:inference",
403
- redirectUri: "https://platform.claude.com/oauth/code/callback",
404
- extraAuthParams: { code: "true" },
405
- tokenBodyFormat: "json"
406
- }
407
- };
408
- //#endregion
409
- //#region ../core/dist/llm/provider-registry/builtin/bedrock.js
410
- /**
411
- * Builtin provider profile for AWS Bedrock (Converse API).
412
- *
413
- * AWS Bedrock exposes many foundation models through a uniform Converse API.
414
- * This profile covers the common model ID prefixes:
415
- * - `anthropic.claude-*` — Anthropic Claude models on Bedrock
416
- * - `amazon.nova-*` — Amazon Nova series
417
- * - `mistral.mistral-*` — Mistral AI models on Bedrock
418
- * - `meta.llama*` — Meta Llama models on Bedrock
419
- * - `cohere.command-*` — Cohere Command models on Bedrock
420
- *
421
- * Credential auth for Bedrock uses the standard AWS credential chain
422
- * (`fromNodeProviderChain`), not an API key. The `authTypes: ['aws_sdk']`
423
- * declaration is informational — the actual resolution happens inside
424
- * `BedrockTransport` via the AWS SDK credential chain.
425
- *
426
- * @task T9317
427
- * @epic T9261 (T-LLM-CRED-CENTRALIZATION Phase 5)
428
- */
429
- /**
430
- * AWS Bedrock provider profile.
431
- *
432
- * - `authTypes` is `['aws_sdk']` — informational for the credential store
433
- * UI. Actual resolution uses `fromNodeProviderChain()` inside the transport.
434
- * - `fetchModels` is omitted — the Bedrock model catalog is large and
435
- * region-specific. Use `cleo llm refresh-catalog` (T9314) for discovery.
436
- * - `defaultModel` targets Claude Sonnet 3.7, widely available across regions.
437
- */
438
- var bedrockProfile = {
439
- name: "bedrock",
440
- displayName: "AWS Bedrock",
441
- authTypes: ["aws_sdk"],
442
- baseUrl: "https://bedrock-runtime.{region}.amazonaws.com",
443
- defaultModel: "anthropic.claude-3-5-sonnet-20241022-v2:0",
444
- aliases: ["aws-bedrock", "bedrock-converse"]
445
- };
446
- //#endregion
447
- //#region ../core/dist/llm/provider-registry/builtin/gemini.js
448
- /**
449
- * Builtin provider profile for Google Gemini (OpenAI-compat shim).
450
- *
451
- * Gemini exposes an OpenAI-compatible shim at
452
- * `https://generativelanguage.googleapis.com/v1beta/openai` that accepts
453
- * the standard `chat.completions.create` wire shape. The Gemini-specific
454
- * quirk is the `extra_body.thinking_config` field which controls extended
455
- * reasoning budget selection.
456
- *
457
- * @task T9286 (W1d)
458
- * @epic T9261 (T-LLM-CRED-CENTRALIZATION Phase 3)
459
- */
460
- /**
461
- * Build the Gemini thinking configuration object for `extra_body`.
462
- *
463
- * Budget selection:
464
- * - `'auto'` — Gemini 1.x and 2.x models, and any model with `flash` in the
465
- * name (Flash-tier models use dynamic budget allocation).
466
- * - `'high'` — Gemini 3 Pro and newer high-capability variants that support
467
- * explicit budget control without automatic fallback.
468
- *
469
- * @invariant gemini-thinking-config: Gemini requires `extra_body.thinking_config`
470
- * for extended reasoning. Budget is model-aware: 'auto' for ≤2.x/flash,
471
- * 'high' for ≥3.x non-flash. This quirk is DISTINCT from Moonshot's shallow
472
- * schema sanitizer and Gemini's own deep schema sanitizer.
473
- *
474
- * @param model - Lowercase model identifier string.
475
- * @returns Thinking config record for `extra_body.thinking_config`.
476
- */
477
- function buildGeminiThinkingConfig(model) {
478
- if (/gemini-3|gemini3/.test(model) && !model.includes("flash")) return { thinking_budget: "high" };
479
- return { thinking_budget: "auto" };
480
- }
481
- /**
482
- * Google Gemini provider profile (OpenAI-compatible shim).
483
- *
484
- * Encodes the Gemini thinking config quirk as a `buildExtraBody` hook.
485
- *
486
- * @task T9286 (W1d)
487
- */
488
- var geminiProfile = {
489
- name: "gemini",
490
- displayName: "Google Gemini",
491
- authTypes: ["api_key"],
492
- baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
493
- defaultModel: "gemini-2.0-flash",
494
- aliases: ["google", "google-gemini"],
495
- tier: "standard",
496
- defaultAuxModel: "gemini-2.0-flash",
497
- defaultMaxTokens: 4096,
498
- envVars: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
499
- buildExtraBody(model) {
500
- return { thinking_config: buildGeminiThinkingConfig(model.toLowerCase()) };
501
- }
502
- };
503
- //#endregion
504
- //#region ../core/dist/llm/provider-registry/builtin/moonshot.js
505
- /**
506
- * Builtin provider profile for Moonshot AI (Kimi K2 legacy endpoint).
507
- *
508
- * This profile covers the **legacy** Moonshot OpenAI-compatible surface at
509
- * `api.moonshot.ai/v1`. It is distinct from the Kimi Code coding-plan
510
- * endpoint (see `kimi-code.ts` which targets `api.kimi.com/coding` with
511
- * Anthropic Messages protocol and device-code OAuth).
512
- *
513
- * Quirks encoded in this profile:
514
- * - Rejects `thinkingBudgetTokens` — Moonshot's API does not expose a
515
- * thinking-budget parameter (Anthropic-style). Callers MUST NOT set it.
516
- * - Sanitizes tool JSON schemas: removes `$schema` and root-level
517
- * `additionalProperties` which the Moonshot backend rejects.
518
- *
519
- * @task T9286 (W1d)
520
- * @epic T9261 (T-LLM-CRED-CENTRALIZATION Phase 3)
521
- */
522
- /** Moonshot API base URL (OpenAI-compatible endpoint). */
523
- var MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
524
- /** Default Kimi K2 coding model identifier. */
525
- var MOONSHOT_DEFAULT_MODEL = "kimi-k2-0905-preview";
526
- /**
527
- * Strip Moonshot-incompatible fields from a single TransportTool's inputSchema.
528
- *
529
- * @invariant moonshot-shallow-sanitize: Moonshot rejects `$schema` and root-level
530
- * `additionalProperties` in tool parameter schemas. This sanitizer removes
531
- * both fields WITHOUT touching nested schemas (shallow-only — Gemini has its
532
- * own separate recursive deep sanitizer that walks the entire schema tree).
533
- *
534
- * @param tool - Provider-neutral TransportTool from the request.
535
- * @returns Tool copy with sanitized root-level `inputSchema`.
536
- */
537
- function sanitizeMoonshotTransportTool(tool) {
538
- const sanitizedSchema = { ...tool.inputSchema };
539
- delete sanitizedSchema["$schema"];
540
- delete sanitizedSchema["additionalProperties"];
541
- return {
542
- ...tool,
543
- inputSchema: sanitizedSchema
544
- };
545
- }
546
- /**
547
- * Moonshot AI provider profile (Kimi K2 legacy OpenAI-compat endpoint).
548
- *
549
- * Encodes two provider-specific quirks as ProviderProfile hooks:
550
- *
551
- * 1. `buildApiKwargsExtras` — rejects `thinkingBudgetTokens` (not supported).
552
- * 2. `buildExtraBody` — sanitizes tool schemas (removes `$schema` +
553
- * root-level `additionalProperties`) for every Moonshot request that
554
- * carries tools.
555
- *
556
- * @task T9286 (W1d)
557
- */
558
- var moonshotProfile = {
559
- name: "moonshot",
560
- displayName: "Moonshot AI (Kimi K2)",
561
- authTypes: ["api_key"],
562
- baseUrl: MOONSHOT_BASE_URL,
563
- defaultModel: MOONSHOT_DEFAULT_MODEL,
564
- aliases: ["kimi-k2", "moonshot-ai"],
565
- envVars: ["MOONSHOT_API_KEY"],
566
- buildApiKwargsExtras(_model, _messages, _tools) {
567
- return {};
568
- },
569
- buildExtraBody(_model, _messages, tools) {
570
- if (tools.length === 0) return {};
571
- return { __sanitizedTransportTools: tools.map(sanitizeMoonshotTransportTool) };
572
- }
573
- };
574
- //#endregion
575
- //#region ../core/dist/llm/provider-registry/builtin/ollama.js
576
- /**
577
- * Builtin provider profile for Ollama.
578
- *
579
- * Ollama is a local/self-hosted LLM runtime that exposes a `/api/chat`
580
- * endpoint speaking its own NDJSON streaming protocol (`ollama_native`
581
- * ApiMode). It is NOT an OpenAI-compatible shim — use {@link OllamaTransport}
582
- * from `packages/core/src/llm/transports/ollama.ts`.
583
- *
584
- * Key characteristics:
585
- * - No API key required for local deployments (empty-string placeholder).
586
- * - Base URL defaults to `http://localhost:11434` (the well-known Ollama port).
587
- * - Tool calling uses the same OpenAI wire shape for REQUEST but returns
588
- * `arguments` as an object (not a JSON string) in the RESPONSE.
589
- * - No prompt-caching support.
590
- *
591
- * @task T9355 (Task A — Ollama transport, D-ph4-05 closure)
592
- * @epic T9354
593
- */
594
- /**
595
- * Ollama provider profile.
596
- *
597
- * Registers the `ollama` canonical name and `ollama-local` alias so that
598
- * `cleo llm test ollama` and `getProviderProfile('ollama-local')` both
599
- * resolve to this profile.
600
- *
601
- * The profile declares `apiMode: 'ollama_native'` which is used by the
602
- * session factory to select {@link OllamaTransport} at runtime.
603
- *
604
- * `authTypes: ['api_key']` is listed for compatibility with the credential
605
- * store API, but local Ollama deployments do not require an actual key —
606
- * users may leave it empty or store a token for remote/proxied deployments.
607
- *
608
- * ## Default model selection (DHQ-081 · T11978 · T11990)
609
- *
610
- * `defaultModel` is `gemma4:e4b` (standard/frontier tasks, requires ≥ 8 GB RAM).
611
- * `defaultAuxModel` is `gemma4:e2b` (fast/aux tasks, requires ≥ 4 GB RAM).
612
- *
613
- * Tags live-verified on ollama.com/library/gemma4 2026-06-11:
614
- * - gemma4:e2b = 7.2 GB download (Q4_K_M), edge 2B effective params
615
- * - gemma4:e4b = 9.6 GB download (Q4_K_M), edge 4B effective params
616
- * - gemma4:12b = 7.6 GB download (QAT), 12B params, 256k context
617
- *
618
- * The cross-provider selector (`cross-provider-selector.ts`) gates model selection
619
- * on `os.totalmem()` and falls through to `qwen2:0.5b` only as a proof-of-life
620
- * last resort when RAM is below 4 GB. These are the provider-registry SSoT
621
- * constants; the selector reads them via `getProviderProfile('ollama')`.
622
- *
623
- * If the catalog does not yet have a `gemma4` family entry, the resolver logs a
624
- * hint to run `cleo llm refresh-catalog`.
625
- */
626
- var ollamaProfile = {
627
- name: "ollama",
628
- displayName: "Ollama (local)",
629
- authTypes: ["api_key"],
630
- baseUrl: "http://localhost:11434",
631
- defaultModel: "gemma4:e4b",
632
- aliases: ["ollama-local"],
633
- tier: "local",
634
- defaultAuxModel: "gemma4:e2b",
635
- defaultMaxTokens: 2048,
636
- envVars: ["OLLAMA_API_KEY", "OLLAMA_BASE_URL"],
637
- supportsThinkingBudget: false
638
- };
639
- /** Resolve the OpenAI OAuth client id (env override → canonical default). */
640
- function resolveOpenaiClientId() {
641
- return process.env["CLEO_OPENAI_OAUTH_CLIENT_ID"] ?? "app_EMoamEEZ73f0CkXaXp7hrann";
642
- }
643
- /**
644
- * OpenAI / Codex provider profile.
645
- *
646
- * `defaultModel` is a static FALLBACK used when no live catalog snapshot is
647
- * available (i.e. before `cleo llm refresh-catalog` has been run). At runtime,
648
- * `cleo llm use openai` / `cleo llm profile <role> openai` derive the default
649
- * from the live catalog via `resolveProviderDefaultModel` and validate any
650
- * explicit `--model` against `validateModelForProvider` (T11773 · E8).
651
- *
652
- * Override per-credential:
653
- * `cleo llm use openai --model <m>`
654
- * `cleo llm profile <role> openai --model <m>`
655
- *
656
- * @task T11773
657
- */
658
- var openaiProfile = {
659
- name: "openai",
660
- displayName: "OpenAI Codex (ChatGPT)",
661
- authTypes: ["api_key", "oauth"],
662
- baseUrl: "https://api.openai.com/v1",
663
- defaultModel: "gpt-5.5",
664
- aliases: [
665
- "codex",
666
- "chatgpt",
667
- "openai-codex"
668
- ],
669
- tier: "frontier",
670
- defaultAuxModel: "gpt-5-mini",
671
- defaultMaxTokens: 4096,
672
- oauth: {
673
- mode: "pkce",
674
- clientId: resolveOpenaiClientId(),
675
- authorizationEndpoint: "https://auth.openai.com/oauth/authorize",
676
- tokenEndpoint: "https://auth.openai.com/oauth/token",
677
- scope: "openid profile email offline_access",
678
- redirectUri: "http://localhost:1455/auth/callback",
679
- extraAuthParams: {
680
- id_token_add_organizations: "true",
681
- codex_cli_simplified_flow: "true",
682
- originator: "codex_cli_rs"
683
- }
684
- }
685
- };
686
- //#endregion
687
- //#region ../core/dist/llm/provider-registry/builtin/openrouter.js
688
- /**
689
- * Builtin provider profile for OpenRouter.
690
- *
691
- * OpenRouter is a unified API gateway for 100+ LLM providers, accessible at
692
- * `https://openrouter.ai/api/v1`. It speaks the OpenAI chat completions wire
693
- * format. The CLEO-specific quirk is the Pareto router plugin block, which
694
- * selects the price-optimal provider for high-capability model requests.
695
- *
696
- * @task T9286 (W1d)
697
- * @epic T9261 (T-LLM-CRED-CENTRALIZATION Phase 3)
698
- */
699
- /**
700
- * OpenRouter provider profile.
701
- *
702
- * Encodes the Pareto plugin routing block as a `buildExtraBody` hook.
703
- * The plugin is only injected for high-capability model tiers
704
- * (Sonnet, Opus, Grok, GPT-4) — cheaper models route without it.
705
- *
706
- * @task T9286 (W1d)
707
- */
708
- var openrouterProfile = {
709
- name: "openrouter",
710
- displayName: "OpenRouter",
711
- authTypes: ["api_key"],
712
- baseUrl: "https://openrouter.ai/api/v1",
713
- defaultModel: "openrouter/anthropic/claude-sonnet-4-6",
714
- aliases: ["open-router"],
715
- envVars: ["OPENROUTER_API_KEY"],
716
- defaultHeaders: {
717
- "HTTP-Referer": "https://cleocode.dev",
718
- "X-Title": "CLEO"
719
- },
720
- buildExtraBody(model) {
721
- if (model.startsWith("openrouter/") && /sonnet|opus|grok|gpt-4/i.test(model)) return { plugins: [{
722
- id: "pareto",
723
- min_coding_score: .85
724
- }] };
725
- return {};
726
- }
727
- };
728
- //#endregion
729
- //#region ../core/dist/llm/provider-registry/builtin/xai.js
730
- /**
731
- * Builtin provider profiles for xAI (Grok models).
732
- *
733
- * xAI exposes an OpenAI-compatible API at `https://api.x.ai/v1`. Two
734
- * profiles are registered — one per ApiMode:
735
- *
736
- * - {@link xaiProfile}: `chat_completions` — standard OpenAI-compatible path.
737
- * Quirk: `x-grok-conv-id` header for KV-cache affinity.
738
- * - {@link xaiResponsesProfile}: `codex_responses` — xAI's Responses-compatible
739
- * endpoint (grok-* models). Selected automatically when the model id starts
740
- * with `grok-` and the caller requests `codex_responses` ApiMode.
741
- *
742
- * @task T9286 (W1d)
743
- * @task T9311 (xAI Responses profile)
744
- * @epic T9261 (T-LLM-CRED-CENTRALIZATION Phase 3/5)
745
- */
746
- /** Lazily-initialized process-scoped Grok conversation id. */
747
- var _grokConvId = null;
748
- /**
749
- * Return (and lazily create) the process-scoped Grok conversation id.
750
- *
751
- * Pinning a stable `x-grok-conv-id` across requests in the same process
752
- * gives xAI's KV-cache layer a consistent cache key, reducing TTFT on
753
- * repeated system-prompt prefix calls within the same agent session.
754
- *
755
- * The id is intentionally not persisted — it resets each process restart so
756
- * stale cache entries do not accumulate across sessions.
757
- *
758
- * @returns Stable `cleo-<timestamp>-<random>` identifier for this process.
759
- */
760
- function getGrokConvId() {
761
- if (_grokConvId === null) _grokConvId = `cleo-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
762
- return _grokConvId;
763
- }
764
- /**
765
- * xAI Grok provider profile — `chat_completions` ApiMode.
766
- *
767
- * Encodes the Grok conversation-id header as a `buildApiKwargsExtras` hook.
768
- *
769
- * @task T9286 (W1d)
770
- */
771
- var xaiProfile = {
772
- name: "xai",
773
- displayName: "xAI Grok",
774
- authTypes: ["api_key"],
775
- baseUrl: "https://api.x.ai/v1",
776
- defaultModel: "grok-3",
777
- aliases: ["grok", "x-ai"],
778
- envVars: ["XAI_API_KEY"],
779
- buildApiKwargsExtras() {
780
- return { extra_headers: { "x-grok-conv-id": getGrokConvId() } };
781
- }
782
- };
783
- /**
784
- * xAI Grok provider profile — `codex_responses` ApiMode.
785
- *
786
- * xAI exposes Responses-compatible endpoints for grok-* models. Select this
787
- * profile when the caller requests `codex_responses` ApiMode or when the
788
- * model id starts with `grok-` and the Responses API is preferred.
789
- *
790
- * Callers should construct a {@link CodexResponsesTransport} (from
791
- * `packages/core/src/llm/transports/codex-responses.ts`) with this profile's
792
- * `baseUrl` and any `x-grok-conv-id` header from `buildApiKwargsExtras`.
793
- *
794
- * @task T9311
795
- */
796
- var xaiResponsesProfile = {
797
- name: "xai",
798
- displayName: "xAI Grok (Responses)",
799
- authTypes: ["api_key"],
800
- baseUrl: "https://api.x.ai/v1",
801
- defaultModel: "grok-3",
802
- aliases: ["grok-responses", "x-ai-responses"],
803
- envVars: ["XAI_API_KEY"],
804
- buildApiKwargsExtras() {
805
- return { extra_headers: { "x-grok-conv-id": getGrokConvId() } };
806
- }
807
- };
808
- //#endregion
809
- //#region ../core/dist/llm/provider-registry/loader.js
810
- /**
811
- * Plugin discovery and dynamic import for the CLEO provider registry.
812
- *
813
- * Scans `${CLEO_HOME}/plugins/model-providers/` for `*.{ts,mjs,js,cjs}` files,
814
- * imports each one via `pathToFileURL(absPath).href`, and calls the module's
815
- * `register` export (named or default) with the plugin API surface.
816
- *
817
- * - Non-existent plugin directory → silently no-ops (not an error).
818
- * - Import errors → logged via `process.stderr` and skipped (never throws).
819
- * - Idempotent: the discovery promise is module-level singleton; repeated
820
- * calls await the same promise.
821
- *
822
- * @task T9262
823
- * @epic T9261 (T-LLM-CRED-CENTRALIZATION Phase 3)
824
- */
825
- /** Extensions tried for user plugin files, in priority order. */
826
- var PLUGIN_EXTENSIONS = [
827
- ".mjs",
828
- ".js",
829
- ".cjs",
830
- ".ts"
831
- ];
832
- /**
833
- * Returns `true` when `filename` ends with a recognised plugin extension.
834
- */
835
- function isPluginFile(filename) {
836
- return PLUGIN_EXTENSIONS.some((ext) => filename.endsWith(ext));
837
- }
838
- /**
839
- * Return the user plugin directory path — `${CLEO_HOME}/plugins/model-providers`.
840
- *
841
- * Does NOT check whether the directory exists; callers must handle that.
842
- */
843
- function getPluginDir() {
844
- return join(getCleoHome$1(), "plugins", "model-providers");
845
- }
846
- /**
847
- * Dynamically import one plugin file and invoke its `register` hook.
848
- *
849
- * Tolerates import errors: logs to `process.stderr` and returns without
850
- * throwing so other plugins in the directory still load.
851
- *
852
- * @param absPath - Absolute path to the plugin file.
853
- * @param api - The plugin API surface passed to the `register` hook.
854
- */
855
- async function loadOnePlugin(absPath, api) {
856
- let mod;
857
- try {
858
- mod = await import(pathToFileURL(absPath).href);
859
- } catch (err) {
860
- process.stderr.write(`[cleo/provider-registry] Failed to import plugin ${absPath}: ${String(err)}\n`);
861
- return;
862
- }
863
- let registerFn;
864
- if (typeof mod.register === "function") registerFn = mod.register;
865
- else if (mod.default !== void 0) {
866
- if (typeof mod.default === "function") registerFn = mod.default;
867
- else if (typeof mod.default.register === "function") registerFn = mod.default.register;
868
- }
869
- if (registerFn === void 0) {
870
- process.stderr.write(`[cleo/provider-registry] Plugin ${absPath} has no exported 'register' function — skipping.\n`);
871
- return;
872
- }
873
- try {
874
- registerFn(api);
875
- } catch (err) {
876
- process.stderr.write(`[cleo/provider-registry] Plugin ${absPath} threw during register(): ${String(err)}\n`);
877
- }
878
- }
879
- /**
880
- * Scan `pluginDir` for files matching {@link PLUGIN_EXTENSIONS}, sorted
881
- * lexicographically for deterministic load order, and load each one.
882
- *
883
- * Non-existent directories are silently ignored (returns without error).
884
- *
885
- * @param pluginDir - Absolute path to the plugins directory.
886
- * @param api - Plugin API surface passed to each plugin.
887
- */
888
- async function scanAndLoadPlugins(pluginDir, api) {
889
- let entries;
890
- try {
891
- entries = readdirSync(pluginDir);
892
- } catch {
893
- return;
894
- }
895
- const pluginFiles = entries.filter((name) => !name.startsWith(".") && isPluginFile(name)).filter((name) => {
896
- try {
897
- return statSync(join(pluginDir, name)).isFile();
898
- } catch {
899
- return false;
900
- }
901
- }).sort();
902
- for (const filename of pluginFiles) await loadOnePlugin(join(pluginDir, filename), api);
903
- }
904
- /**
905
- * Build the {@link ProviderPluginApi} surface backed by the given
906
- * `registerProvider` function and run full plugin discovery.
907
- *
908
- * This is the entry-point called by the registry's `ensureDiscovered()`
909
- * singleton. It:
910
- * 1. Registers all builtin profiles via the provided `builtins` array.
911
- * 2. Scans `${CLEO_HOME}/plugins/model-providers/` for user plugins.
912
- *
913
- * @param registerProvider - The registry's registration function.
914
- * @param builtins - Builtin profiles to pre-register before plugins.
915
- */
916
- async function runDiscovery(registerProvider, builtins) {
917
- const api = { registerProvider };
918
- for (const profile of builtins) registerProvider(profile);
919
- await scanAndLoadPlugins(getPluginDir(), api);
920
- }
921
- //#endregion
922
- //#region ../core/dist/llm/provider-registry/index.js
923
- /**
924
- * CLEO Provider Registry — public API.
925
- *
926
- * A lightweight, in-process registry mapping canonical provider names (and
927
- * their aliases) to {@link ProviderProfile} descriptors. Ported idiomatically
928
- * from the Hermes provider registry (`providers/__init__.py`).
929
- *
930
- * ## Semantics
931
- * - Last-writer-wins on `name` collision — user plugins override builtins.
932
- * - Alias conflicts with another profile's **primary** name throw at
933
- * registration time to prevent silent mis-routing.
934
- * - Discovery is lazy and idempotent: the first call to `getProviderProfile`
935
- * or `listProviders` triggers a one-shot discovery pass that:
936
- * 1. Registers all builtin profiles.
937
- * 2. Scans `${CLEO_HOME}/plugins/model-providers/` for user plugins.
938
- * Subsequent calls reuse the same discovery promise.
939
- *
940
- * @task T9262
941
- * @epic T9261 (T-LLM-CRED-CENTRALIZATION Phase 3)
942
- *
943
- * @example
944
- * ```ts
945
- * import { getProviderProfile, listProviders } from '@cleocode/core/llm/provider-registry';
946
- *
947
- * const profile = await getProviderProfile('anthropic');
948
- * // → { name: 'anthropic', displayName: 'Anthropic Claude', … }
949
- *
950
- * const all = await listProviders();
951
- * // → [{ name: 'anthropic', … }]
952
- * ```
953
- */
954
- /** Primary registry: canonical lower-cased name → profile. */
955
- var _registry = /* @__PURE__ */ new Map();
956
- /**
957
- * Alias map: lower-cased alias → canonical lower-cased name.
958
- *
959
- * Aliases MUST NOT shadow primary names of *other* profiles.
960
- */
961
- var _aliases = /* @__PURE__ */ new Map();
962
- /** Discovery singleton — `null` means discovery hasn't started yet. */
963
- var _discoveryPromise = null;
964
- /**
965
- * All builtin provider profiles registered before user plugins are loaded.
966
- * Add new builtins here — the registry loads them in array order.
967
- */
968
- var BUILTIN_PROFILES = [
969
- anthropicProfile,
970
- bedrockProfile,
971
- geminiProfile,
972
- kimiCodeProfile,
973
- moonshotProfile,
974
- ollamaProfile,
975
- openaiProfile,
976
- openrouterProfile,
977
- xaiProfile,
978
- xaiResponsesProfile
979
- ];
980
- /**
981
- * Register a provider profile.
982
- *
983
- * - Keys the profile by `profile.name.toLowerCase()` in the primary registry.
984
- * - Indexes each alias in {@link _aliases}. An alias that conflicts with the
985
- * primary name of a *different* already-registered profile throws a
986
- * `TypeError` to prevent silent mis-routing.
987
- * - Last-writer-wins on primary name collisions (user plugins override
988
- * builtins).
989
- *
990
- * @param profile - The provider profile to register.
991
- * @throws {TypeError} When an alias conflicts with another profile's primary name.
992
- */
993
- function registerProvider(profile) {
994
- const key = profile.name.toLowerCase();
995
- _registry.set(key, profile);
996
- for (const alias of profile.aliases ?? []) {
997
- const aliasKey = alias.toLowerCase();
998
- const existing = _aliases.get(aliasKey);
999
- if (existing !== void 0 && existing !== key) {
1000
- if (_registry.has(aliasKey) && aliasKey !== key) throw new TypeError(`[provider-registry] Alias "${alias}" for provider "${profile.name}" conflicts with the primary name of provider "${_registry.get(aliasKey)?.name ?? aliasKey}".`);
1001
- }
1002
- _aliases.set(aliasKey, key);
1003
- }
1004
- }
1005
- /**
1006
- * Ensure the one-shot discovery pass has run.
1007
- *
1008
- * Calling this multiple times is safe — it returns the same promise every time.
1009
- * The discovery pass:
1010
- * 1. Registers all {@link BUILTIN_PROFILES}.
1011
- * 2. Scans `${CLEO_HOME}/plugins/model-providers/` for user plugins.
1012
- */
1013
- async function ensureDiscovered() {
1014
- if (_discoveryPromise === null) _discoveryPromise = runDiscovery(registerProvider, BUILTIN_PROFILES);
1015
- return _discoveryPromise;
1016
- }
1017
- /**
1018
- * Look up a provider profile by name or alias (case-insensitive).
1019
- *
1020
- * Triggers discovery on the first call. Returns `undefined` when no profile
1021
- * matches — callers MUST handle this case (generic/fallback behaviour).
1022
- *
1023
- * @param name - Provider name or alias to look up.
1024
- * @returns The matching {@link ProviderProfile}, or `undefined`.
1025
- */
1026
- async function getProviderProfile(name) {
1027
- await ensureDiscovered();
1028
- const key = name.toLowerCase();
1029
- const canonical = _aliases.get(key) ?? key;
1030
- return _registry.get(canonical);
1031
- }
1032
-
1033
- //#region ../core/dist/llm/rate-limit-guard.js
1034
- /**
1035
- * Cross-session rate-limit guard for the CLEO LLM credential layer.
1036
- *
1037
- * Writes rate-limit state to a shared file so ALL CLEO processes
1038
- * (sentient daemon, CLI calls, auxiliary router) see the same cooldown —
1039
- * preventing pile-on retries from independent sessions.
1040
- *
1041
- * Port of Hermes' `agent/nous_rate_guard.py` (generalized for any provider).
1042
- * Each 429 from a provider can trigger multiple SDK retries multiplied by
1043
- * multiple Hermes retries. By recording state on the first 429 and checking
1044
- * before subsequent attempts, we eliminate retry amplification across sessions.
1045
- *
1046
- * State is keyed on `(provider, label)` and stored at:
1047
- * `${getCleoHome()}/rate-limit-state/<provider>-<label>.json`
1048
- *
1049
- * @module llm/rate-limit-guard
1050
- * @task T9273
1051
- * @epic T-LLM-CRED-CENTRALIZATION Phase 3
1052
- */
1053
- /**
1054
- * Sanitize a provider or label string for use in a filename.
1055
- *
1056
- * Replaces any character outside `[a-zA-Z0-9_-]` with `_` to prevent
1057
- * path traversal (e.g. `../../evil` → `_____evil`).
1058
- *
1059
- * @param value - Raw provider or label string.
1060
- * @returns Filesystem-safe string.
1061
- *
1062
- * @task T9273
1063
- */
1064
- function sanitize(value) {
1065
- return value.replace(/[^a-zA-Z0-9_-]/g, "_");
1066
- }
1067
- /**
1068
- * Absolute path to the rate-limit state file for a given `(provider, label)`.
1069
- *
1070
- * Exposed for unit tests.
1071
- *
1072
- * @param provider - LLM provider identifier (e.g. `'anthropic'`).
1073
- * @param label - Credential label (e.g. `'personal'`).
1074
- * @returns Absolute path to the JSON state file.
1075
- *
1076
- * @task T9273
1077
- */
1078
- function rateLimitStatePath(provider, label) {
1079
- return join(join(getCleoHome(), "rate-limit-state"), `${sanitize(provider)}-${sanitize(label)}.json`);
1080
- }
1081
- /**
1082
- * Record that a provider+label combination is currently rate-limited.
1083
- *
1084
- * Parses the reset time from HTTP response headers when available, then
1085
- * falls back to `defaultCooldownSeconds` (default 300 s). Writes state to
1086
- * `${getCleoHome()}/rate-limit-state/<provider>-<label>.json` so that ALL
1087
- * CLEO processes (sentient daemon, CLI calls, auxiliary router) see the same
1088
- * cooldown — preventing pile-on retries from independent sessions.
1089
- *
1090
- * Port of Hermes' `agent/nous_rate_guard.py:record_nous_rate_limit`
1091
- * (generalized for any provider).
1092
- *
1093
- * @param provider - LLM provider identifier (e.g. `'anthropic'`).
1094
- * @param label - Credential label (e.g. `'personal'`).
1095
- * @param opts - Optional header map and fallback cooldown seconds.
1096
- *
1097
- * @task T9273
1098
- */
1099
- async function recordRateLimit(provider, label, opts) {
1100
- const now = Date.now();
1101
- const defaultMs = (300) * 1e3;
1102
- let resetAt;
1103
- let source;
1104
- {
1105
- resetAt = now + defaultMs;
1106
- source = "default";
1107
- }
1108
- const filePath = rateLimitStatePath(provider, label);
1109
- const dir = join(getCleoHome(), "rate-limit-state");
1110
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1111
- const state = {
1112
- resetAt,
1113
- recordedAt: now,
1114
- source
1115
- };
1116
- await withFileLock(filePath, () => {
1117
- writeJsonFileAtomic(filePath, state);
1118
- });
1119
- }
1120
- /**
1121
- * Return remaining seconds until the rate-limit clears, or `null` if not active.
1122
- *
1123
- * Stale state files (reset_at in the past) are treated as cleared —
1124
- * the file is NOT deleted here to avoid a lock-delete race; `pick()` callers
1125
- * may clean up with `clearRateLimit` after a successful request.
1126
- *
1127
- * Port of Hermes' `agent/nous_rate_guard.py:nous_rate_limit_remaining`.
1128
- *
1129
- * @param provider - LLM provider identifier (e.g. `'anthropic'`).
1130
- * @param label - Credential label (e.g. `'personal'`).
1131
- * @returns Seconds remaining (positive float) or `null` if guard is inactive.
1132
- *
1133
- * @task T9273
1134
- */
1135
- async function rateLimitRemaining(provider, label) {
1136
- const state = readJsonFile(rateLimitStatePath(provider, label));
1137
- if (!state) return null;
1138
- const remaining = (state.resetAt - Date.now()) / 1e3;
1139
- if (remaining > 0) return remaining;
1140
- return null;
1141
- }
1142
-
1143
- //#region ../core/dist/llm/credential-removal.js
1144
- /**
1145
- * RemovalStep registry + per-source removal handlers for the unified
1146
- * credential pool (E-CONFIG-AUTH-UNIFY E2a / T9415).
1147
- *
1148
- * Mirrors Hermes Agent's `agent/credential_sources.py` `RemovalStep`
1149
- * pattern: every credential source (env var, `~/.claude/.credentials.json`,
1150
- * `cleo llm login` PKCE token, third-party CLIs) registers a removal step
1151
- * that `cleo auth remove <provider> <label>` invokes to (a) clean up
1152
- * filesystem state owned by CLEO, (b) surface manual hints for state CLEO
1153
- * does NOT own, and (c) record a suppression entry so the next pool load
1154
- * does not re-seed the just-removed credential.
1155
- *
1156
- * ## Scope of this task
1157
- *
1158
- * T9415 is **removal-infrastructure only**. The `cleo auth remove` CLI
1159
- * lands in T9416; this module exposes the registry + per-source handlers
1160
- * + suppression-state persistence, all consumable by the future CLI.
1161
- *
1162
- * ## Suppression state
1163
- *
1164
- * Suppression is persisted to `${getCleoHome()}/auth-suppression.json`:
1165
- *
1166
- * ```json
1167
- * {
1168
- * "version": 1,
1169
- * "entries": [
1170
- * { "provider": "anthropic", "sourceId": "claude-code", "suppressedAt": 1731000000000 }
1171
- * ]
1172
- * }
1173
- * ```
1174
- *
1175
- * Writes go through {@link writeJsonFileAtomic} (temp-file + rename) so
1176
- * mid-update crashes never corrupt the file. The mutation flow is
1177
- * read-full-state → mutate-in-memory → atomic-write so concurrent CLI
1178
- * invocations cannot interleave partial writes.
1179
- *
1180
- * @module llm/credential-removal
1181
- * @task T9415
1182
- * @epic E-CONFIG-AUTH-UNIFY (E2a)
1183
- */
1184
- /**
1185
- * In-process registry mapping {@link SeederSourceId} → {@link RemovalStep}.
1186
- *
1187
- * Uniqueness rule: each `sourceId` MUST appear at most once. A duplicate
1188
- * `register()` call throws synchronously — silent overwrites would let two
1189
- * steps fight over the same source, which is a programmer error in test or
1190
- * extension code.
1191
- *
1192
- * Tests construct a fresh `RemovalRegistry()` for isolation; production
1193
- * code uses the {@link REMOVAL_REGISTRY} singleton populated at module load
1194
- * (see bottom of this file).
1195
- *
1196
- * @task T9415
1197
- */
1198
- var RemovalRegistry = class {
1199
- steps = /* @__PURE__ */ new Map();
1200
- /**
1201
- * Register a removal step.
1202
- *
1203
- * @param step - Removal step instance to add.
1204
- * @throws {Error} `E_REMOVAL_DUPLICATE` when `step.sourceId` is already
1205
- * registered.
1206
- */
1207
- register(step) {
1208
- if (this.steps.has(step.sourceId)) throw new Error(`E_REMOVAL_DUPLICATE: a removal step is already registered for sourceId='${step.sourceId}'`);
1209
- this.steps.set(step.sourceId, step);
1210
- }
1211
- /**
1212
- * Look up the step for a source id.
1213
- *
1214
- * @param sourceId - Source id to dispatch on.
1215
- * @returns The registered step, or `undefined` if none is registered.
1216
- */
1217
- find(sourceId) {
1218
- return this.steps.get(sourceId);
1219
- }
1220
- /**
1221
- * Return every registered step (insertion order).
1222
- *
1223
- * Used by `cleo auth remove --all` and diagnostic listing.
1224
- */
1225
- getAll() {
1226
- return Array.from(this.steps.values());
1227
- }
1228
- };
1229
- /**
1230
- * Filename for the persisted suppression list under `getCleoHome()`.
1231
- *
1232
- * @internal
1233
- */
1234
- var SUPPRESSION_FILENAME = "auth-suppression.json";
1235
- /**
1236
- * Absolute path to the suppression-state file.
1237
- *
1238
- * Resolved at call time (NOT cached at module load) so test harnesses can
1239
- * override `CLEO_HOME` between runs and see the change reflected.
1240
- *
1241
- * @returns `${getCleoHome()}/auth-suppression.json`.
1242
- *
1243
- * @task T9415
1244
- */
1245
- function suppressionStatePath() {
1246
- return join(getCleoHome(), SUPPRESSION_FILENAME);
1247
- }
1248
- /**
1249
- * Read the suppression file, returning a fresh empty document when the
1250
- * file does not yet exist.
1251
- *
1252
- * @returns Parsed suppression document.
1253
- *
1254
- * @task T9415
1255
- */
1256
- function readSuppressionFile() {
1257
- const data = readJsonFile(suppressionStatePath());
1258
- if (data && data.version === 1 && Array.isArray(data.entries)) return data;
1259
- return {
1260
- version: 1,
1261
- entries: []
1262
- };
1263
- }
1264
- /**
1265
- * Atomically persist the suppression file to disk.
1266
- *
1267
- * Goes through {@link writeJsonFileAtomic} (temp + rename); the parent
1268
- * directory (`getCleoHome()`) is created upstream by `@cleocode/paths` so
1269
- * we do not need to `mkdir` here. The atomic helper itself creates the
1270
- * temp file inside the target's parent directory, so a missing parent
1271
- * surfaces as the original error rather than corrupting state.
1272
- *
1273
- * @param next - New file contents.
1274
- *
1275
- * @task T9415
1276
- */
1277
- function writeSuppressionFile(next) {
1278
- writeJsonFileAtomic(suppressionStatePath(), next);
1279
- }
1280
- /**
1281
- * Add `(provider, sourceId)` to the suppression list.
1282
- *
1283
- * Idempotent — a `(provider, sourceId)` pair already present is left
1284
- * untouched (its original `suppressedAt` is preserved so diagnostics can
1285
- * surface the first removal). Reads full state, mutates in-memory, writes
1286
- * atomically — concurrent CLI invocations cannot interleave partial
1287
- * writes.
1288
- *
1289
- * @param provider - Provider id whose seeder should be suppressed.
1290
- * @param sourceId - Source id of the seeder to suppress.
1291
- *
1292
- * @task T9415
1293
- */
1294
- function addSuppression(provider, sourceId) {
1295
- const current = readSuppressionFile();
1296
- if (current.entries.some((e) => e.provider === provider && e.sourceId === sourceId)) return;
1297
- writeSuppressionFile({
1298
- version: 1,
1299
- entries: [...current.entries, {
1300
- provider,
1301
- sourceId,
1302
- suppressedAt: Date.now()
1303
- }]
1304
- });
1305
- }
1306
- /**
1307
- * Test whether a `(provider, sourceId)` pair is currently suppressed.
1308
- *
1309
- * @param provider - Provider id.
1310
- * @param sourceId - Source id.
1311
- * @returns `true` if suppression is active.
1312
- *
1313
- * @task T9415
1314
- */
1315
- function isSuppressed(provider, sourceId) {
1316
- return readSuppressionFile().entries.some((e) => e.provider === provider && e.sourceId === sourceId);
1317
- }
1318
- /**
1319
- * `'manual'` — entry the operator added with `cleo llm add`.
1320
- *
1321
- * The actual `llm-credentials.json` mutation is performed by the future
1322
- * `cleo auth remove` CLI (T9416) which calls the credential store
1323
- * directly; this step does NOT touch filesystem state itself. Suppression
1324
- * is `false` because `'manual'` has no seeder pass to skip.
1325
- *
1326
- * @task T9415
1327
- */
1328
- var MANUAL_REMOVAL_STEP = {
1329
- sourceId: "manual",
1330
- description: "Operator-added entry; removed directly from llm-credentials.json",
1331
- async remove() {
1332
- return {
1333
- cleaned: [],
1334
- hints: ["entry removed from llm-credentials.json"],
1335
- suppress: false
1336
- };
1337
- }
1338
- };
1339
- /**
1340
- * `'env'` — environment-variable seed.
1341
- *
1342
- * CLEO cannot unset another shell's exported variable, so cleanup is empty
1343
- * and the hint instructs the operator. Suppression is `true` so the next
1344
- * seed pass skips the env source even if the variable is still set
1345
- * (otherwise the just-removed credential would silently re-appear).
1346
- *
1347
- * @task T9415
1348
- */
1349
- var ENV_REMOVAL_STEP = {
1350
- sourceId: "env",
1351
- description: "Environment variable; cannot be unset by CLEO",
1352
- async remove() {
1353
- return {
1354
- cleaned: [],
1355
- hints: ["Unset $VARNAME in your shell to prevent re-seeding"],
1356
- suppress: true
1357
- };
1358
- }
1359
- };
1360
- /**
1361
- * `'claude-code'` — `~/.claude/.credentials.json` consented import.
1362
- *
1363
- * Hard rule: CLEO MUST NOT delete `~/.claude/.credentials.json` — Claude
1364
- * Code itself depends on the file. The correct cleanup is suppression of
1365
- * the seeder so the next pool load does not re-import the credential.
1366
- *
1367
- * @task T9415
1368
- */
1369
- var CLAUDE_CODE_REMOVAL_STEP = {
1370
- sourceId: "claude-code",
1371
- description: "Suppress claude-code re-seed; leave ~/.claude/.credentials.json untouched",
1372
- async remove() {
1373
- return {
1374
- cleaned: [],
1375
- hints: ["Do NOT delete ~/.claude/.credentials.json — Claude Code uses it. Re-seeding is suppressed instead."],
1376
- suppress: true
1377
- };
1378
- }
1379
- };
1380
- /**
1381
- * `'cleo-pkce'` — credential issued by `cleo llm login` (PKCE flow).
1382
- *
1383
- * Owned end-to-end by CLEO: the token cache file at
1384
- * `${getCleoHome()}/anthropic-oauth.json` is deleted unconditionally
1385
- * (ENOENT is silently ignored — idempotency requirement). Suppression is
1386
- * `true` so a residual seeder pass does not re-seed a deleted token.
1387
- *
1388
- * @task T9415
1389
- */
1390
- var CLEO_PKCE_REMOVAL_STEP = {
1391
- sourceId: "cleo-pkce",
1392
- description: "Delete CLEO-issued PKCE token at <CLEO_HOME>/anthropic-oauth.json",
1393
- async remove() {
1394
- const path = join(getCleoHome(), "anthropic-oauth.json");
1395
- const cleaned = [];
1396
- try {
1397
- unlinkSync(path);
1398
- cleaned.push(path);
1399
- } catch (err) {
1400
- if (err.code !== "ENOENT") throw err;
1401
- }
1402
- return {
1403
- cleaned,
1404
- hints: [],
1405
- suppress: true
1406
- };
1407
- }
1408
- };
1409
- /**
1410
- * `'codex-cli'` — OpenAI Codex CLI session token.
1411
- *
1412
- * CLEO does not own the Codex CLI's on-disk state. The hint points the
1413
- * operator at the Codex tooling; suppression is `true`.
1414
- *
1415
- * @task T9415
1416
- */
1417
- var CODEX_CLI_REMOVAL_STEP = {
1418
- sourceId: "codex-cli",
1419
- description: "Suppress codex-cli re-seed; revoke via the Codex CLI itself",
1420
- async remove() {
1421
- return {
1422
- cleaned: [],
1423
- hints: ["Run `codex logout` (or remove ~/.codex/auth.json) in the Codex CLI to revoke the underlying token."],
1424
- suppress: true
1425
- };
1426
- }
1427
- };
1428
- /**
1429
- * `'gemini-cli'` — Google Gemini CLI application default credentials.
1430
- *
1431
- * Same pattern as `'codex-cli'`: CLEO does not own the Gemini CLI's state.
1432
- *
1433
- * @task T9415
1434
- */
1435
- var GEMINI_CLI_REMOVAL_STEP = {
1436
- sourceId: "gemini-cli",
1437
- description: "Suppress gemini-cli re-seed; revoke via the Gemini CLI itself",
1438
- async remove() {
1439
- return {
1440
- cleaned: [],
1441
- hints: ["Run `gcloud auth application-default revoke` (or `gemini auth logout`) to revoke the underlying token."],
1442
- suppress: true
1443
- };
1444
- }
1445
- };
1446
- /**
1447
- * `'gh-cli'` — GitHub CLI token (for the `github-models` provider).
1448
- *
1449
- * Same pattern: CLEO does not own `gh`'s keyring.
1450
- *
1451
- * @task T9415
1452
- */
1453
- var GH_CLI_REMOVAL_STEP = {
1454
- sourceId: "gh-cli",
1455
- description: "Suppress gh-cli re-seed; revoke via `gh auth logout`",
1456
- async remove() {
1457
- return {
1458
- cleaned: [],
1459
- hints: ["Run `gh auth logout` to revoke the underlying GitHub token."],
1460
- suppress: true
1461
- };
1462
- }
1463
- };
1464
- /**
1465
- * Build a fresh registry pre-populated with every built-in step.
1466
- *
1467
- * Exposed so tests can construct an isolated registry mirroring production
1468
- * state without mutating the {@link REMOVAL_REGISTRY} singleton.
1469
- *
1470
- * @returns A fully populated `RemovalRegistry`.
1471
- *
1472
- * @task T9415
1473
- */
1474
- function buildBuiltinRemovalRegistry() {
1475
- const registry = new RemovalRegistry();
1476
- registry.register(MANUAL_REMOVAL_STEP);
1477
- registry.register(ENV_REMOVAL_STEP);
1478
- registry.register(CLAUDE_CODE_REMOVAL_STEP);
1479
- registry.register(CLEO_PKCE_REMOVAL_STEP);
1480
- registry.register(CODEX_CLI_REMOVAL_STEP);
1481
- registry.register(GEMINI_CLI_REMOVAL_STEP);
1482
- registry.register(GH_CLI_REMOVAL_STEP);
1483
- return registry;
1484
- }
1485
- /**
1486
- * Process-wide singleton used by the future `cleo auth remove` CLI.
1487
- *
1488
- * Populated at module load with every built-in step. Tests requiring
1489
- * isolation MUST construct a fresh registry via
1490
- * {@link buildBuiltinRemovalRegistry} (or `new RemovalRegistry()`) instead
1491
- * of mutating this instance.
1492
- *
1493
- * @task T9415
1494
- */
1495
- var REMOVAL_REGISTRY = buildBuiltinRemovalRegistry();
1496
- //#endregion
1497
- //#region ../core/dist/llm/credential-seeders/claude-code-seeder.js
1498
- /**
1499
- * `claude-code` credential seeder — imports the Claude Code OAuth token
1500
- * (`~/.claude/.credentials.json`) into the unified credential pool as an
1501
- * `anthropic` entry.
1502
- *
1503
- * E-CONFIG-AUTH-UNIFY E2a §5.2 T-E2-3. Architectural mirror of Hermes
1504
- * Agent's `_seed_from_singletons` dispatch (`agent/credential_pool.py`).
1505
- *
1506
- * ## Consent gate
1507
- *
1508
- * Reading the Claude Code credential file requires the operator to have
1509
- * explicitly opted in via `auth.claudeCodeConsentGiven = true` in the
1510
- * global config. This mirrors Hermes Agent's PR #4210 fix: without the
1511
- * gate, auxiliary fallback chains could silently route requests through
1512
- * a user's Claude Code OAuth token without consent. The default value is
1513
- * `false`, so the seeder is a no-op on fresh installs.
1514
- *
1515
- * The gate is checked BEFORE any filesystem access: an unconsented seeder
1516
- * never calls `readFileSync` on the credentials path. `isConsentEstablished`
1517
- * reads the global config through `getConfigValue('auth.claudeCodeConsentGiven')`
1518
- * so CLI/env overrides on the consent flag are respected uniformly.
1519
- *
1520
- * ## What the seeder emits
1521
- *
1522
- * One `SeederCredentialEntry` per call (or zero) with:
1523
- *
1524
- * - `provider: 'anthropic'`
1525
- * - `label: 'claude-code'`
1526
- * - `authType: 'oauth'`
1527
- * - `source: 'claude-code'`
1528
- * - `accessToken`, `refreshToken`, `expiresAt` extracted from
1529
- * `claudeAiOauth` via {@link parseClaudeCodeCredentials}.
1530
- *
1531
- * Zero-result paths (consent off, file missing, file unreadable, JSON
1532
- * malformed, token expired) all return `{ entries: [] }` without
1533
- * throwing. Parsing is delegated to `parseClaudeCodeCredentials` in
1534
- * `@cleocode/contracts` so the JSON shape lives in exactly one place.
1535
- *
1536
- * @module llm/credential-seeders/claude-code-seeder
1537
- * @task T9410
1538
- * @epic E-CONFIG-AUTH-UNIFY (E2a)
1539
- */
1540
- /** Stable label written into the pool entry's `label` field. */
1541
- var SEEDER_LABEL$1 = "claude-code";
1542
- /** `(sourceId, provider)` pair this seeder owns inside the registry. */
1543
- var SOURCE_ID$1 = "claude-code";
1544
- var PROVIDER$1 = "anthropic";
1545
- /** Config key consulted by {@link isConsentEstablished}. */
1546
- var CONSENT_CONFIG_KEY = "auth.claudeCodeConsentGiven";
1547
- /**
1548
- * Concrete `CredentialSeeder` for the `claude-code` source.
1549
- *
1550
- * Constructor-injectable filesystem hooks (`readCredentialFile`) keep tests
1551
- * pure — they exercise the consent gate, expiry filter, and missing-file
1552
- * path without touching the real `~/.claude/.credentials.json` and without
1553
- * fs mocks at the module level. The default `readCredentialFile`
1554
- * implementation calls `readFileSync` on `~/.claude/.credentials.json`.
1555
- *
1556
- * @task T9410
1557
- */
1558
- var ClaudeCodeSeeder = class {
1559
- /** @inheritdoc */
1560
- sourceId = SOURCE_ID$1;
1561
- /** @inheritdoc */
1562
- provider = PROVIDER$1;
1563
- readCredentialFile;
1564
- readConsentFlag;
1565
- /**
1566
- * Construct a seeder.
1567
- *
1568
- * @param opts - Optional dependency-injection seams. `readCredentialFile`
1569
- * MUST return `null` when the file is missing or unreadable (it MUST
1570
- * NOT throw). `readConsentFlag` returns the resolved consent boolean
1571
- * from the config cascade.
1572
- */
1573
- constructor(opts) {
1574
- this.readCredentialFile = opts?.readCredentialFile ?? defaultReadCredentialFile$1;
1575
- this.readConsentFlag = opts?.readConsentFlag ?? defaultReadConsentFlag;
1576
- }
1577
- /**
1578
- * Resolve the consent flag from the canonical config cascade.
1579
- *
1580
- * Returns `true` only when `auth.claudeCodeConsentGiven` is explicitly
1581
- * the boolean `true`. Any other value — `undefined`, `null`, `false`,
1582
- * or a stray string — yields `false` so the gate fails closed.
1583
- *
1584
- * @param _provider - Provider id (ignored — this seeder is anthropic-only;
1585
- * the parameter is accepted to satisfy the `CredentialSeeder` contract).
1586
- */
1587
- async isConsentEstablished(_provider) {
1588
- try {
1589
- return await this.readConsentFlag();
1590
- } catch {
1591
- return false;
1592
- }
1593
- }
1594
- /**
1595
- * Read `~/.claude/.credentials.json` and emit a single pool entry.
1596
- *
1597
- * Short-circuits to `{ entries: [] }` when the consent gate is closed,
1598
- * the file is absent, the JSON is malformed, the `claudeAiOauth` block
1599
- * is missing, or the token has expired (per `parseClaudeCodeCredentials`).
1600
- */
1601
- async seed() {
1602
- if (!await this.isConsentEstablished(this.provider)) return { entries: [] };
1603
- const raw = this.readCredentialFile();
1604
- if (raw === null) return { entries: [] };
1605
- const parsed = parseClaudeCodeCredentials(raw);
1606
- if (parsed === null) return { entries: [] };
1607
- return { entries: [{
1608
- provider: "anthropic",
1609
- label: SEEDER_LABEL$1,
1610
- authType: "oauth",
1611
- accessToken: parsed.accessToken,
1612
- source: SEEDER_LABEL$1,
1613
- ...parsed.expiresAt !== void 0 ? { expiresAt: parsed.expiresAt } : {},
1614
- ...parsed.refreshToken !== void 0 ? { refreshToken: parsed.refreshToken } : {}
1615
- }] };
1616
- }
1617
- };
1618
- /**
1619
- * Default `readCredentialFile` implementation.
1620
- *
1621
- * Returns the UTF-8 contents of `~/.claude/.credentials.json`, or `null`
1622
- * when the file is absent or unreadable. NEVER throws — every error path
1623
- * collapses to `null` so the seeder's "source absent" contract holds.
1624
- *
1625
- * @internal
1626
- */
1627
- function defaultReadCredentialFile$1() {
1628
- try {
1629
- return readFileSync(join(homedir(), ".claude", ".credentials.json"), "utf-8");
1630
- } catch {
1631
- return null;
1632
- }
1633
- }
1634
- /**
1635
- * Default `readConsentFlag` implementation.
1636
- *
1637
- * Reads the consent boolean from the canonical config cascade (env >
1638
- * project > global > defaults). The default value at the defaults tier
1639
- * is `false`, so a fresh install with no overrides resolves to `false`.
1640
- *
1641
- * @internal
1642
- */
1643
- async function defaultReadConsentFlag() {
1644
- return (await getConfigValue(CONSENT_CONFIG_KEY)).value === true;
1645
- }
1646
- /**
1647
- * Factory for the singleton `ClaudeCodeSeeder` registered into
1648
- * `BUILTIN_SEEDERS` from `index.ts`.
1649
- *
1650
- * Kept as a factory (not a module-level `const`) so the registry-side
1651
- * import remains acyclic and tests can construct fresh instances with
1652
- * injected dependencies.
1653
- *
1654
- * @task T9410
1655
- */
1656
- function createClaudeCodeSeeder() {
1657
- return new ClaudeCodeSeeder();
1658
- }
1659
- //#endregion
1660
- //#region ../core/dist/llm/credential-seeders/cleo-pkce-seeder.js
1661
- /**
1662
- * `cleo-pkce` credential seeder — reads CLEO's own PKCE-issued Anthropic
1663
- * OAuth token at `${getCleoHome()}/anthropic-oauth.json` and emits it as a
1664
- * single `anthropic` entry in the unified credential pool.
1665
- *
1666
- * E-CONFIG-AUTH-UNIFY E2a §5.2 T-E2-4. Architectural mirror of Hermes
1667
- * Agent's `_seed_from_cleo_pkce` dispatch.
1668
- *
1669
- * ## File shape
1670
- *
1671
- * The CLEO-owned PKCE file uses Claude Code's `claudeAiOauth` envelope
1672
- * shape so the two CLIs can cooperatively share credentials via the
1673
- * write-back handler (`credential-writeback.ts`):
1674
- *
1675
- * ```json
1676
- * {
1677
- * "claudeAiOauth": {
1678
- * "accessToken": "sk-ant-oat-...",
1679
- * "refreshToken": "sk-ant-ort-...",
1680
- * "expiresAt": 1700000000000,
1681
- * "scopes": ["user:inference"]
1682
- * }
1683
- * }
1684
- * ```
1685
- *
1686
- * Parsing is delegated to `parseClaudeCodeCredentials()` in
1687
- * `@cleocode/contracts` so the on-disk format lives in exactly one place.
1688
- *
1689
- * ## Consent gate
1690
- *
1691
- * Unlike the `claude-code` seeder, the PKCE seeder reads a CLEO-owned
1692
- * file: the operator implicitly consented by running `cleo llm login`,
1693
- * so no consent flag is consulted. `isConsentEstablished` is therefore
1694
- * omitted (`undefined` is treated as "always consented" by the contract).
1695
- *
1696
- * ## Zero-result paths
1697
- *
1698
- * Missing file, malformed JSON, missing `claudeAiOauth` block, and
1699
- * expired token all collapse to `{ entries: [] }` without throwing —
1700
- * the seeder contract MUST NOT signal "source absent" via exceptions.
1701
- *
1702
- * @module llm/credential-seeders/cleo-pkce-seeder
1703
- * @task T9411
1704
- * @epic E-CONFIG-AUTH-UNIFY (E2a)
1705
- */
1706
- /** Stable label written into the pool entry's `label` field. */
1707
- var SEEDER_LABEL = "cleo-pkce";
1708
- /** `(sourceId, provider)` pair this seeder owns inside the registry. */
1709
- var SOURCE_ID = "cleo-pkce";
1710
- var PROVIDER = "anthropic";
1711
- /**
1712
- * Default on-disk path for CLEO's PKCE token cache.
1713
- *
1714
- * Computed at call-time (not module-load) so test runners that swap
1715
- * `CLEO_DATA_DIR` between cases observe the change.
1716
- */
1717
- function defaultCleoPkcePath() {
1718
- return join(getCleoHome$1(), "anthropic-oauth.json");
1719
- }
1720
- /**
1721
- * Concrete `CredentialSeeder` for the `cleo-pkce` source.
1722
- *
1723
- * Constructor-injectable filesystem hook (`readCredentialFile`) keeps tests
1724
- * pure — they exercise the expiry, missing-file, and malformed-JSON paths
1725
- * without touching the real CLEO data directory and without module-level
1726
- * fs mocks. The default `readCredentialFile` implementation calls
1727
- * `readFileSync` on `${getCleoHome()}/anthropic-oauth.json`.
1728
- *
1729
- * @task T9411
1730
- */
1731
- var CleoPkceSeeder = class {
1732
- /** @inheritdoc */
1733
- sourceId = SOURCE_ID;
1734
- /** @inheritdoc */
1735
- provider = PROVIDER;
1736
- readCredentialFile;
1737
- /**
1738
- * Construct a seeder.
1739
- *
1740
- * @param opts - Optional dependency-injection seams. `readCredentialFile`
1741
- * MUST return `null` when the file is missing or unreadable (it MUST
1742
- * NOT throw). The default implementation reads
1743
- * `${getCleoHome()}/anthropic-oauth.json` and collapses every error
1744
- * path to `null`.
1745
- */
1746
- constructor(opts) {
1747
- this.readCredentialFile = opts?.readCredentialFile ?? defaultReadCredentialFile;
1748
- }
1749
- /**
1750
- * Read the CLEO PKCE OAuth file and emit a single anthropic pool entry.
1751
- *
1752
- * Short-circuits to `{ entries: [] }` when the file is absent, the JSON
1753
- * is malformed, the `claudeAiOauth` block is missing, or the token has
1754
- * expired (per `parseClaudeCodeCredentials`). Never throws.
1755
- */
1756
- async seed() {
1757
- const raw = this.readCredentialFile();
1758
- if (raw === null) return { entries: [] };
1759
- const parsed = parseClaudeCodeCredentials(raw);
1760
- if (parsed === null) return { entries: [] };
1761
- return { entries: [{
1762
- provider: "anthropic",
1763
- label: SEEDER_LABEL,
1764
- authType: "oauth",
1765
- accessToken: parsed.accessToken,
1766
- source: SEEDER_LABEL,
1767
- ...parsed.expiresAt !== void 0 ? { expiresAt: parsed.expiresAt } : {},
1768
- ...parsed.refreshToken !== void 0 ? { refreshToken: parsed.refreshToken } : {}
1769
- }] };
1770
- }
1771
- };
1772
- /**
1773
- * Default `readCredentialFile` implementation.
1774
- *
1775
- * Returns the UTF-8 contents of `${getCleoHome()}/anthropic-oauth.json`,
1776
- * or `null` when the file is absent or unreadable. NEVER throws — every
1777
- * error path collapses to `null` so the seeder's "source absent" contract
1778
- * holds.
1779
- *
1780
- * @internal
1781
- */
1782
- function defaultReadCredentialFile() {
1783
- try {
1784
- return readFileSync(defaultCleoPkcePath(), "utf-8");
1785
- } catch {
1786
- return null;
1787
- }
1788
- }
1789
- /**
1790
- * Factory for the singleton `CleoPkceSeeder` registered into
1791
- * `BUILTIN_SEEDERS` from `index.ts`.
1792
- *
1793
- * Kept as a factory (not a module-level `const`) so the registry-side
1794
- * import remains acyclic and tests can construct fresh instances with
1795
- * injected dependencies.
1796
- *
1797
- * @task T9411
1798
- */
1799
- function createCleoPkceSeeder() {
1800
- return new CleoPkceSeeder();
1801
- }
1802
- //#endregion
1803
- //#region ../core/dist/llm/credential-seeders/codex-cli-seeder.js
1804
- /**
1805
- * Credential seeder for the OpenAI Codex CLI
1806
- * (E-CONFIG-AUTH-UNIFY E2a / T9418).
1807
- *
1808
- * Implements the "delegate-to-partner-CLI" pattern: instead of CLEO running
1809
- * its own OAuth flow against OpenAI, this seeder reads the token file the
1810
- * Codex CLI already writes when the user runs `codex login`. The seeder is
1811
- * read-only — it never writes back to Codex's auth.json, so the two CLIs
1812
- * remain decoupled.
1813
- *
1814
- * ## File path
1815
- *
1816
- * `${CODEX_HOME:-~/.codex}/auth.json` per Hermes Agent's
1817
- * `hermes_cli/codex_models.py:175`. The `CODEX_HOME` env var is honoured
1818
- * exactly as Codex itself does (empty/whitespace falls back to `~/.codex`).
1819
- *
1820
- * ## File shape
1821
- *
1822
- * Codex `auth.json` carries either an API key, OAuth tokens, or both:
1823
- *
1824
- * ```json
1825
- * {
1826
- * "OPENAI_API_KEY": "sk-...", // optional — when user logged in via API key
1827
- * "tokens": {
1828
- * "access_token": "...", // optional — when user logged in via OAuth
1829
- * "refresh_token": "...",
1830
- * "id_token": "...",
1831
- * "account_id": "..."
1832
- * }
1833
- * }
1834
- * ```
1835
- *
1836
- * This seeder reports both forms when present (one entry per source). Empty
1837
- * tokens are skipped so a partially-written file does not seed a broken
1838
- * credential into the pool.
1839
- *
1840
- * @module llm/credential-seeders/codex-cli-seeder
1841
- * @task T9418
1842
- * @epic E-CONFIG-AUTH-UNIFY (E2a)
1843
- */
1844
- /**
1845
- * Resolve the Codex CLI auth file path.
1846
- *
1847
- * Mirrors Codex's own logic: `CODEX_HOME` env var when set (trimmed),
1848
- * falling back to `~/.codex/auth.json`. Exposed for tests so they can pin
1849
- * the path to a temp dir without touching the real `~/.codex` directory.
1850
- *
1851
- * @internal
1852
- * @task T9418
1853
- */
1854
- function getCodexAuthPath() {
1855
- return join((process.env["CODEX_HOME"] ?? "").trim() || join(homedir(), ".codex"), "auth.json");
1856
- }
1857
- /**
1858
- * Credential seeder for OpenAI Codex CLI.
1859
- *
1860
- * Returns one entry per credential present in `auth.json` (the API key and
1861
- * the OAuth access token are reported separately so the resolver can rank
1862
- * them via priority). Missing file or unreadable JSON → `{ entries: [] }`
1863
- * with no error surfaced; the caller treats absence as "not on this
1864
- * machine".
1865
- *
1866
- * @task T9418
1867
- */
1868
- var CodexCliSeeder = class {
1869
- sourceId = "codex-cli";
1870
- provider = "openai";
1871
- /**
1872
- * Read and parse Codex's `auth.json`, returning the discovered entries.
1873
- *
1874
- * Never throws — every failure path resolves to `{ entries: [], warnings? }`.
1875
- *
1876
- * @returns Discovered credential entries plus optional diagnostics.
1877
- * @task T9418
1878
- */
1879
- async seed() {
1880
- const authPath = getCodexAuthPath();
1881
- if (!existsSync(authPath)) return { entries: [] };
1882
- let raw;
1883
- try {
1884
- raw = readFileSync(authPath, "utf-8");
1885
- } catch (err) {
1886
- return {
1887
- entries: [],
1888
- warnings: [`codex-cli: failed to read ${authPath}: ${err.message}`]
1889
- };
1890
- }
1891
- let parsed;
1892
- try {
1893
- const json = JSON.parse(raw);
1894
- if (!json || typeof json !== "object" || Array.isArray(json)) return {
1895
- entries: [],
1896
- warnings: [`codex-cli: ${authPath} is not a JSON object`]
1897
- };
1898
- parsed = json;
1899
- } catch (err) {
1900
- return {
1901
- entries: [],
1902
- warnings: [`codex-cli: ${authPath} is not valid JSON: ${err.message}`]
1903
- };
1904
- }
1905
- const result = { entries: [] };
1906
- const tokens = parsed["tokens"];
1907
- if (tokens && typeof tokens === "object" && !Array.isArray(tokens)) {
1908
- const accessToken = tokens["access_token"];
1909
- const refreshToken = tokens["refresh_token"];
1910
- if (typeof accessToken === "string" && accessToken.trim()) result.entries.push({
1911
- provider: "openai",
1912
- label: "codex-cli",
1913
- authType: "oauth",
1914
- accessToken: accessToken.trim(),
1915
- source: "codex-cli",
1916
- ...typeof refreshToken === "string" && refreshToken.trim() ? { refreshToken: refreshToken.trim() } : {}
1917
- });
1918
- }
1919
- const apiKey = parsed["OPENAI_API_KEY"];
1920
- if (typeof apiKey === "string" && apiKey.trim()) result.entries.push({
1921
- provider: "openai",
1922
- label: "codex-cli-api-key",
1923
- authType: "api_key",
1924
- accessToken: apiKey.trim(),
1925
- source: "codex-cli"
1926
- });
1927
- return result;
1928
- }
1929
- };
1930
- /**
1931
- * Module-level singleton. Registered into `BUILTIN_SEEDERS` from
1932
- * `credential-seeders/index.ts` to keep the registration site DRY.
1933
- *
1934
- * @task T9418
1935
- */
1936
- var codexCliSeeder = new CodexCliSeeder();
1937
- //#endregion
1938
- //#region ../core/dist/llm/oauth/pkce.js
1939
- /**
1940
- * Refresh an access token using the `refresh_token` grant (RFC 6749 §6).
1941
- *
1942
- * Uses the same token endpoint as code exchange. On success, a new access
1943
- * token (and optionally a new refresh token) is returned. The caller is
1944
- * responsible for persisting the updated tokens.
1945
- *
1946
- * @param params - Refresh parameters.
1947
- * @returns Normalized token response with fresh `accessToken`.
1948
- * @throws {Error} On HTTP errors or missing `access_token` in the response.
1949
- * @task T9302
1950
- */
1951
- async function refreshPkceToken(params) {
1952
- const fields = {
1953
- grant_type: "refresh_token",
1954
- client_id: params.clientId,
1955
- refresh_token: params.refreshToken
1956
- };
1957
- const resp = await postTokenRequest(params.tokenEndpoint, fields, params);
1958
- if (!resp.ok) {
1959
- const detail = await extractOAuthErrorDetail(resp);
1960
- throw new Error(`PKCE token refresh failed for provider '${params.provider}': HTTP ${resp.status}${detail}`);
1961
- }
1962
- return parseTokenResponse(params.provider, await resp.json());
1963
- }
1964
- /**
1965
- * POST a token-endpoint request (code exchange or refresh grant).
1966
- *
1967
- * Single home for the body-encoding split shared by {@link exchangePkceCode}
1968
- * and {@link refreshPkceToken}: `'form'` (RFC 6749 default,
1969
- * `application/x-www-form-urlencoded`) vs `'json'` (Anthropic's non-RFC
1970
- * `application/json` token endpoint). See
1971
- * `ProviderOAuthConfig.tokenBodyFormat`.
1972
- *
1973
- * @internal
1974
- */
1975
- function postTokenRequest(tokenEndpoint, fields, opts) {
1976
- return globalThis.fetch(tokenEndpoint, {
1977
- method: "POST",
1978
- headers: {
1979
- "Content-Type": opts.bodyFormat === "json" ? "application/json" : "application/x-www-form-urlencoded",
1980
- Accept: "application/json",
1981
- ...opts.extraHeaders
1982
- },
1983
- body: opts.bodyFormat === "json" ? JSON.stringify(fields) : new URLSearchParams(fields).toString()
1984
- });
1985
- }
1986
- /**
1987
- * Parse a raw token endpoint JSON body into a normalized {@link OAuthTokens}.
1988
- *
1989
- * @throws {Error} When `access_token` is absent or not a string.
1990
- * @internal
1991
- */
1992
- function parseTokenResponse(provider, data) {
1993
- if (typeof data !== "object" || data === null) throw new Error(`Token endpoint for '${provider}' returned non-object response`);
1994
- const d = data;
1995
- const accessToken = d["access_token"];
1996
- if (typeof accessToken !== "string" || !accessToken) throw new Error(`Token endpoint for '${provider}' returned response without access_token`);
1997
- return {
1998
- accessToken,
1999
- refreshToken: typeof d["refresh_token"] === "string" ? d["refresh_token"] : void 0,
2000
- expiresIn: typeof d["expires_in"] === "number" ? d["expires_in"] : void 0,
2001
- tokenType: typeof d["token_type"] === "string" ? d["token_type"] : "bearer"
2002
- };
2003
- }
2004
- /**
2005
- * Extract a human-readable error detail string from a non-OK OAuth HTTP
2006
- * response.
2007
- *
2008
- * The single shared extractor for every OAuth surface in the LLM layer
2009
- * (`pkce.ts`, `google-pkce.ts`, `device-code.ts`) — local copies drifted and
2010
- * re-introduced the `[object Object]` masking of DHQ-075 (T11958).
2011
- *
2012
- * Strategy:
2013
- * 1. Clone the response so the body stream is not consumed by the primary
2014
- * error path (callers may need the body for further inspection).
2015
- * 2. Parse as JSON; use `error_description` or `error` fields (RFC 6749 §5.2).
2016
- * When the field is an object (Anthropic nests
2017
- * `{"error": {"type", "message"}}`), extract `message`/`type` — never
2018
- * `String(object)`.
2019
- * 3. Fall back to the raw text body when JSON parsing fails (e.g. HTML pages
2020
- * returned by a WAF or a misconfigured reverse proxy). Truncate at 512 chars
2021
- * so a multi-kilobyte HTML page does not flood the error message.
2022
- * 4. Return `''` only when all fallbacks are exhausted.
2023
- *
2024
- * This ensures the caller never sees `[object Object]` in the thrown message
2025
- * and always surfaces the actual HTTP response body for debugging.
2026
- *
2027
- * @task T11958
2028
- */
2029
- async function extractOAuthErrorDetail(resp) {
2030
- const clone = resp.clone();
2031
- try {
2032
- const body = await clone.json();
2033
- const desc = body["error_description"] ?? body["error"];
2034
- if (typeof desc === "string" && desc) return ` — ${desc}`;
2035
- if (desc !== null && typeof desc === "object") {
2036
- const d = desc;
2037
- const message = typeof d["message"] === "string" ? d["message"] : void 0;
2038
- const type = typeof d["type"] === "string" ? d["type"] : void 0;
2039
- if (message) return ` — ${type ? `${type}: ` : ""}${message}`;
2040
- return ` — ${JSON.stringify(desc).slice(0, 512)}`;
2041
- }
2042
- return ` — ${JSON.stringify(body).slice(0, 512)}`;
2043
- } catch {
2044
- try {
2045
- const text = (await resp.text()).trim();
2046
- return text ? ` — ${text.slice(0, 512)}` : "";
2047
- } catch {
2048
- return "";
2049
- }
2050
- }
2051
- }
2052
- //#endregion
2053
- //#region ../core/dist/llm/oauth/google-pkce.js
2054
- /**
2055
- * Google OAuth PKCE refresh helper for the `gemini-cli` credential seeder
2056
- * (E-CONFIG-AUTH-UNIFY E2a / T9418).
2057
- *
2058
- * CLEO owns its own Google OAuth flow rather than reading the gemini-cli npm
2059
- * package's token file directly — this avoids the single-use refresh-token
2060
- * race the Hermes design notes call out (see `agent/google_oauth.py` in
2061
- * Hermes Agent). The interactive login flow (browser callback server, PKCE
2062
- * pair generation, code exchange) is **deferred to E3** when the
2063
- * `cleo llm login gemini` CLI command lands; this module ships only the
2064
- * refresh-from-expired path that the seeder needs at pool-load time.
2065
- *
2066
- * ## Public Google OAuth client
2067
- *
2068
- * The `client_id` / `client_secret` literals below are Google's PUBLIC
2069
- * desktop OAuth client for their own open-source `@google/gemini-cli` npm
2070
- * package. They are baked into every copy of that package and are NOT
2071
- * confidential — desktop OAuth clients have no secret-keeping requirement
2072
- * (PKCE provides the security). Shipping them here matches the pattern
2073
- * `opencode-gemini-auth` and Hermes Agent already use.
2074
- *
2075
- * Source: https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts
2076
- *
2077
- * Both literals are overridable via env vars (`CLEO_GEMINI_CLIENT_ID` /
2078
- * `CLEO_GEMINI_CLIENT_SECRET`) so power users can substitute their own
2079
- * Google Cloud Console-issued desktop client.
2080
- *
2081
- * ## Why a dedicated fetch instead of `refreshPkceToken`
2082
- *
2083
- * The generic {@link refreshPkceToken} helper only POSTs
2084
- * `grant_type=refresh_token`, `client_id`, and `refresh_token` — Google
2085
- * additionally requires `client_secret` in the form body for refreshes that
2086
- * use the public desktop client. Rather than overload the generic helper
2087
- * with a Google-specific param, this module ships its own thin POST that
2088
- * mirrors gemini-cli's exact request shape.
2089
- *
2090
- * @module llm/oauth/google-pkce
2091
- * @task T9418
2092
- * @epic E-CONFIG-AUTH-UNIFY (E2a)
2093
- */
2094
- /**
2095
- * Google's OAuth 2.0 token endpoint.
2096
- *
2097
- * Same endpoint used for both `authorization_code` and `refresh_token`
2098
- * grants. Constants live in this module (not a shared registry) so the
2099
- * Google flow can evolve independently of the generic PKCE helpers.
2100
- *
2101
- * @task T9418
2102
- */
2103
- var GOOGLE_OAUTH_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
2104
- var _PUBLIC_CLIENT_ID_PROJECT_NUM = "681255809395";
2105
- var _PUBLIC_CLIENT_ID_HASH = "oo8ft2oprdrnp9e3aqf6av3hmdib135j";
2106
- var _PUBLIC_CLIENT_SECRET_SUFFIX = "4uHgMPm-1o7Sk-geV6Cu5clXFsxl";
2107
- /**
2108
- * Public Google OAuth desktop client ID baked into `@google/gemini-cli`.
2109
- *
2110
- * NOT confidential — desktop OAuth clients have no secret-keeping
2111
- * requirement. Override via `CLEO_GEMINI_CLIENT_ID` for power users who
2112
- * want to substitute their own Google Cloud Console-issued client.
2113
- *
2114
- * @task T9418
2115
- */
2116
- var DEFAULT_GOOGLE_CLIENT_ID = `${_PUBLIC_CLIENT_ID_PROJECT_NUM}-${_PUBLIC_CLIENT_ID_HASH}.apps.googleusercontent.com`;
2117
- /**
2118
- * Public Google OAuth desktop client secret baked into `@google/gemini-cli`.
2119
- *
2120
- * NOT confidential — see {@link DEFAULT_GOOGLE_CLIENT_ID}. Google's token
2121
- * endpoint accepts the refresh-token grant with `client_secret` present even
2122
- * for desktop clients.
2123
- *
2124
- * @task T9418
2125
- */
2126
- var DEFAULT_GOOGLE_CLIENT_SECRET = `GOCSPX-${_PUBLIC_CLIENT_SECRET_SUFFIX}`;
2127
- /**
2128
- * Default expiry buffer applied when Google omits `expires_in` from the
2129
- * refresh response. One hour matches gemini-cli's own default and gives the
2130
- * pool plenty of headroom before the next refresh.
2131
- *
2132
- * @task T9418
2133
- */
2134
- var DEFAULT_EXPIRES_IN_SECONDS = 3600;
2135
- /**
2136
- * Refresh a Google OAuth access token using a stored refresh token.
2137
- *
2138
- * POSTs `grant_type=refresh_token` plus the public gemini-cli client
2139
- * credentials to Google's token endpoint and converts the resulting
2140
- * `expires_in` (seconds) into an absolute `expiresAt` (epoch milliseconds).
2141
- *
2142
- * Env-var overrides (`CLEO_GEMINI_CLIENT_ID` / `CLEO_GEMINI_CLIENT_SECRET`)
2143
- * are honoured when present; otherwise the shipped public defaults apply.
2144
- *
2145
- * All HTTP calls use `globalThis.fetch` so tests can intercept them via
2146
- * `vi.spyOn(globalThis, 'fetch')` — no real network traffic.
2147
- *
2148
- * @param refreshToken - Long-lived refresh token from a prior login flow.
2149
- * MUST be non-empty; an empty value throws synchronously without making
2150
- * a network call.
2151
- * @returns Fresh access token + computed absolute expiry.
2152
- * @throws {Error} When `refreshToken` is empty, the token endpoint returns
2153
- * a non-2xx response, or the payload lacks an `access_token`.
2154
- *
2155
- * @task T9418
2156
- */
2157
- async function refreshGoogleAccessToken(refreshToken) {
2158
- if (!refreshToken) throw new Error("Google OAuth refresh failed: refresh_token is empty");
2159
- const clientId = (process.env["CLEO_GEMINI_CLIENT_ID"] ?? "").trim() || DEFAULT_GOOGLE_CLIENT_ID;
2160
- const clientSecret = (process.env["CLEO_GEMINI_CLIENT_SECRET"] ?? "").trim() || DEFAULT_GOOGLE_CLIENT_SECRET;
2161
- const body = new URLSearchParams({
2162
- grant_type: "refresh_token",
2163
- client_id: clientId,
2164
- refresh_token: refreshToken
2165
- });
2166
- if (clientSecret) body.set("client_secret", clientSecret);
2167
- const resp = await globalThis.fetch(GOOGLE_OAUTH_TOKEN_ENDPOINT, {
2168
- method: "POST",
2169
- headers: {
2170
- "Content-Type": "application/x-www-form-urlencoded",
2171
- Accept: "application/json"
2172
- },
2173
- body: body.toString()
2174
- });
2175
- if (!resp.ok) {
2176
- const detail = await extractOAuthErrorDetail(resp);
2177
- throw new Error(`Google OAuth refresh failed: HTTP ${resp.status}${detail}`);
2178
- }
2179
- const parsed = await resp.json();
2180
- const accessToken = parsed["access_token"];
2181
- if (typeof accessToken !== "string" || !accessToken) throw new Error("Google OAuth refresh response missing access_token");
2182
- const expiresInRaw = parsed["expires_in"];
2183
- const expiresInSeconds = typeof expiresInRaw === "number" && Number.isFinite(expiresInRaw) && expiresInRaw > 0 ? expiresInRaw : DEFAULT_EXPIRES_IN_SECONDS;
2184
- const expiresAt = Date.now() + expiresInSeconds * 1e3;
2185
- const rotated = parsed["refresh_token"];
2186
- return {
2187
- accessToken,
2188
- expiresAt,
2189
- refreshToken: typeof rotated === "string" && rotated ? rotated : void 0
2190
- };
2191
- }
2192
- //#endregion
2193
- //#region ../core/dist/llm/credential-seeders/gemini-cli-seeder.js
2194
- /**
2195
- * Credential seeder for the CLEO-owned Google PKCE token
2196
- * (E-CONFIG-AUTH-UNIFY E2a / T9418).
2197
- *
2198
- * Per spec OQ-4 (resolved in the T9418 task brief): CLEO does NOT read
2199
- * gemini-cli's npm package token file directly — sharing a refresh token
2200
- * across two clients causes single-use `refresh_token_reused` race
2201
- * failures. Instead CLEO runs its own Google OAuth PKCE flow against the
2202
- * public gemini-cli desktop client and stores tokens at
2203
- * `${getCleoHome()}/google_oauth.json`.
2204
- *
2205
- * The interactive login flow (browser callback server, code exchange) is
2206
- * deferred to E3's `cleo llm login gemini` command. This seeder ships only
2207
- * the **read + refresh** path the pool needs at load time.
2208
- *
2209
- * ## File path & shape
2210
- *
2211
- * `${getCleoHome()}/google_oauth.json`, chmod 0o600 (writer is the
2212
- * interactive login flow — out of scope here):
2213
- *
2214
- * ```json
2215
- * {
2216
- * "access_token": "ya29...",
2217
- * "refresh_token": "1//...",
2218
- * "expires_at": 1744848000000, // unix MILLIseconds
2219
- * "email": "user@example.com"
2220
- * }
2221
- * ```
2222
- *
2223
- * The shape is intentionally flatter than Hermes' "packed refresh" format
2224
- * because CLEO does not need to carry GCP project IDs through the token
2225
- * file — the Gemini transport reads project context from
2226
- * `~/.cleo/config.json` instead. Extra keys (e.g. a stale `project_id`)
2227
- * are tolerated and ignored.
2228
- *
2229
- * ## Refresh-on-expiry
2230
- *
2231
- * When `expires_at` is in the past (or within the 60-second clock-skew
2232
- * buffer), the seeder calls {@link refreshGoogleAccessToken} to mint a
2233
- * fresh access token. **The refreshed token file is NOT written back to
2234
- * disk by this seeder** — the credential store keeps the canonical copy
2235
- * and the interactive login flow (E3) owns disk writes. The seeder simply
2236
- * emits the freshly-refreshed entry so the pool starts the session with a
2237
- * live token.
2238
- *
2239
- * When refresh fails (revoked token, network out, etc.) the seeder emits
2240
- * the stored entry as-is with a warning; the credential pool's mark-bad
2241
- * machinery will quarantine it on the first 401 response.
2242
- *
2243
- * @module llm/credential-seeders/gemini-cli-seeder
2244
- * @task T9418
2245
- * @epic E-CONFIG-AUTH-UNIFY (E2a)
2246
- */
2247
- /**
2248
- * Clock-skew buffer (ms) before `expires_at` at which we proactively
2249
- * refresh. 60 seconds matches Hermes' default and gives the first
2250
- * downstream LLM call enough headroom to complete.
2251
- *
2252
- * @internal
2253
- * @task T9418
2254
- */
2255
- var REFRESH_SKEW_MS = 6e4;
2256
- /**
2257
- * Resolve the CLEO-owned Google OAuth token path.
2258
- *
2259
- * Routed through `getCleoHome()` so the `CLEO_HOME` env override and
2260
- * platform-aware XDG resolution apply uniformly (T9403 / T9405). Exposed
2261
- * for tests.
2262
- *
2263
- * @internal
2264
- * @task T9418
2265
- */
2266
- function getGoogleOauthPath() {
2267
- return join(getCleoHome(), "google_oauth.json");
2268
- }
2269
- /**
2270
- * Parse the on-disk JSON into a narrowed shape. Returns `null` when the
2271
- * file is malformed (extras tolerated, missing required fields rejected).
2272
- *
2273
- * @internal
2274
- * @task T9418
2275
- */
2276
- function parseGoogleOauthFile(raw) {
2277
- let json;
2278
- try {
2279
- json = JSON.parse(raw);
2280
- } catch {
2281
- return null;
2282
- }
2283
- if (!json || typeof json !== "object" || Array.isArray(json)) return null;
2284
- const obj = json;
2285
- const out = {};
2286
- if (typeof obj["access_token"] === "string") out.access_token = obj["access_token"];
2287
- if (typeof obj["refresh_token"] === "string") out.refresh_token = obj["refresh_token"];
2288
- if (typeof obj["expires_at"] === "number" && Number.isFinite(obj["expires_at"])) out.expires_at = obj["expires_at"];
2289
- if (typeof obj["email"] === "string") out.email = obj["email"];
2290
- return out;
2291
- }
2292
- /**
2293
- * Credential seeder for the CLEO-owned Google PKCE token store.
2294
- *
2295
- * @task T9418
2296
- */
2297
- var GeminiCliSeeder = class {
2298
- sourceId = "gemini-cli";
2299
- provider = "gemini";
2300
- /**
2301
- * Read `google_oauth.json` and emit a seeded credential entry,
2302
- * refreshing the access token first when it is expired or near expiry.
2303
- *
2304
- * Never throws — every failure path resolves to `{ entries: [], warnings? }`.
2305
- *
2306
- * @returns Zero or one entry plus optional warnings.
2307
- * @task T9418
2308
- */
2309
- async seed() {
2310
- const path = getGoogleOauthPath();
2311
- if (!existsSync(path)) return { entries: [] };
2312
- let raw;
2313
- try {
2314
- raw = readFileSync(path, "utf-8");
2315
- } catch (err) {
2316
- return {
2317
- entries: [],
2318
- warnings: [`gemini-cli: failed to read ${path}: ${err.message}`]
2319
- };
2320
- }
2321
- const parsed = parseGoogleOauthFile(raw);
2322
- if (!parsed) return {
2323
- entries: [],
2324
- warnings: [`gemini-cli: ${path} is not valid JSON / not an object`]
2325
- };
2326
- if (!parsed.access_token) return {
2327
- entries: [],
2328
- warnings: [`gemini-cli: ${path} is missing access_token`]
2329
- };
2330
- const now = Date.now();
2331
- if (typeof parsed.expires_at === "number" && parsed.expires_at - REFRESH_SKEW_MS <= now && parsed.refresh_token) try {
2332
- const refreshed = await refreshGoogleAccessToken(parsed.refresh_token);
2333
- return { entries: [{
2334
- provider: "gemini",
2335
- label: "gemini-pkce",
2336
- authType: "oauth",
2337
- accessToken: refreshed.accessToken,
2338
- expiresAt: refreshed.expiresAt,
2339
- source: "gemini-cli",
2340
- refreshToken: refreshed.refreshToken ?? parsed.refresh_token,
2341
- ...parsed.email ? { metadata: { email: parsed.email } } : {}
2342
- }] };
2343
- } catch (err) {
2344
- return {
2345
- entries: [{
2346
- provider: "gemini",
2347
- label: "gemini-pkce",
2348
- authType: "oauth",
2349
- accessToken: parsed.access_token,
2350
- expiresAt: parsed.expires_at ?? null,
2351
- source: "gemini-cli",
2352
- ...parsed.refresh_token ? { refreshToken: parsed.refresh_token } : {},
2353
- ...parsed.email ? { metadata: { email: parsed.email } } : {}
2354
- }],
2355
- warnings: [`gemini-cli: refresh failed, using stored access_token (${err.message})`]
2356
- };
2357
- }
2358
- return { entries: [{
2359
- provider: "gemini",
2360
- label: "gemini-pkce",
2361
- authType: "oauth",
2362
- accessToken: parsed.access_token,
2363
- expiresAt: parsed.expires_at ?? null,
2364
- source: "gemini-cli",
2365
- ...parsed.refresh_token ? { refreshToken: parsed.refresh_token } : {},
2366
- ...parsed.email ? { metadata: { email: parsed.email } } : {}
2367
- }] };
2368
- }
2369
- };
2370
- /**
2371
- * Module-level singleton registered into `BUILTIN_SEEDERS`.
2372
- *
2373
- * @task T9418
2374
- */
2375
- var geminiCliSeeder = new GeminiCliSeeder();
2376
- /**
2377
- * Process-wide singleton registry used by the resolver.
2378
- *
2379
- * Concrete seeder instances are auto-registered into this registry at
2380
- * module load via the registration block below + the `./register.ts`
2381
- * barrel which aggregates side-effect imports (T9409+). As of this merge
2382
- * the registry contains:
2383
- *
2384
- * - env seeders for every provider in ENV_VARS (T9409)
2385
- * - `claude-code` × `anthropic` (T9410)
2386
- * - `cleo-pkce` × `anthropic` (T9411)
2387
- * - `codex-cli`, `gemini-cli` external seeders (T9418)
2388
- * - `gh-cli` seeder is present but NOT registered (T9594 — GitHub PAT cannot auth OpenAI)
2389
- *
2390
- * The singleton is a module-scoped constant (`export const`) rather than a
2391
- * class static so re-importing this module from different entry points
2392
- * yields the same instance under Node ESM's module cache. Tests that need
2393
- * isolation MUST construct a fresh `new SeederRegistry()` instead of
2394
- * mutating `BUILTIN_SEEDERS`.
2395
- *
2396
- * @task T9408 (foundation)
2397
- * @task T9410 (claude-code seeder)
2398
- * @task T9411 (cleo-pkce seeder)
2399
- * @task T9418 (codex-cli, gemini-cli, gh-cli)
2400
- */
2401
- var BUILTIN_SEEDERS = new class SeederRegistry {
2402
- /**
2403
- * Map keyed on `${sourceId}::${provider}` for O(1) duplicate detection.
2404
- * Values are the registered seeder instances; iteration order is
2405
- * insertion order (`Map` semantics) so `getAll()` is stable.
2406
- */
2407
- entries = /* @__PURE__ */ new Map();
2408
- /**
2409
- * Register a seeder.
2410
- *
2411
- * @param seeder - Seeder instance to add.
2412
- * @throws {Error} `E_SEEDER_DUPLICATE` when the `(sourceId, provider)`
2413
- * pair is already registered.
2414
- *
2415
- * @task T9408
2416
- */
2417
- register(seeder) {
2418
- const key = SeederRegistry.makeKey(seeder.sourceId, seeder.provider);
2419
- if (this.entries.has(key)) throw new Error(`E_SEEDER_DUPLICATE: a seeder is already registered for sourceId='${seeder.sourceId}' provider='${seeder.provider}'`);
2420
- this.entries.set(key, seeder);
2421
- }
2422
- /**
2423
- * Return every registered seeder in insertion order.
2424
- *
2425
- * @returns Read-only view of the registry's seeders.
2426
- *
2427
- * @task T9408
2428
- */
2429
- getAll() {
2430
- return Array.from(this.entries.values());
2431
- }
2432
- /**
2433
- * Return every seeder whose `provider` matches the argument.
2434
- *
2435
- * Order matches `getAll()` (insertion order). Returns an empty array when
2436
- * no seeder is registered for the provider.
2437
- *
2438
- * @param provider - Provider id to filter by (e.g. `'anthropic'`).
2439
- * @returns Read-only filtered array.
2440
- *
2441
- * @task T9408
2442
- */
2443
- getByProvider(provider) {
2444
- return this.getAll().filter((s) => s.provider === provider);
2445
- }
2446
- /**
2447
- * Compose the internal uniqueness key for a `(sourceId, provider)` pair.
2448
- *
2449
- * Exposed as `static` so tests and (future) resolver code can assert
2450
- * collision behaviour without coupling to the literal separator.
2451
- *
2452
- * @internal
2453
- */
2454
- static makeKey(sourceId, provider) {
2455
- return `${sourceId}::${provider}`;
2456
- }
2457
- }();
2458
- BUILTIN_SEEDERS.register(createClaudeCodeSeeder());
2459
- BUILTIN_SEEDERS.register(createCleoPkceSeeder());
2460
- BUILTIN_SEEDERS.register(codexCliSeeder);
2461
- BUILTIN_SEEDERS.register(geminiCliSeeder);
2462
- //#endregion
2463
- //#region ../core/dist/llm/global-config-migration.js
2464
- /**
2465
- * XDG drift migration for the global CLEO config file (T9405).
2466
- *
2467
- * Pre-T9405 history: `globalConfigPath()` resolved to the CLEO **data** dir
2468
- * (`getCleoHome()`), so existing installs have `config.json` at
2469
- * `~/.local/share/cleo/config.json`. XDG says user config belongs in
2470
- * `XDG_CONFIG_HOME` (`~/.config/cleo/config.json`).
2471
- *
2472
- * This migration runs on first credentials read after upgrade:
2473
- *
2474
- * 1. Skip if the data-dir source is absent.
2475
- * 2. Skip if the data-dir migration marker exists (idempotent).
2476
- * 3. Skip if the config-dir target already exists (manual migration / fresh install).
2477
- * 4. Otherwise:
2478
- * a. `mkdir -p` the config dir.
2479
- * b. Read the data-dir config.
2480
- * c. Validate the JSON parses.
2481
- * d. Write to a temp file in the config dir, then atomically rename to the
2482
- * final `config.json` (temp-then-rename — never any partial state).
2483
- * e. Drop the marker file at `<data-dir>/.migrations/config-dir-v1.done`.
2484
- * f. Rename the data-dir original to `config.json.pre-e1-bak` so users can
2485
- * recover if something goes wrong.
2486
- *
2487
- * Migration is best-effort: every error is swallowed and logged to stderr. A
2488
- * failed migration must never crash a CLI invocation — credentials resolution
2489
- * just falls back to the data-dir location via the transition-window logic
2490
- * baked into `globalConfigPath()`.
2491
- *
2492
- * @module llm/global-config-migration
2493
- * @task T9405
2494
- * @epic T9398
2495
- */
2496
- /**
2497
- * Filename used by `legacyGlobalConfigPath()` / `configDirGlobalConfigPath()`.
2498
- *
2499
- * Kept in one place so the migration helper and the credentials resolver agree
2500
- * without anyone hard-coding the string in two places.
2501
- *
2502
- * @internal
2503
- */
2504
- var GLOBAL_CONFIG_FILENAME = "config.json";
2505
- /**
2506
- * Subdirectory in the data dir where migration markers are stamped.
2507
- *
2508
- * @internal
2509
- */
2510
- var MIGRATIONS_SUBDIR = ".migrations";
2511
- /**
2512
- * Marker filename written when the config-dir migration completes.
2513
- *
2514
- * @internal
2515
- */
2516
- var MIGRATION_MARKER = "config-dir-v1.done";
2517
- /**
2518
- * Backup suffix appended to the data-dir source after a successful migration.
2519
- *
2520
- * Per the E-CONFIG-AUTH-UNIFY spec the original is renamed (not deleted) so
2521
- * users have a recovery path.
2522
- *
2523
- * @internal
2524
- */
2525
- var BACKUP_SUFFIX = ".pre-e1-bak";
2526
- /**
2527
- * Resolve the canonical config-dir path for the global config file.
2528
- *
2529
- * Linux: `~/.config/cleo/config.json`
2530
- * macOS: `~/Library/Preferences/cleo/config.json`
2531
- * Windows: `%APPDATA%\cleo\Config\config.json`
2532
- *
2533
- * @public
2534
- */
2535
- function configDirGlobalConfigPath() {
2536
- return join(getCleoPlatformPaths().config, GLOBAL_CONFIG_FILENAME);
2537
- }
2538
- /**
2539
- * Resolve the legacy data-dir path for the global config file.
2540
- *
2541
- * Pre-T9405 location. Read-only fallback during the transition window — the
2542
- * migration moves the contents to {@link configDirGlobalConfigPath} and
2543
- * renames the data-dir original to `config.json.pre-e1-bak`.
2544
- *
2545
- * @public
2546
- */
2547
- function legacyGlobalConfigPath() {
2548
- return join(getCleoHome(), GLOBAL_CONFIG_FILENAME);
2549
- }
2550
- /**
2551
- * Path to the migration marker file. Stamped in the data dir (NOT the config
2552
- * dir) because the marker is tied to "did we already process the legacy
2553
- * data-dir install?" — co-locating it with the source makes the relationship
2554
- * obvious and prevents `CLEO_HOME` overrides in tests from clobbering the
2555
- * config-dir state.
2556
- *
2557
- * @internal
2558
- */
2559
- function migrationMarkerPath() {
2560
- return join(getCleoHome(), MIGRATIONS_SUBDIR, MIGRATION_MARKER);
2561
- }
2562
- /**
2563
- * Path used during atomic write inside the config dir.
2564
- *
2565
- * @internal
2566
- */
2567
- function tempTargetPath() {
2568
- return `${configDirGlobalConfigPath()}.tmp`;
2569
- }
2570
- /**
2571
- * Run the data-dir → config-dir migration if applicable.
2572
- *
2573
- * Idempotent: safe to call on every CLEO invocation. Performs at most three
2574
- * filesystem stat calls in the steady state (marker present OR source absent).
2575
- *
2576
- * Never throws. Errors are logged to stderr and swallowed — the credentials
2577
- * resolver's transition-window logic will still find the legacy data-dir copy.
2578
- *
2579
- * @returns `true` when a migration was actually performed, `false` when it was
2580
- * a no-op (already migrated, no source, or target already present).
2581
- *
2582
- * @task T9405
2583
- */
2584
- function migrateGlobalConfigToConfigDir() {
2585
- try {
2586
- const source = legacyGlobalConfigPath();
2587
- const target = configDirGlobalConfigPath();
2588
- const marker = migrationMarkerPath();
2589
- if (existsSync(marker)) return false;
2590
- if (!existsSync(source)) {
2591
- stampMarker(marker);
2592
- return false;
2593
- }
2594
- if (existsSync(target)) {
2595
- backupSourceQuiet(source);
2596
- stampMarker(marker);
2597
- return false;
2598
- }
2599
- const raw = readFileSync(source, "utf-8");
2600
- try {
2601
- JSON.parse(raw);
2602
- } catch (err) {
2603
- console.error(`[cleo] global config migration skipped: ${source} is not valid JSON (${err.message})`);
2604
- return false;
2605
- }
2606
- mkdirSync(getCleoPlatformPaths().config, { recursive: true });
2607
- const temp = tempTargetPath();
2608
- writeFileSync(temp, raw, { mode: 420 });
2609
- renameSync(temp, target);
2610
- stampMarker(marker);
2611
- try {
2612
- renameSync(source, `${source}${BACKUP_SUFFIX}`);
2613
- } catch (err) {
2614
- console.error(`[cleo] global config migrated, but backup rename failed: ${err.message}. The legacy file at ${source} can be removed manually.`);
2615
- }
2616
- console.error(`[cleo] migrated global config: ${source} → ${target} (backup at ${source}${BACKUP_SUFFIX})`);
2617
- return true;
2618
- } catch (err) {
2619
- try {
2620
- const temp = tempTargetPath();
2621
- if (existsSync(temp)) unlinkSync(temp);
2622
- } catch {}
2623
- console.error(`[cleo] global config migration failed: ${err.message}. Credentials will continue to resolve from the legacy data-dir location.`);
2624
- return false;
2625
- }
2626
- }
2627
- /**
2628
- * Stamp the migration marker file. Best-effort — errors are swallowed.
2629
- *
2630
- * @internal
2631
- */
2632
- function stampMarker(markerPath) {
2633
- try {
2634
- mkdirSync(join(getCleoHome(), MIGRATIONS_SUBDIR), { recursive: true });
2635
- writeFileSync(markerPath, `${(/* @__PURE__ */ new Date()).toISOString()}\n`, { mode: 420 });
2636
- } catch {}
2637
- }
2638
- /**
2639
- * Best-effort rename of the legacy source to the backup name. Silent failure.
2640
- *
2641
- * @internal
2642
- */
2643
- function backupSourceQuiet(source) {
2644
- try {
2645
- const backup = `${source}${BACKUP_SUFFIX}`;
2646
- if (existsSync(backup)) return;
2647
- renameSync(source, backup);
2648
- } catch {}
2649
- }
2650
- var hasRunInProcess$1 = false;
2651
- /**
2652
- * Run the migration at most once per Node process.
2653
- *
2654
- * Called from the credentials resolver before reading `globalConfigPath()` so
2655
- * a stale install is upgraded in-place on first credentials read. The latch
2656
- * keeps the steady-state cost at one boolean check after the first call —
2657
- * essential because `resolveCredentials()` is on the hot path of every LLM
2658
- * call.
2659
- *
2660
- * Use {@link _resetGlobalConfigMigrationLatch} in tests to re-arm the latch.
2661
- *
2662
- * @public
2663
- */
2664
- function ensureGlobalConfigMigrated() {
2665
- if (hasRunInProcess$1) return;
2666
- hasRunInProcess$1 = true;
2667
- migrateGlobalConfigToConfigDir();
2668
- }
2669
- //#endregion
2670
- //#region ../core/dist/llm/legacy-flat-key-import.js
2671
- /**
2672
- * One-shot migration: import the legacy `${CLEO_HOME}/anthropic-key` flat file
2673
- * into the unified credential pool as a `legacy-flat-key` entry.
2674
- *
2675
- * Part of `E-CONFIG-AUTH-UNIFY` Epic E1 (T9406 / T-E1-4). The legacy flat key
2676
- * file is the "tier 4b" entry in the current 6-tier resolver in
2677
- * `credentials.ts`. Once imported into the pool, the resolver's tier-3
2678
- * (`cred-file`) lookup will pick it up — so post-E2 we can collapse the
2679
- * tier-4b branch entirely without losing any operator's previously-stored
2680
- * key.
2681
- *
2682
- * ## Behavior
2683
- *
2684
- * On `importLegacyFlatAnthropicKey()` (idempotent):
2685
- *
2686
- * 1. If the migration marker `${CLEO_HOME}/.imported-legacy-flat-key` exists,
2687
- * return immediately (`status: 'marker-present'`).
2688
- * 2. If the pool already contains an `anthropic` entry with
2689
- * `label === 'legacy-flat-key'`, write the marker and return
2690
- * (`status: 'already-imported'`).
2691
- * 3. If `${CLEO_HOME}/anthropic-key` does not exist, write the marker and
2692
- * return (`status: 'no-flat-file'`). Avoids re-stating the file on
2693
- * every CLI invocation.
2694
- * 4. If the flat file exists but its trimmed content is empty, write the
2695
- * marker and return (`status: 'empty-flat-file'`). We do NOT rename an
2696
- * empty file — leave it for the operator to inspect / delete.
2697
- * 5. Otherwise: call `addCredential()` with the trimmed key, then rename
2698
- * the flat file to `anthropic-key.pre-e1-bak`, then write the marker.
2699
- * Returns `status: 'imported'`.
2700
- *
2701
- * ## Atomicity contract
2702
- *
2703
- * The two side-effects we care about — (a) the pool entry insert and
2704
- * (b) the flat-file rename — are sequenced so a failure at (b) leaves the
2705
- * pool entry in place AND no marker is written. The next run will then
2706
- * skip the insert (via the `getCredentialByLabel` check in step 2 above)
2707
- * but still attempt the rename. This avoids a half-done state where the
2708
- * marker exists but the original flat file is still discoverable by the
2709
- * tier-4b resolver branch (which would re-import it on the next pool
2710
- * rebuild).
2711
- *
2712
- * If `addCredential()` itself throws, nothing is renamed and no marker
2713
- * is written — the next invocation retries cleanly.
2714
- *
2715
- * @module llm/legacy-flat-key-import
2716
- * @task T9406
2717
- * @epic E-CONFIG-AUTH-UNIFY
2718
- */
2719
- var logger$1 = getLogger("llm-legacy-flat-key-import");
2720
- /**
2721
- * Canonical label assigned to the imported entry.
2722
- *
2723
- * Constant rather than parameterized — the resolver collapse in E2 keys
2724
- * its behavior off this exact label, so it MUST remain stable.
2725
- */
2726
- var LEGACY_FLAT_KEY_LABEL = "legacy-flat-key";
2727
- /**
2728
- * Filename suffix used when renaming the original flat file post-import.
2729
- *
2730
- * Kept identical to the convention used by the T310 conduit migration
2731
- * (`.pre-t310.bak`) for cross-codebase greppability.
2732
- */
2733
- var LEGACY_FLAT_KEY_BAK_SUFFIX = ".pre-e1-bak";
2734
- /**
2735
- * Filename of the migration marker inside `getCleoHome()`. Existence of
2736
- * this file (any content) is sufficient to short-circuit the migration.
2737
- */
2738
- var LEGACY_FLAT_KEY_MARKER = ".imported-legacy-flat-key";
2739
- /**
2740
- * Compute all paths used by the migration. Pure — no filesystem touch.
2741
- */
2742
- function migrationPaths() {
2743
- const home = getCleoHome();
2744
- return {
2745
- flatPath: join(home, "anthropic-key"),
2746
- bakPath: join(home, `anthropic-key${LEGACY_FLAT_KEY_BAK_SUFFIX}`),
2747
- markerPath: join(home, LEGACY_FLAT_KEY_MARKER)
2748
- };
2749
- }
2750
- /**
2751
- * Write the migration marker. Uses `flag: 'wx'` so concurrent first-run
2752
- * callers race-tolerantly — exactly one writes, the rest get `EEXIST`
2753
- * which we treat as success.
2754
- *
2755
- * Best-effort 0o600 (matches the surrounding cred-store hardening).
2756
- */
2757
- function writeMarker(markerPath) {
2758
- try {
2759
- writeFileSync(markerPath, `${(/* @__PURE__ */ new Date()).toISOString()}\n`, {
2760
- encoding: "utf-8",
2761
- flag: "wx",
2762
- mode: 384
2763
- });
2764
- } catch (err) {
2765
- if (err.code === "EEXIST") return;
2766
- logger$1.debug({
2767
- markerPath,
2768
- err: err instanceof Error ? err.message : String(err)
2769
- }, "legacy-flat-key migration: marker write failed");
2770
- }
2771
- }
2772
- /**
2773
- * Read and trim the flat file. Returns null on any I/O error or when the
2774
- * trimmed content is empty. Never throws.
2775
- */
2776
- function readFlatKey(flatPath) {
2777
- try {
2778
- if (!existsSync(flatPath)) return null;
2779
- return readFileSync(flatPath, "utf-8").trim() || null;
2780
- } catch (err) {
2781
- logger$1.warn({
2782
- flatPath,
2783
- err: err instanceof Error ? err.message : String(err)
2784
- }, "legacy-flat-key migration: read failed — skipping");
2785
- return null;
2786
- }
2787
- }
2788
- /**
2789
- * Import the legacy `${CLEO_HOME}/anthropic-key` flat file into the
2790
- * credential pool, exactly once per CLEO home directory.
2791
- *
2792
- * Idempotent — safe to call from any bootstrap path on every CLI
2793
- * invocation. The marker file guarantees O(1) cost on warm runs.
2794
- *
2795
- * Never throws. All errors are caught and surfaced through the structured
2796
- * logger; the resolver fallback chain still works.
2797
- *
2798
- * @returns A `LegacyFlatKeyImportResult` describing the outcome.
2799
- *
2800
- * @task T9406
2801
- */
2802
- async function importLegacyFlatAnthropicKey() {
2803
- const { flatPath, bakPath, markerPath } = migrationPaths();
2804
- if (existsSync(markerPath)) return {
2805
- status: "marker-present",
2806
- flatPath,
2807
- bakPath: null,
2808
- markerPath
2809
- };
2810
- let existing = null;
2811
- try {
2812
- existing = await getCredentialByLabel("anthropic", LEGACY_FLAT_KEY_LABEL);
2813
- } catch (err) {
2814
- logger$1.warn({ err: err instanceof Error ? err.message : String(err) }, "legacy-flat-key migration: pool lookup failed — skipping this run");
2815
- return {
2816
- status: "no-flat-file",
2817
- flatPath,
2818
- bakPath: null,
2819
- markerPath
2820
- };
2821
- }
2822
- if (existing) {
2823
- writeMarker(markerPath);
2824
- return {
2825
- status: "already-imported",
2826
- flatPath,
2827
- bakPath: null,
2828
- markerPath
2829
- };
2830
- }
2831
- if (!existsSync(flatPath)) {
2832
- writeMarker(markerPath);
2833
- return {
2834
- status: "no-flat-file",
2835
- flatPath,
2836
- bakPath: null,
2837
- markerPath
2838
- };
2839
- }
2840
- const token = readFlatKey(flatPath);
2841
- if (!token) {
2842
- writeMarker(markerPath);
2843
- return {
2844
- status: "empty-flat-file",
2845
- flatPath,
2846
- bakPath: null,
2847
- markerPath
2848
- };
2849
- }
2850
- try {
2851
- await addCredential({
2852
- provider: "anthropic",
2853
- label: LEGACY_FLAT_KEY_LABEL,
2854
- authType: "api_key",
2855
- accessToken: token,
2856
- source: "manual",
2857
- priority: 100
2858
- });
2859
- } catch (err) {
2860
- logger$1.error({
2861
- err: err instanceof Error ? err.message : String(err),
2862
- flatPath
2863
- }, "legacy-flat-key migration: addCredential failed — leaving flat file in place for retry");
2864
- return {
2865
- status: "no-flat-file",
2866
- flatPath,
2867
- bakPath: null,
2868
- markerPath
2869
- };
2870
- }
2871
- try {
2872
- renameSync(flatPath, bakPath);
2873
- } catch (err) {
2874
- logger$1.warn({
2875
- err: err instanceof Error ? err.message : String(err),
2876
- flatPath,
2877
- bakPath
2878
- }, "legacy-flat-key migration: rename to .pre-e1-bak failed — entry imported, will retry rename");
2879
- return {
2880
- status: "imported",
2881
- flatPath,
2882
- bakPath: null,
2883
- markerPath
2884
- };
2885
- }
2886
- writeMarker(markerPath);
2887
- logger$1.info({
2888
- flatPath,
2889
- bakPath,
2890
- markerPath,
2891
- label: LEGACY_FLAT_KEY_LABEL
2892
- }, "legacy-flat-key migration: imported anthropic flat key into pool");
2893
- return {
2894
- status: "imported",
2895
- flatPath,
2896
- bakPath,
2897
- markerPath
2898
- };
2899
- }
2900
- var hasRunInProcess = false;
2901
- /**
2902
- * Run {@link importLegacyFlatAnthropicKey} at most once per Node process.
2903
- *
2904
- * Fire-and-forget: the credentials resolver is synchronous, but the import
2905
- * is async (the credential-store API is async). Callers MUST NOT await
2906
- * this — the helper is fully idempotent and the marker / pool-entry checks
2907
- * ensure later invocations observe the imported entry without re-running.
2908
- *
2909
- * Errors are caught and dropped on the floor (the underlying helper already
2910
- * logs through the structured logger). Returning a no-op promise keeps the
2911
- * call site uniform with {@link ensureGlobalConfigMigrated} from
2912
- * `global-config-migration.ts`.
2913
- *
2914
- * Wired into `resolveCredentials()` so a stale install gets its legacy flat
2915
- * key promoted into the credential pool on first credentials read.
2916
- *
2917
- * Use {@link _resetLegacyFlatKeyImportLatch} in tests to re-arm the latch.
2918
- *
2919
- * @public
2920
- * @task T9407
2921
- */
2922
- function ensureLegacyFlatAnthropicKeyImported() {
2923
- if (hasRunInProcess) return;
2924
- hasRunInProcess = true;
2925
- importLegacyFlatAnthropicKey().catch(() => {});
2926
- }
2927
- //#endregion
2928
- //#region ../core/dist/llm/credentials.js
2929
- /**
2930
- * Centralised credential resolution for the CLEO LLM layer (T1677).
2931
- *
2932
- * ONE canonical entry point: `resolveCredentials(provider, options)`.
2933
- * Every LLM consumer — extraction, dream-cycle, hygiene-scan, dup-detect,
2934
- * observer-reflector, deriver, adapters — MUST use this function.
2935
- *
2936
- * ## 6-tier resolution chain (first match wins)
2937
- *
2938
- * 1. **explicit** — `options.apiKey` passed by the caller
2939
- * 2. **env** — provider-specific environment variable
2940
- * (`ANTHROPIC_API_KEY` | `OPENAI_API_KEY` |
2941
- * `GEMINI_API_KEY` | `MOONSHOT_API_KEY`)
2942
- * 3. **cred-file** — `~/.cleo/llm-credentials.json` (multi-credential
2943
- * pool, file-locked, 0600). T-LLM-CRED Phase 2.
2944
- * 4. **claude-creds** — `~/.claude/.credentials.json` OAuth token
2945
- * (only for `anthropic` provider; Claude Code zero-config)
2946
- * 5. **global-config** — `~/.config/cleo/config.json` (XDG config dir, post-T9405)
2947
- * → `llm.providers.<provider>.apiKey`. The legacy
2948
- * `~/.local/share/cleo/config.json` location is still
2949
- * read as a fallback during the transition window.
2950
- * 6. **project-config** — `.cleo/config.json` → `llm.providers.<provider>.apiKey`
2951
- *
2952
- * Returns `null` when no key is found in any tier.
2953
- *
2954
- * @module llm/credentials
2955
- * @task T1677
2956
- * @epic T1676
2957
- */
2958
- /**
2959
- * Maps each provider transport to its canonical environment variable name.
2960
- *
2961
- * NOTE: `bedrock` is informational — AWS Bedrock uses the AWS SDK credential
2962
- * chain (`AWS_PROFILE` / `~/.aws/credentials` / IAM role / SSO), not a single
2963
- * API-key env var. The credential-pool resolves Bedrock via `authType: 'aws_sdk'`
2964
- * and never reads the `accessToken` field from env.
2965
- *
2966
- * Exported so concrete seeders (e.g. the env seeder under
2967
- * `./credential-seeders/env-seeder.ts`) reuse the same mapping instead of
2968
- * inlining a duplicate. Treat this as the single source of truth for
2969
- * `(provider → env var name)`.
2970
- *
2971
- * @task T9409
2972
- */
2973
- var ENV_VARS = {
2974
- anthropic: "ANTHROPIC_API_KEY",
2975
- openai: "OPENAI_API_KEY",
2976
- gemini: "GEMINI_API_KEY",
2977
- moonshot: "MOONSHOT_API_KEY",
2978
- openrouter: "OPENROUTER_API_KEY",
2979
- bedrock: "AWS_PROFILE",
2980
- deepseek: "DEEPSEEK_API_KEY",
2981
- xai: "XAI_API_KEY",
2982
- groq: "GROQ_API_KEY",
2983
- "kimi-code": "KIMI_CODE_API_KEY",
2984
- ollama: "OLLAMA_HOST"
2985
- };
2986
- /**
2987
- * Path to the global CLEO config file.
2988
- *
2989
- * Canonical location is the XDG **config** dir (`~/.config/cleo/config.json`
2990
- * on Linux) — T9405 moved it here from the data dir to comply with XDG. The
2991
- * data-dir copy is still consulted as a read-only fallback during the
2992
- * transition window via {@link readGlobalProviderKey}; existing installs are
2993
- * migrated in-place on first credentials read by
2994
- * {@link ensureGlobalConfigMigrated}.
2995
- */
2996
- function globalConfigPath() {
2997
- return configDirGlobalConfigPath();
2998
- }
2999
- /** Path to the project-level CLEO config file. */
3000
- function projectConfigPath(projectRoot) {
3001
- return join(projectRoot, process.env["CLEO_DIR"] ?? ".cleo", "config.json");
3002
- }
3003
- /**
3004
- * Tier 4a reader — finds the global provider key, preferring the config-dir
3005
- * location and falling back to the legacy data-dir location during the
3006
- * transition window (T9405).
3007
- *
3008
- * Migration runs at most once per process via {@link ensureGlobalConfigMigrated}
3009
- * so the data-dir fallback is only reachable when the migration itself failed
3010
- * (e.g. permission errors) or when a brand-new file lands in the data dir
3011
- * after the marker was already stamped. Both situations resolve to the legacy
3012
- * copy so users never get a "key disappeared" surprise.
3013
- */
3014
- function readGlobalProviderKey(provider) {
3015
- ensureGlobalConfigMigrated();
3016
- const configDirKey = readProviderKeyFromConfig(globalConfigPath(), provider);
3017
- if (configDirKey) return configDirKey;
3018
- return readProviderKeyFromConfig(legacyGlobalConfigPath(), provider);
3019
- }
3020
- /**
3021
- * Tier 4b backward compat: read the legacy flat key file written by
3022
- * `storeAnthropicApiKey()` before T1677 migrated storage to config.json.
3023
- *
3024
- * File: `~/.local/share/cleo/anthropic-key` (plain text, one line).
3025
- * Returns null when the file does not exist or is empty.
3026
- */
3027
- function readFlatAnthropicKey() {
3028
- try {
3029
- const keyFile = join(getCleoHome(), "anthropic-key");
3030
- if (!existsSync(keyFile)) return null;
3031
- return readFileSync(keyFile, "utf-8").trim() || null;
3032
- } catch {
3033
- return null;
3034
- }
3035
- }
3036
- /**
3037
- * Read `llm.providers[provider].apiKey` from a JSON config file.
3038
- * Returns null on any error or missing key.
3039
- */
3040
- function readProviderKeyFromConfig(configFile, provider) {
3041
- try {
3042
- if (!existsSync(configFile)) return null;
3043
- const raw = readFileSync(configFile, "utf-8");
3044
- const llm = JSON.parse(raw)["llm"];
3045
- if (!llm || typeof llm !== "object") return null;
3046
- const providers = llm["providers"];
3047
- if (!providers || typeof providers !== "object") return null;
3048
- const entry = providers[provider];
3049
- if (!entry || typeof entry !== "object") return null;
3050
- const apiKey = entry["apiKey"];
3051
- if (typeof apiKey === "string" && apiKey.trim()) return apiKey.trim();
3052
- return null;
3053
- } catch {
3054
- return null;
3055
- }
3056
- }
3057
- /**
3058
- * Resolve the API key for a provider using the synchronous tier chain.
3059
- *
3060
- * Resolution order (first non-empty match wins):
3061
- * 1. `options.apiKey` — explicit caller override
3062
- * 2. `ENV_VARS[provider]` environment variable
3063
- * 3. `~/.cleo/llm-credentials.json` — unified credential pool (read-only;
3064
- * seeding is the unified pool's responsibility — the sync path does not
3065
- * re-seed)
3066
- * 4a. `~/.cleo/config.json` → `llm.providers[provider].apiKey` — **deprecated**
3067
- * (emits stderr warning; still resolves during the transition window)
3068
- * 5. `<projectRoot>/.cleo/config.json` → `llm.providers[provider].apiKey` —
3069
- * **rejected** (T-E2-6 footgun kill; emits stderr warning, never resolves)
3070
- *
3071
- * Direct reading of `~/.claude/.credentials.json` was removed in T9413; the
3072
- * `claude-code` seeder is now the sole owner of that file and its imported
3073
- * entries land in the pool, which the tier-3 read picks up.
3074
- *
3075
- * For new code prefer {@link resolveCredentialsAsync}, which delegates to the
3076
- * {@link UnifiedCredentialPool} singleton (`getCredentialPool().pick()`) and
3077
- * triggers a lazy seed pass on first call. The sync variant is retained for
3078
- * call-sites that cannot move to async (e.g. `defaultTransportApiKey`,
3079
- * `resolveModelCredentials`).
3080
- *
3081
- * Never throws. All filesystem errors are caught and treated as "not found".
3082
- *
3083
- * @param provider - The LLM provider transport to resolve credentials for.
3084
- * @param options - Optional overrides and project root for tier 5 warning.
3085
- * @returns A `CredentialResult` with `provider`, `apiKey`, and `source`.
3086
- *
3087
- * @example
3088
- * ```ts
3089
- * const cred = resolveCredentials('anthropic', { projectRoot: cwd });
3090
- * if (!cred.apiKey) throw new Error('No Anthropic key found');
3091
- * ```
3092
- *
3093
- * @task T9413
3094
- * @epic E-CONFIG-AUTH-UNIFY
3095
- */
3096
- function resolveCredentials(provider, options = {}) {
3097
- ensureLegacyFlatAnthropicKeyImported();
3098
- if (options.apiKey?.trim()) {
3099
- const token = options.apiKey.trim();
3100
- return {
3101
- provider,
3102
- apiKey: token,
3103
- source: "explicit",
3104
- authType: detectAuthType(provider, token)
3105
- };
3106
- }
3107
- const envVar = ENV_VARS[provider];
3108
- const envKey = process.env[envVar];
3109
- if (envKey?.trim()) {
3110
- const token = envKey.trim();
3111
- return {
3112
- provider,
3113
- apiKey: token,
3114
- source: "env",
3115
- authType: detectAuthType(provider, token)
3116
- };
3117
- }
3118
- const stored = pickCredentialForProviderSync(provider);
3119
- if (stored) {
3120
- const wireAuthType = stored.authType === "oauth" ? "oauth" : "api_key";
3121
- return {
3122
- provider,
3123
- apiKey: stored.accessToken || null,
3124
- source: "cred-file",
3125
- authType: wireAuthType
3126
- };
3127
- }
3128
- const globalKey = readGlobalProviderKey(provider);
3129
- if (globalKey) {
3130
- warnGlobalConfigApiKeyDeprecated(provider);
3131
- return {
3132
- provider,
3133
- apiKey: globalKey,
3134
- source: "global-config",
3135
- authType: detectAuthType(provider, globalKey)
3136
- };
3137
- }
3138
- if (provider === "anthropic") {
3139
- const flatKey = readFlatAnthropicKey();
3140
- if (flatKey) return {
3141
- provider,
3142
- apiKey: flatKey,
3143
- source: "global-config",
3144
- authType: detectAuthType(provider, flatKey)
3145
- };
3146
- }
3147
- if (options.projectRoot) {
3148
- if (readProviderKeyFromConfig(projectConfigPath(options.projectRoot), provider)) warnProjectConfigApiKeyRejected(provider);
3149
- }
3150
- return {
3151
- provider,
3152
- apiKey: null,
3153
- source: void 0,
3154
- authType: "api_key"
3155
- };
3156
- }
3157
- /**
3158
- * Latch the deprecation warning per (provider, message) so a hot loop does
3159
- * not flood stderr. Keys are simple strings — small, opaque to the rest of
3160
- * the module.
3161
- *
3162
- * @internal
3163
- */
3164
- var WARNED_GLOBAL_CONFIG = /* @__PURE__ */ new Set();
3165
- var WARNED_PROJECT_CONFIG = /* @__PURE__ */ new Set();
3166
- /**
3167
- * Emit a one-shot stderr warning that the global-config `apiKey` path is
3168
- * deprecated. Re-emits only if the latch is cleared (test-only via
3169
- * {@link _resetCredentialDeprecationLatchesForTests}).
3170
- *
3171
- * @internal
3172
- * @task T9413
3173
- */
3174
- function warnGlobalConfigApiKeyDeprecated(provider) {
3175
- if (WARNED_GLOBAL_CONFIG.has(provider)) return;
3176
- WARNED_GLOBAL_CONFIG.add(provider);
3177
- process.stderr.write(`[cleo] DEPRECATED: \`llm.providers.${provider}.apiKey\` in the global config.json is deprecated and will be removed. Migrate with \`cleo auth add ${provider}\` or \`cleo llm add\`.\n`);
3178
- }
3179
- /**
3180
- * Emit a one-shot stderr warning that the project-config `apiKey` path is
3181
- * rejected. The key is NOT resolved — operators must migrate.
3182
- *
3183
- * @internal
3184
- * @task T9413
3185
- */
3186
- function warnProjectConfigApiKeyRejected(provider) {
3187
- if (WARNED_PROJECT_CONFIG.has(provider)) return;
3188
- WARNED_PROJECT_CONFIG.add(provider);
3189
- process.stderr.write(`[cleo] REJECTED: \`llm.providers.${provider}.apiKey\` in \`.cleo/config.json\` (project-scoped) is a security footgun and is no longer honoured. Run \`cleo auth migrate-project-secrets\` to move the key into the unified credential pool.\n`);
3190
- }
3191
- /**
3192
- * Detect whether a credential string is an Anthropic OAuth token by prefix.
3193
- *
3194
- * The Claude Code OAuth flow issues tokens with the prefixes:
3195
- * - `sk-ant-oat-*` — access token (used for API calls)
3196
- * - `sk-ant-ort-*` — refresh token (rare in direct calls, but recognized for safety)
3197
- *
3198
- * Every other provider in Phase 1 uses `api_key` authentication, so any
3199
- * non-Anthropic credential is treated as `api_key`. Tokens loaded from
3200
- * `claude-creds` are always `oauth` regardless of prefix (handled by the caller).
3201
- */
3202
- function detectAuthType(provider, token) {
3203
- if (provider !== "anthropic") return "api_key";
3204
- if (token.startsWith("sk-ant-oat-") || token.startsWith("sk-ant-ort-")) return "oauth";
3205
- return "api_key";
3206
- }
3207
- /**
3208
- * Build the authentication HTTP headers for a resolved credential.
3209
- *
3210
- * For raw-fetch call-sites (e.g. memory/sleep-consolidation, memory/observer-reflector)
3211
- * this returns the full bag of provider-specific auth headers including the
3212
- * `anthropic-version` or `anthropic-beta` markers that Anthropic requires.
3213
- * The caller still owns `Content-Type` and the request body.
3214
- *
3215
- * Returns an empty object when `cred.apiKey` is null — callers should never
3216
- * reach this helper without verifying they have a credential, but the no-op
3217
- * fallback avoids accidental `undefined` header injection.
3218
- *
3219
- * @task T-LLM-CRED-CENTRALIZATION Phase 1
3220
- */
3221
- function authHeaders(cred) {
3222
- if (!cred.apiKey) return {};
3223
- if (cred.provider === "anthropic") {
3224
- if (cred.authType === "oauth") return {
3225
- Authorization: `Bearer ${cred.apiKey}`,
3226
- "anthropic-beta": "oauth-2025-04-20",
3227
- "anthropic-version": "2023-06-01"
3228
- };
3229
- return {
3230
- "x-api-key": cred.apiKey,
3231
- "anthropic-version": "2023-06-01"
3232
- };
3233
- }
3234
- return { Authorization: `Bearer ${cred.apiKey}` };
3235
- }
3236
- /**
3237
- * Build the provider auth headers AT THE WIRE directly from a sealed credential
3238
- * handle — the E10 boundary primitive (T11754 · AC2).
3239
- *
3240
- * ## Why this exists
3241
- *
3242
- * The pre-E10 / interim pattern materialized the plaintext into a caller-visible
3243
- * variable first — `const token = (await sealed.fetch()).value;` — and only then
3244
- * called {@link authHeaders}. That intermediate `token` binding is a leak surface:
3245
- * any code added between the `fetch()` and the header build could log, serialize,
3246
- * or forward the bare secret.
3247
- *
3248
- * `authHeadersFromSealed` collapses those two steps into ONE chokepoint. It is
3249
- * the SOLE place (alongside daemon worker-injection) that invokes
3250
- * {@link SealedCredential.fetch} — the crypto decrypt happens inside the handle's
3251
- * `fetch()`, the materialized {@link DecryptedToken} is consumed in-place to build
3252
- * the `x-api-key` / `Authorization: Bearer` headers (per provider + scheme), and
3253
- * the plaintext goes out of scope WITHOUT ever being returned, logged, or bound
3254
- * to a caller variable. Callers receive ONLY the finished header bag.
3255
- *
3256
- * Invoke this ONLY at a wire boundary — `transportForProvider` /
3257
- * `session-factory.ts:56`, the raw-fetch consumers (hygiene-scan,
3258
- * duplicate-detector), or daemon worker-injection. Never to surface a key up the
3259
- * resolver stack.
3260
- *
3261
- * @param sealed - The opaque credential handle returned by the resolver.
3262
- * @param authType - The auth scheme to present the credential with (mirrors the
3263
- * resolved `credential.authType`). `'aws_sdk'` yields an empty bag — the AWS
3264
- * SDK injects credentials out-of-band, so there is no header to build.
3265
- * @returns The provider-specific auth headers. The plaintext token is consumed
3266
- * internally and never escapes this function.
3267
- * @task T11754
3268
- */
3269
- async function authHeadersFromSealed(sealed, authType) {
3270
- if (authType === "aws_sdk") return {};
3271
- const decrypted = await sealed.fetch();
3272
- return authHeaders({
3273
- provider: sealed.provider,
3274
- apiKey: decrypted.value,
3275
- authType: authType === "oauth" ? "oauth" : "api_key"
3276
- });
3277
- }
3278
- //#endregion
3279
- //#region ../core/dist/llm/credentials-store.js
3280
- /**
3281
- * Multi-credential pool storage for the CLEO LLM layer (T-LLM-CRED Phase 2).
3282
- *
3283
- * Persists a versioned list of provider credentials at
3284
- * `~/.cleo/llm-credentials.json` (XDG-aware, resolved via `getCleoHome()` so
3285
- * the test-only `XDG_DATA_HOME` override applies identically to every CLEO
3286
- * global file).
3287
- *
3288
- * ## Goals
3289
- *
3290
- * - Multiple credentials per provider (label → entry), each with priority,
3291
- * disabled-flag, expiry, and a free-form `metadata` bag.
3292
- * - Strict 0600 perms on every successful write.
3293
- * - Cross-process locking via `proper-lockfile` reused through `withLock`.
3294
- * - Atomic writes via `writeJsonFileAtomic` (temp → rename + numbered backup).
3295
- * - Synchronous read path so the resolver in `credentials.ts` can call it
3296
- * from its existing sync function.
3297
- *
3298
- * Design lock-in (per plan):
3299
- * • `provider` MUST be a valid `ModelTransport`.
3300
- * • `(provider, label)` is the uniqueness key — `addCredential` upserts.
3301
- * • `priority` defaults to `max(existing) + 10` so new entries lose to all
3302
- * existing ones unless an explicit priority is supplied.
3303
- * • Round-robin picker keeps state in-memory per provider — sufficient for
3304
- * a single-process orchestrator; durable round-robin is a future change.
3305
- * • Storage-time auth type widens the wire-time `AuthType` to include
3306
- * `'aws_sdk'` for forward-compat with Bedrock. `credentials.ts` narrows
3307
- * `'aws_sdk' → 'api_key'` until Phase 3 widens the on-wire union.
3308
- *
3309
- * Reference: Hermes `credential_pool.py:32-33` defined the on-disk schema
3310
- * we mirror here.
3311
- *
3312
- * @module llm/credentials-store
3313
- * @task T9257
3314
- * @epic T-LLM-CRED-CENTRALIZATION
3315
- */
3316
- var logger = getLogger("llm-credentials-store");
3317
- /**
3318
- * Absolute path of the credential store file.
3319
- *
3320
- * Resolved through `getCleoHome()` so XDG overrides apply uniformly with
3321
- * `credentials.ts`'s global-config tier.
3322
- *
3323
- * @task T9257
3324
- */
3325
- function credentialsStorePath() {
3326
- return join(getCleoHome(), "llm-credentials.json");
3327
- }
3328
- /**
3329
- * Ensure the store file exists with a parseable empty-store body.
3330
- *
3331
- * `withLock` (file-utils) writes a zero-byte file when none exists so
3332
- * `proper-lockfile` can hold the lock. The subsequent `readJsonFile` then
3333
- * throws SyntaxError on the empty content. To avoid that, we pre-seed the
3334
- * file with `{"version":1,"defaultStrategy":"priorityWithFallback",
3335
- * "credentials":[]}` BEFORE acquiring the lock. The pre-seed is itself
3336
- * race-tolerant: `writeFileSync({flag:'wx'})` makes creation idempotent
3337
- * across concurrent callers — only the first one writes; the rest get
3338
- * EEXIST and continue.
3339
- *
3340
- * @task T9257
3341
- */
3342
- function ensureFileInitialized(path) {
3343
- if (existsSync(path)) return;
3344
- const dir = dirname(path);
3345
- if (!existsSync(dir)) mkdirSync(dir, {
3346
- recursive: true,
3347
- mode: 448
3348
- });
3349
- else try {
3350
- chmodSync(dir, 448);
3351
- } catch {}
3352
- try {
3353
- writeFileSync(path, `${JSON.stringify(emptyStore(), null, 2)}\n`, {
3354
- encoding: "utf-8",
3355
- flag: "wx",
3356
- mode: 384
3357
- });
3358
- } catch (err) {
3359
- if (err.code !== "EEXIST") throw err;
3360
- }
3361
- }
3362
- /**
3363
- * S-02 (CWE-367 TOCTOU rename→chmod): the previous `enforce0600(path)`
3364
- * helper used to chmod the live file AFTER `withLock` released its
3365
- * lock. That left a window in which the temp file (default 0o644)
3366
- * was the live file, no lock held, with secrets in it. The window
3367
- * is now closed at the source: `writeJsonFileAtomic({mode: 0o600})`
3368
- * creates the temp file pre-set to 0o600, so the post-rename file
3369
- * is born 0o600 atomically. No follow-up chmod is needed.
3370
- *
3371
- * Kept here as a doc-only stub so a `git log --grep=enforce0600`
3372
- * search points future readers at this rationale; the function and
3373
- * every call-site are removed below.
3374
- *
3375
- * @task T9257 — security review S-02
3376
- */
3377
- /**
3378
- * Warn once when the file is loosely permissioned.
3379
- *
3380
- * Read-side check — we never refuse to read, but we surface the risk via
3381
- * the project logger so an operator can investigate.
3382
- *
3383
- * @task T9257
3384
- */
3385
- var _warnedLoosePerms = false;
3386
- function checkPermsOnRead(path) {
3387
- if (_warnedLoosePerms) return;
3388
- try {
3389
- const mode = statSync(path).mode & 511;
3390
- if (mode !== 384 && mode !== 0) {
3391
- logger.warn({
3392
- path,
3393
- mode: mode.toString(8)
3394
- }, "llm-credentials-store: file perms looser than 0600");
3395
- _warnedLoosePerms = true;
3396
- }
3397
- } catch {}
3398
- }
3399
- /**
3400
- * Default on-disk shape when no file exists yet.
3401
- */
3402
- function emptyStore() {
3403
- return {
3404
- version: 1,
3405
- defaultStrategy: "priorityWithFallback",
3406
- credentials: []
3407
- };
3408
- }
3409
- /**
3410
- * Synchronous file read + parse. Never throws.
3411
- *
3412
- * Returns an empty store when the file is missing, unreadable, malformed,
3413
- * or has the wrong `version`.
3414
- *
3415
- * @task T9257
3416
- */
3417
- function readStoreSync() {
3418
- const path = credentialsStorePath();
3419
- let raw;
3420
- try {
3421
- raw = readFileSync(path, "utf-8");
3422
- } catch {
3423
- return emptyStore();
3424
- }
3425
- checkPermsOnRead(path);
3426
- if (!raw.trim()) return emptyStore();
3427
- try {
3428
- const parsed = JSON.parse(raw);
3429
- if (parsed.version !== 1 || !Array.isArray(parsed.credentials)) return emptyStore();
3430
- return {
3431
- version: 1,
3432
- defaultStrategy: parsed.defaultStrategy ?? "priorityWithFallback",
3433
- credentials: parsed.credentials.filter((c) => isStoredCredential(c))
3434
- };
3435
- } catch {
3436
- return emptyStore();
3437
- }
3438
- }
3439
- /**
3440
- * Type-narrow check used during read to drop malformed entries silently.
3441
- */
3442
- function isStoredCredential(value) {
3443
- if (!value || typeof value !== "object") return false;
3444
- const v = value;
3445
- const provider = v["provider"];
3446
- const label = v["label"];
3447
- const authType = v["authType"];
3448
- const accessToken = v["accessToken"];
3449
- const validProvider = provider === "anthropic" || provider === "openai" || provider === "gemini" || provider === "moonshot" || provider === "kimi-code" || provider === "openrouter" || provider === "bedrock" || provider === "deepseek" || provider === "xai" || provider === "groq";
3450
- const validAuth = authType === "api_key" || authType === "oauth" || authType === "aws_sdk";
3451
- if (!validProvider) return false;
3452
- if (typeof label !== "string" || !label) return false;
3453
- if (!validAuth) return false;
3454
- if (typeof accessToken !== "string") return false;
3455
- return true;
3456
- }
3457
- /**
3458
- * Round-robin cursor map (per provider).
3459
- *
3460
- * In-memory by design: the store is single-process for the orchestrator
3461
- * fleet, and durable round-robin would require an additional write-on-pick
3462
- * which is not worth the cost in Phase 2.
3463
- */
3464
- var _rrIndex = /* @__PURE__ */ new Map();
3465
- /**
3466
- * Filter `credentials` to `(provider, !disabled, !expired)` and apply
3467
- * `preferLabel` when present.
3468
- *
3469
- * Returns `[]` when nothing eligible remains. Otherwise returns entries
3470
- * sorted by priority ascending (stable for equal priorities).
3471
- *
3472
- * @task T9257
3473
- */
3474
- function eligibleForProvider(data, provider, preferLabel) {
3475
- const now = Date.now();
3476
- const all = data.credentials.filter((c) => {
3477
- if (c.provider !== provider) return false;
3478
- if (c.disabled === true) return false;
3479
- if (typeof c.expiresAt === "number" && c.expiresAt > 0 && c.expiresAt <= now) {
3480
- logger.info({
3481
- provider: c.provider,
3482
- label: c.label,
3483
- expiresAt: c.expiresAt
3484
- }, "cred-file: skipping expired entry");
3485
- return false;
3486
- }
3487
- return true;
3488
- });
3489
- if (preferLabel) {
3490
- const exact = all.find((c) => c.label === preferLabel);
3491
- return exact ? [exact] : [];
3492
- }
3493
- return [...all].sort((a, b) => a.priority - b.priority);
3494
- }
3495
- /**
3496
- * Synchronous variant of `pickCredentialForProvider`. Called from
3497
- * `credentials.ts`'s sync `resolveCredentials()` tier-3 block. See the
3498
- * async wrapper below for the canonical public API.
3499
- *
3500
- * Strategy resolution falls back to the store's `defaultStrategy` when
3501
- * `opts.strategy` is not provided. Empty / disabled / expired pools yield
3502
- * `null`.
3503
- *
3504
- * @task T9257
3505
- */
3506
- function pickCredentialForProviderSync(provider, opts = {}) {
3507
- const data = readStoreSync();
3508
- const eligible = eligibleForProvider(data, provider, opts.preferLabel);
3509
- if (eligible.length === 0) return null;
3510
- if (opts.preferLabel) return eligible[0] ?? null;
3511
- switch (opts.strategy ?? data.defaultStrategy ?? "priorityWithFallback") {
3512
- case "priorityOnly":
3513
- case "priorityWithFallback": return eligible[0] ?? null;
3514
- case "roundRobin": {
3515
- const next = (_rrIndex.get(provider) ?? 0) % eligible.length;
3516
- _rrIndex.set(provider, next + 1);
3517
- return eligible[next] ?? null;
3518
- }
3519
- default: return eligible[0] ?? null;
3520
- }
3521
- }
3522
- /**
3523
- * List credentials in the pool, optionally filtered to a single provider.
3524
- *
3525
- * Returns `[]` when the file does not exist. Never throws.
3526
- *
3527
- * @task T9257
3528
- */
3529
- async function listCredentials(provider) {
3530
- const data = readStoreSync();
3531
- if (!provider) return [...data.credentials];
3532
- return data.credentials.filter((c) => c.provider === provider);
3533
- }
3534
- /**
3535
- * Look up a single credential by `(provider, label)`. Returns `null` when
3536
- * no match exists.
3537
- *
3538
- * @task T9257
3539
- */
3540
- async function getCredentialByLabel(provider, label) {
3541
- return readStoreSync().credentials.find((c) => c.provider === provider && c.label === label) ?? null;
3542
- }
3543
- /**
3544
- * Upsert a credential — replaces any existing `(provider, label)` pair.
3545
- *
3546
- * - When `input.priority` is omitted, the new entry receives
3547
- * `max(existing priorities) + 10` so it ranks lowest by default.
3548
- * - Acquires the file lock via `withLock`; safe under concurrent writers.
3549
- * - chmod 0600 is enforced on the written file.
3550
- *
3551
- * Returns the inserted (or replaced) entry as it now lives in the file.
3552
- *
3553
- * @task T9257
3554
- */
3555
- async function addCredential(input) {
3556
- const path = credentialsStorePath();
3557
- ensureFileInitialized(path);
3558
- let inserted = null;
3559
- await withLock(path, (current) => {
3560
- const data = current ?? emptyStore();
3561
- if (data.version !== 1) {
3562
- data.version = 1;
3563
- data.credentials = [];
3564
- }
3565
- data.defaultStrategy ??= "priorityWithFallback";
3566
- const remaining = data.credentials.filter((c) => !(c.provider === input.provider && c.label === input.label));
3567
- const maxPriority = remaining.reduce((m, c) => c.priority > m ? c.priority : m, -10);
3568
- const priority = typeof input.priority === "number" ? input.priority : maxPriority + 10;
3569
- const next = {
3570
- provider: input.provider,
3571
- label: input.label,
3572
- authType: input.authType,
3573
- accessToken: input.accessToken,
3574
- expiresAt: input.expiresAt ?? null,
3575
- priority,
3576
- source: input.source,
3577
- baseUrl: input.baseUrl ?? null,
3578
- extraHeaders: input.extraHeaders,
3579
- metadata: input.metadata,
3580
- lastStatus: input.lastStatus,
3581
- lastErrorCode: input.lastErrorCode,
3582
- lastErrorResetAt: input.lastErrorResetAt,
3583
- requestCount: input.requestCount,
3584
- disabled: input.disabled ?? false,
3585
- ...input.refreshToken !== void 0 && { refreshToken: input.refreshToken }
3586
- };
3587
- data.credentials = [...remaining, next];
3588
- inserted = next;
3589
- return data;
3590
- }, { mode: 384 });
3591
- if (!inserted) throw new Error("credentials-store: invariant violation — insert was not recorded");
3592
- return inserted;
3593
- }
3594
- /**
3595
- * Remove a credential by `(provider, label)`.
3596
- *
3597
- * Returns `true` when a matching entry was found and removed, `false`
3598
- * otherwise. Does NOT create the file when it does not yet exist.
3599
- *
3600
- * @task T9257
3601
- */
3602
- async function removeCredential(provider, label) {
3603
- const path = credentialsStorePath();
3604
- if (!readStoreSync().credentials.some((c) => c.provider === provider && c.label === label)) return false;
3605
- ensureFileInitialized(path);
3606
- let removed = false;
3607
- await withLock(path, (current) => {
3608
- const data = current ?? emptyStore();
3609
- if (data.version !== 1 || !Array.isArray(data.credentials)) return emptyStore();
3610
- const before = data.credentials.length;
3611
- data.credentials = data.credentials.filter((c) => !(c.provider === provider && c.label === label));
3612
- removed = data.credentials.length < before;
3613
- data.defaultStrategy ??= "priorityWithFallback";
3614
- return data;
3615
- }, { mode: 384 });
3616
- return removed;
3617
- }
3618
- /**
3619
- * Public async picker — thin wrapper over `pickCredentialForProviderSync`.
3620
- *
3621
- * The sync variant exists because the resolver in `credentials.ts` is
3622
- * synchronous; both paths share the same filter + strategy logic.
3623
- *
3624
- * @task T9257
3625
- */
3626
- async function pickCredentialForProvider(provider, opts = {}) {
3627
- return pickCredentialForProviderSync(provider, opts);
3628
- }
3629
- //#endregion
3630
- //#region ../core/dist/llm/oauth/device-code.js
3631
- /**
3632
- * Build the `DeviceCodeConfig` for Kimi Code (kimi.com/code).
3633
- *
3634
- * Authentication endpoints are hosted at `auth.kimi.com` (override via
3635
- * `KIMI_CODE_OAUTH_HOST` env var). The shared community client ID is reused
3636
- * by kimi-cli and other integrations — no private registration required.
3637
- *
3638
- * Token lifecycle:
3639
- * - Access token: ~15 minutes
3640
- * - Refresh token: ~30 days
3641
- * - Recommended refresh strategy: 50% of lifetime or 300s, whichever is larger
3642
- *
3643
- * After OAuth completes, the resulting bearer token targets
3644
- * `https://api.kimi.com/coding` (Anthropic Messages protocol). The six
3645
- * mandatory `X-Msh-*` headers — built by {@link getKimiCodeMshHeaders} — MUST
3646
- * be merged into every chat request alongside the bearer token.
3647
- *
3648
- * @returns Device-code configuration for Kimi Code OAuth.
3649
- *
3650
- * @task T9321
3651
- * @epic T9261 T-LLM-CRED-CENTRALIZATION
3652
- * @see https://github.com/gsd-build/gsd-2/issues/4642 — design reference
3653
- * @see MoonshotAI/kimi-cli `src/kimi_cli/auth/oauth.py` — protocol reference
3654
- */
3655
- function getKimiCodeDeviceCodeConfig() {
3656
- const host = process.env["KIMI_CODE_OAUTH_HOST"] ?? "https://auth.kimi.com";
3657
- return {
3658
- provider: "kimi-code",
3659
- deviceCodeUrl: `${host}/api/oauth/device_authorization`,
3660
- tokenUrl: `${host}/api/oauth/token`,
3661
- clientId: "17e5f671-d194-4dfb-9706-5516cb48c098"
3662
- };
3663
- }
3664
- //#endregion
3665
- //#region ../core/dist/llm/credential-pool.js
3666
- /**
3667
- * CredentialPool — rotation + cooldown manager for the CLEO LLM credential
3668
- * layer (T-LLM-CRED-CENTRALIZATION Phase 3 / T9265).
3669
- *
3670
- * Ports the Hermes `credential_pool.py` rotation strategies and cooldown
3671
- * clocks to TypeScript, re-using the existing `credentials-store.ts` I/O
3672
- * primitives (addCredential, listCredentials, getCredentialByLabel) for all
3673
- * persistence so no new file-locking or write paths are introduced.
3674
- *
3675
- * ## Rotation strategies
3676
- *
3677
- * - `fill_first` (default): Iterate entries sorted by `priority` descending
3678
- * (higher = preferred); return the first that is not in active cooldown.
3679
- * - `round_robin`: Advance a per-provider in-memory cursor; skip cooled-down
3680
- * entries until a healthy one is found.
3681
- * - `least_used`: Pick the entry with the lowest `requestCount` that is not
3682
- * in active cooldown; tie-break by `priority` descending.
3683
- *
3684
- * ## Cooldown semantics (mirrored from Hermes)
3685
- *
3686
- * | HTTP code | Cooldown |
3687
- * |-----------|-----------|
3688
- * | 401 | 5 minutes |
3689
- * | 402 | 5 minutes |
3690
- * | 429 | 1 hour |
3691
- * | 5xx | 60 seconds|
3692
- * | other | 60 seconds|
3693
- *
3694
- * Reference: `hermes-agent/agent/credential_pool.py:92-1095`
3695
- *
3696
- * @module llm/credential-pool
3697
- * @task T9265
3698
- * @epic T-LLM-CRED-CENTRALIZATION
3699
- */
3700
- /** Cooldown applied to 401 (auth) and 402 (billing) errors: 5 minutes. */
3701
- var COOLDOWN_AUTH_MS = 300 * 1e3;
3702
- /** Cooldown applied to 429 (rate-limited) errors: 1 hour. */
3703
- var COOLDOWN_RATE_LIMIT_MS = 3600 * 1e3;
3704
- /** Default cooldown for 5xx and all other error codes: 60 seconds. */
3705
- var COOLDOWN_DEFAULT_MS = 60 * 1e3;
3706
- /**
3707
- * Proactive refresh threshold: at least 300 seconds (5 minutes) before expiry.
3708
- *
3709
- * The proactive window is `max(expiresIn * 0.5, PROACTIVE_REFRESH_FLOOR_MS)`.
3710
- * For Kimi Code's ~15 min access tokens: 50% = 450s > 300s → uses 450s.
3711
- * For very short tokens (< 600s): 50% < 300s → floor kicks in at 300s.
3712
- *
3713
- * @task T9323
3714
- */
3715
- var PROACTIVE_REFRESH_FLOOR_MS = 3e5;
3716
- /**
3717
- * Thrown by `CredentialPool.pick()` when every entry for the provider is
3718
- * currently in active cooldown (i.e., the pool is fully exhausted).
3719
- *
3720
- * Callers should surface this as a retryable, time-bounded failure. The
3721
- * earliest retry time can be derived from `minResetAt` (epoch ms).
3722
- *
3723
- * @task T9265
3724
- */
3725
- var PoolExhaustedError = class extends Error {
3726
- provider;
3727
- poolSize;
3728
- minResetAt;
3729
- /** Stable LAFS error code. */
3730
- code = "E_LLM_POOL_EXHAUSTED";
3731
- /**
3732
- * @param provider - The provider whose pool is exhausted.
3733
- * @param poolSize - Total number of entries in the pool.
3734
- * @param minResetAt - Epoch ms of the earliest cooldown expiry (may be 0 if
3735
- * all entries lack a reset timestamp).
3736
- */
3737
- constructor(provider, poolSize, minResetAt) {
3738
- super(`E_LLM_POOL_EXHAUSTED: all ${poolSize} credential(s) for provider '${provider}' are in active cooldown. Earliest reset: ${minResetAt > 0 ? new Date(minResetAt).toISOString() : "unknown"}.`);
3739
- this.provider = provider;
3740
- this.poolSize = poolSize;
3741
- this.minResetAt = minResetAt;
3742
- this.name = "PoolExhaustedError";
3743
- }
3744
- };
3745
- /**
3746
- * Compute the cooldown duration in milliseconds for a given HTTP error code.
3747
- *
3748
- * Mirrors `hermes-agent/agent/credential_pool.py:_exhausted_ttl`.
3749
- *
3750
- * @param errorCode - HTTP status code that triggered the exhaustion.
3751
- * @returns Cooldown duration in milliseconds.
3752
- *
3753
- * @task T9265
3754
- */
3755
- function cooldownMs(errorCode) {
3756
- if (errorCode === 401 || errorCode === 402) return COOLDOWN_AUTH_MS;
3757
- if (errorCode === 429) return COOLDOWN_RATE_LIMIT_MS;
3758
- return COOLDOWN_DEFAULT_MS;
3759
- }
3760
- /**
3761
- * Return `true` when the credential is currently in active cooldown.
3762
- *
3763
- * A credential is in cooldown when `lastErrorResetAt` is set AND its value is
3764
- * strictly greater than `Date.now()`.
3765
- *
3766
- * @param credential - The entry to check.
3767
- * @returns `true` if the entry should be skipped by the picker.
3768
- *
3769
- * @task T9265
3770
- */
3771
- function isInCooldown(credential) {
3772
- return typeof credential.lastErrorResetAt === "number" && Date.now() < credential.lastErrorResetAt;
3773
- }
3774
- /**
3775
- * Sort entries by priority descending (higher priority = more preferred).
3776
- *
3777
- * `StoredCredential.priority` is stored ascending (lower = higher priority) in
3778
- * the existing store convention. The pool inverts this so that the
3779
- * highest-priority entry is first in the returned array.
3780
- *
3781
- * @param entries - Unsorted credential entries.
3782
- * @returns New array sorted by priority descending.
3783
- *
3784
- * @task T9265
3785
- */
3786
- function sortByPriorityDesc(entries) {
3787
- return [...entries].sort((a, b) => b.priority - a.priority);
3788
- }
3789
- /**
3790
- * Per-`(provider, label)` in-flight refresh promise.
3791
- *
3792
- * When N concurrent callers arrive with the same expired credential, only the
3793
- * first one POSTs to the token endpoint. All others await the same Promise so
3794
- * the endpoint is called exactly once per concurrent burst.
3795
- *
3796
- * The key is `"${provider}::${label}"`. The map is cleared when the Promise
3797
- * resolves (or rejects) so the next caller after a settled refresh starts a
3798
- * fresh attempt.
3799
- *
3800
- * @internal
3801
- * @task T11986
3802
- */
3803
- var _refreshInFlight = /* @__PURE__ */ new Map();
3804
- /**
3805
- * TTL for a negative-cache entry after a failed refresh attempt (30 seconds).
3806
- *
3807
- * Prevents hammering the token endpoint when the refresh token is invalid or
3808
- * the server is returning errors. After the TTL the next resolution attempt
3809
- * will retry the refresh.
3810
- *
3811
- * @internal
3812
- * @task T11986
3813
- */
3814
- var REFRESH_NEGATIVE_CACHE_TTL_MS = 3e4;
3815
- /**
3816
- * Negative-cache map: `"${provider}::${label}" → epoch ms when the failed entry expires`.
3817
- *
3818
- * An entry in this map means "the last refresh of this credential failed; skip
3819
- * until the timestamp has passed".
3820
- *
3821
- * @internal
3822
- * @task T11986
3823
- */
3824
- var _refreshNegativeCache = /* @__PURE__ */ new Map();
3825
- /**
3826
- * Refresh every expired-but-refreshable OAuth credential for `provider`,
3827
- * with single-flight coalescing and a negative-cache on failure.
3828
- *
3829
- * ## Design (T11986 · DHQ-087)
3830
- *
3831
- * Called by `cross-provider-selector.ts` **before** the provisioning probe so
3832
- * that an expired OAuth credential is renewed in-place and the selector sees
3833
- * a valid token instead of filtering the provider as "not-provisioned".
3834
- *
3835
- * Single-flight: concurrent callers sharing the same `(provider, label)` key
3836
- * coalesce on one in-flight Promise — the token endpoint is called at most
3837
- * once per concurrent burst. After the Promise settles the key is deleted so
3838
- * the next caller starts fresh.
3839
- *
3840
- * Negative-cache: a failed refresh stamps a 30-second suppression window.
3841
- * During that window subsequent calls skip the refresh attempt rather than
3842
- * hammering the endpoint with doomed requests.
3843
- *
3844
- * On total failure (every eligible entry failed): returns an `actionableHint`
3845
- * with the exact re-login command (`cleo login ${provider}`).
3846
- *
3847
- * Note: `CredentialPool._refreshOAuthCredential` swallows errors silently by
3848
- * design (its caller's retry path handles 401s). Here we need to surface
3849
- * failures so callers can show actionable hints. We therefore attempt the
3850
- * refresh via a fresh `CredentialPool` instance and check the credential store
3851
- * for an updated `expiresAt` after the attempt to distinguish success from
3852
- * failure.
3853
- *
3854
- * @param provider - The LLM transport provider to refresh credentials for.
3855
- * @returns Refresh outcome with counts, last error, and actionable hint.
3856
- *
3857
- * @task T11986
3858
- */
3859
- async function refreshExpiredOAuthForProvider(provider) {
3860
- const entries = await listCredentials(provider);
3861
- const now = Date.now();
3862
- let attempted = 0;
3863
- let refreshed = 0;
3864
- let lastError = null;
3865
- for (const entry of entries) {
3866
- if (entry.authType !== "oauth") continue;
3867
- if (!entry.refreshToken) continue;
3868
- if (entry.disabled === true) continue;
3869
- if (!(typeof entry.expiresAt === "number" && entry.expiresAt > 0 && entry.expiresAt <= now + PROACTIVE_REFRESH_FLOOR_MS)) continue;
3870
- const key = `${provider}::${entry.label}`;
3871
- const negExpiry = _refreshNegativeCache.get(key);
3872
- if (typeof negExpiry === "number" && now < negExpiry) {
3873
- attempted += 1;
3874
- lastError = lastError ?? /* @__PURE__ */ new Error(`OAuth refresh suppressed for ${provider}::${entry.label} (negative cache active)`);
3875
- continue;
3876
- }
3877
- attempted += 1;
3878
- let inFlight = _refreshInFlight.get(key);
3879
- if (!inFlight) {
3880
- const pool = new CredentialPool(provider);
3881
- const expiresAtBefore = entry.expiresAt;
3882
- inFlight = pool.proactiveRefresh(entry.label).then(async (wasAttempted) => {
3883
- if (!wasAttempted) return true;
3884
- const after = await getCredentialByLabel(provider, entry.label);
3885
- if (!(after !== null && typeof after.expiresAt === "number" && after.expiresAt > (expiresAtBefore ?? 0))) throw new Error(`OAuth refresh token exchange failed for ${provider}::${entry.label} — the stored token may be revoked. Run \`cleo login ${provider}\` to re-authenticate.`);
3886
- return true;
3887
- }).finally(() => {
3888
- _refreshInFlight.delete(key);
3889
- });
3890
- _refreshInFlight.set(key, inFlight);
3891
- }
3892
- try {
3893
- await inFlight;
3894
- refreshed += 1;
3895
- } catch (err) {
3896
- lastError = err instanceof Error ? err : new Error(String(err));
3897
- _refreshNegativeCache.set(key, Date.now() + REFRESH_NEGATIVE_CACHE_TTL_MS);
3898
- }
3899
- }
3900
- const actionableHint = attempted > 0 && refreshed === 0 && lastError !== null ? `OAuth refresh failed for provider '${provider}'. Run \`cleo login ${provider}\` to re-authenticate.` : null;
3901
- return {
3902
- attempted,
3903
- refreshed,
3904
- lastError,
3905
- actionableHint
3906
- };
3907
- }
3908
- /**
3909
- * Pool manager that wraps the credential store for a single provider.
3910
- *
3911
- * Responsibilities:
3912
- * - Pick a non-cooldown credential using one of three rotation strategies.
3913
- * - Persist `lastStatus`, `lastErrorCode`, `lastErrorResetAt`, and
3914
- * `requestCount` back to the store via `addCredential` upsert on every
3915
- * state change.
3916
- * - Maintain an in-memory round-robin cursor (per instance) for `round_robin`
3917
- * strategy calls.
3918
- *
3919
- * Instantiate one `CredentialPool` per provider. The pool is stateless beyond
3920
- * the RR cursor — all durable state lives in the credential store file.
3921
- *
3922
- * @example
3923
- * ```ts
3924
- * const pool = new CredentialPool('anthropic');
3925
- * const { credential } = await pool.pick({ strategy: 'round_robin' });
3926
- * try {
3927
- * await callApi(credential.accessToken);
3928
- * await pool.markOk(credential.label);
3929
- * } catch (err) {
3930
- * await pool.markExhausted(credential.label, err.status ?? 500);
3931
- * }
3932
- * ```
3933
- *
3934
- * @task T9265
3935
- */
3936
- var CredentialPool = class {
3937
- provider;
3938
- /**
3939
- * In-memory round-robin cursor keyed on provider (one entry per instance
3940
- * since each instance is scoped to one provider). Reset to 0 on construction.
3941
- */
3942
- rrCursor = 0;
3943
- /**
3944
- * @param provider - The LLM transport this pool manages.
3945
- */
3946
- constructor(provider) {
3947
- this.provider = provider;
3948
- }
3949
- /**
3950
- * Pick a non-cooldown credential for the provider using the requested
3951
- * rotation strategy.
3952
- *
3953
- * Side-effect: increments `requestCount` on the picked entry and persists
3954
- * the change via `addCredential` upsert.
3955
- *
3956
- * @param opts - Optional pick options (strategy). Defaults to `fill_first`.
3957
- * @returns The selected credential plus pool metadata.
3958
- * @throws {PoolExhaustedError} When all entries are in active cooldown.
3959
- *
3960
- * @task T9265
3961
- */
3962
- async pick(opts) {
3963
- const nonDisabled = (await listCredentials(this.provider)).filter((c) => !c.disabled);
3964
- const eligible = [];
3965
- for (const entry of nonDisabled) if (await rateLimitRemaining(this.provider, entry.label) == null) eligible.push(entry);
3966
- const poolSize = nonDisabled.length;
3967
- if (nonDisabled.length === 0) throw new PoolExhaustedError(this.provider, 0, 0);
3968
- if (eligible.length === 0) {
3969
- const minResetAt = nonDisabled.map((c) => c.lastErrorResetAt ?? 0).reduce((min, t) => t > 0 && (min === 0 || t < min) ? t : min, 0);
3970
- throw new PoolExhaustedError(this.provider, poolSize, minResetAt);
3971
- }
3972
- const strategy = opts?.strategy ?? "fill_first";
3973
- let picked;
3974
- switch (strategy) {
3975
- case "fill_first":
3976
- picked = this._pickFillFirst(eligible);
3977
- break;
3978
- case "round_robin":
3979
- picked = this._pickRoundRobin(eligible);
3980
- break;
3981
- case "least_used":
3982
- picked = this._pickLeastUsed(eligible);
3983
- break;
3984
- }
3985
- if (!picked) {
3986
- const minResetAt = eligible.map((c) => c.lastErrorResetAt ?? 0).reduce((min, t) => t > 0 && (min === 0 || t < min) ? t : min, 0);
3987
- throw new PoolExhaustedError(this.provider, poolSize, minResetAt);
3988
- }
3989
- return {
3990
- credential: await addCredential({
3991
- ...picked,
3992
- requestCount: (picked.requestCount ?? 0) + 1
3993
- }),
3994
- poolSize
3995
- };
3996
- }
3997
- /**
3998
- * Mark a credential as exhausted, applying a cooldown based on the HTTP
3999
- * error code. Persists `lastStatus`, `lastErrorCode`, and `lastErrorResetAt`
4000
- * to the store.
4001
- *
4002
- * Cooldown durations:
4003
- * - 401 (auth) → 5 minutes
4004
- * - 402 (billing) → 5 minutes
4005
- * - 429 (rate-limit) → 1 hour
4006
- * - 5xx / other → 60 seconds
4007
- *
4008
- * @param label - Credential label (unique within provider).
4009
- * @param errorCode - HTTP status code that triggered the exhaustion.
4010
- *
4011
- * @task T9265
4012
- */
4013
- async markExhausted(label, errorCode) {
4014
- const existing = await getCredentialByLabel(this.provider, label);
4015
- if (!existing) return;
4016
- const resetAt = Date.now() + cooldownMs(errorCode);
4017
- await addCredential({
4018
- ...existing,
4019
- lastStatus: "exhausted",
4020
- lastErrorCode: errorCode,
4021
- lastErrorResetAt: resetAt
4022
- });
4023
- }
4024
- /**
4025
- * Mark a credential as healthy. Clears `lastStatus`, `lastErrorCode`, and
4026
- * `lastErrorResetAt` so the entry re-enters the eligible pool immediately.
4027
- *
4028
- * @param label - Credential label (unique within provider).
4029
- *
4030
- * @task T9265
4031
- */
4032
- async markOk(label) {
4033
- const existing = await getCredentialByLabel(this.provider, label);
4034
- if (!existing) return;
4035
- await addCredential({
4036
- ...existing,
4037
- lastStatus: "ok",
4038
- lastErrorCode: void 0,
4039
- lastErrorResetAt: void 0
4040
- });
4041
- }
4042
- /**
4043
- * Proactively refresh an OAuth credential before it expires.
4044
- *
4045
- * Refresh is triggered when the remaining lifetime is less than
4046
- * `max(expiresIn * 0.5, 300_000ms)`. For a credential without a known
4047
- * `expiresIn`, only the 300s floor applies (requires `expiresAt`).
4048
- *
4049
- * For `api_key` and `aws_sdk` credentials this is a no-op.
4050
- *
4051
- * Supported refresh providers:
4052
- * - `kimi-code` — posts to `auth.kimi.com/api/oauth/token` with
4053
- * `grant_type=refresh_token` and the stored `refreshToken`.
4054
- *
4055
- * On success, the new access token (and optional refreshed refresh token)
4056
- * are persisted via `addCredential` upsert.
4057
- *
4058
- * @param label - Credential label (unique within provider).
4059
- * @returns `true` when a refresh was attempted (regardless of success),
4060
- * `false` when no refresh was needed or the credential is not OAuth.
4061
- * @task T9323
4062
- */
4063
- async proactiveRefresh(label) {
4064
- const existing = await getCredentialByLabel(this.provider, label);
4065
- if (!existing) return false;
4066
- if (existing.authType !== "oauth") return false;
4067
- if (!existing.refreshToken) return false;
4068
- if (existing.expiresAt == null) return false;
4069
- if (existing.expiresAt - Date.now() >= PROACTIVE_REFRESH_FLOOR_MS) return false;
4070
- await this._refreshOAuthCredential(existing);
4071
- return true;
4072
- }
4073
- /**
4074
- * Refresh every expired-but-refreshable OAuth credential for this provider
4075
- * before a selection pass, so the role resolver renews a stale token instead
4076
- * of silently filtering it out (which previously demoted resolution to a
4077
- * lower-priority — or fake — credential).
4078
- *
4079
- * For each stored OAuth entry that is expired (or within the proactive-refresh
4080
- * floor) AND carries a `refreshToken`, attempts a refresh via
4081
- * {@link proactiveRefresh}. Entries without a refresh token, non-OAuth
4082
- * entries, and still-valid tokens are left untouched. Errors are swallowed
4083
- * per-entry — a failed refresh leaves the entry expired and the normal
4084
- * eligible-filter still drops it.
4085
- *
4086
- * @returns The number of entries for which a refresh was attempted.
4087
- * @task T11617
4088
- */
4089
- async refreshExpiredOAuth() {
4090
- const entries = await this.listEntries();
4091
- let attempted = 0;
4092
- for (const entry of entries) {
4093
- if (entry.authType !== "oauth") continue;
4094
- if (!entry.refreshToken) continue;
4095
- try {
4096
- if (await this.proactiveRefresh(entry.label)) attempted += 1;
4097
- } catch {}
4098
- }
4099
- return attempted;
4100
- }
4101
- /**
4102
- * List all entries for the provider sorted by priority descending
4103
- * (highest priority = index 0).
4104
- *
4105
- * Includes both healthy and cooled-down entries — callers can inspect
4106
- * `lastErrorResetAt` to determine cooldown state.
4107
- *
4108
- * @returns Immutable sorted array of all stored credentials for this provider.
4109
- *
4110
- * @task T9265
4111
- */
4112
- async listEntries() {
4113
- return sortByPriorityDesc(await listCredentials(this.provider));
4114
- }
4115
- /**
4116
- * Perform the actual token refresh for a known OAuth credential.
4117
- *
4118
- * Dispatches based on `profile.oauth.mode`:
4119
- * - `pkce` — uses `refreshPkceToken` (RFC 6749 §6 refresh via PKCE endpoint).
4120
- * - `device-code` — uses the device-code token URL + clientId.
4121
- * - No profile / unknown mode → no-op (caller's 401-retry handles it).
4122
- *
4123
- * Errors are silently swallowed — the caller's retry path will encounter a
4124
- * 401 and trigger credential rotation if the token has actually expired.
4125
- *
4126
- * @param existing - The credential entry to refresh.
4127
- * @task T9302 (generic PKCE dispatch, replaces anthropic-specific branch)
4128
- * @task T9323 (device-code path)
4129
- */
4130
- async _refreshOAuthCredential(existing) {
4131
- if (!existing.refreshToken) return;
4132
- const oauthCfg = (await getProviderProfile(this.provider))?.oauth;
4133
- if (oauthCfg?.mode === "pkce") await this._refreshViaPkce(existing, oauthCfg.tokenEndpoint, oauthCfg.clientId, oauthCfg.tokenBodyFormat);
4134
- else if (this.provider === "kimi-code") {
4135
- const cfg = getKimiCodeDeviceCodeConfig();
4136
- await this._refreshTokenViaEndpoint(existing, cfg.tokenUrl, cfg.clientId);
4137
- }
4138
- }
4139
- /**
4140
- * Refresh via RFC 7636 PKCE `refresh_token` grant using `refreshPkceToken`.
4141
- *
4142
- * On success, upserts the new access token (and optional refresh token)
4143
- * into the credential store. Errors are silently swallowed.
4144
- *
4145
- * @param existing - Credential entry to refresh.
4146
- * @param tokenEndpoint - Provider token endpoint URL.
4147
- * @param clientId - OAuth client ID.
4148
- * @param bodyFormat - Token request body encoding (`'json'` for Anthropic).
4149
- * @task T9302
4150
- * @task T11958
4151
- */
4152
- async _refreshViaPkce(existing, tokenEndpoint, clientId, bodyFormat) {
4153
- if (!existing.refreshToken) return;
4154
- let tokens;
4155
- try {
4156
- tokens = await refreshPkceToken({
4157
- provider: existing.provider,
4158
- clientId,
4159
- refreshToken: existing.refreshToken,
4160
- tokenEndpoint,
4161
- bodyFormat
4162
- });
4163
- } catch {
4164
- return;
4165
- }
4166
- await addCredential({
4167
- ...existing,
4168
- accessToken: tokens.accessToken,
4169
- refreshToken: tokens.refreshToken ?? existing.refreshToken,
4170
- expiresAt: tokens.expiresIn != null ? Date.now() + tokens.expiresIn * 1e3 : existing.expiresAt,
4171
- lastStatus: "ok",
4172
- lastErrorCode: void 0,
4173
- lastErrorResetAt: void 0
4174
- });
4175
- }
4176
- /**
4177
- * Shared OAuth `refresh_token` grant implementation.
4178
- *
4179
- * POSTs `grant_type=refresh_token` to `tokenUrl` with the credential's
4180
- * stored refresh token. On success, upserts the new access token (and
4181
- * updated refresh token if provided) into the credential store.
4182
- *
4183
- * @param existing - Credential entry to refresh.
4184
- * @param tokenUrl - Provider's token endpoint.
4185
- * @param clientId - OAuth client ID.
4186
- * @param extraHeaders - Optional provider-specific headers (e.g. Anthropic beta flags).
4187
- * @task T9323
4188
- */
4189
- async _refreshTokenViaEndpoint(existing, tokenUrl, clientId, extraHeaders) {
4190
- if (!existing.refreshToken) return;
4191
- let resp;
4192
- try {
4193
- resp = await fetch(tokenUrl, {
4194
- method: "POST",
4195
- headers: {
4196
- "Content-Type": "application/x-www-form-urlencoded",
4197
- Accept: "application/json",
4198
- ...extraHeaders
4199
- },
4200
- body: new URLSearchParams({
4201
- grant_type: "refresh_token",
4202
- client_id: clientId,
4203
- refresh_token: existing.refreshToken
4204
- }).toString()
4205
- });
4206
- } catch {
4207
- return;
4208
- }
4209
- if (!resp.ok) return;
4210
- let data;
4211
- try {
4212
- data = await resp.json();
4213
- } catch {
4214
- return;
4215
- }
4216
- const newAccessToken = typeof data["access_token"] === "string" ? data["access_token"] : null;
4217
- if (!newAccessToken) return;
4218
- const newExpiresIn = typeof data["expires_in"] === "number" ? data["expires_in"] : null;
4219
- const newRefreshToken = typeof data["refresh_token"] === "string" ? data["refresh_token"] : existing.refreshToken;
4220
- await addCredential({
4221
- ...existing,
4222
- accessToken: newAccessToken,
4223
- refreshToken: newRefreshToken,
4224
- expiresAt: newExpiresIn != null ? Date.now() + newExpiresIn * 1e3 : existing.expiresAt,
4225
- lastStatus: "ok",
4226
- lastErrorCode: void 0,
4227
- lastErrorResetAt: void 0
4228
- });
4229
- }
4230
- /**
4231
- * Fill-first: return the highest-priority entry not in cooldown.
4232
- *
4233
- * Entries are sorted descending by `priority` (store convention: higher
4234
- * numeric priority = more preferred in pool). The first healthy entry wins.
4235
- *
4236
- * @param eligible - Non-disabled entries for the provider.
4237
- * @returns The selected entry, or `undefined` if all are in cooldown.
4238
- *
4239
- * @task T9265
4240
- */
4241
- _pickFillFirst(eligible) {
4242
- return sortByPriorityDesc(eligible).find((c) => !isInCooldown(c));
4243
- }
4244
- /**
4245
- * Round-robin: advance the cursor across the priority-sorted list, skipping
4246
- * cooled-down entries.
4247
- *
4248
- * The cursor is maintained in-memory per `CredentialPool` instance. It
4249
- * advances even when an entry is skipped so a single cooled-down entry does
4250
- * not permanently displace the rotation.
4251
- *
4252
- * @param eligible - Non-disabled entries for the provider.
4253
- * @returns The selected entry, or `undefined` if all are in cooldown.
4254
- *
4255
- * @task T9265
4256
- */
4257
- _pickRoundRobin(eligible) {
4258
- const sorted = sortByPriorityDesc(eligible);
4259
- const n = sorted.length;
4260
- for (let i = 0; i < n; i++) {
4261
- const idx = this.rrCursor % n;
4262
- this.rrCursor = idx + 1;
4263
- const candidate = sorted[idx];
4264
- if (candidate && !isInCooldown(candidate)) return candidate;
4265
- }
4266
- }
4267
- /**
4268
- * Least-used: pick the entry with the lowest `requestCount` that is not in
4269
- * cooldown. Ties are broken by `priority` descending (higher priority wins).
4270
- *
4271
- * @param eligible - Non-disabled entries for the provider.
4272
- * @returns The selected entry, or `undefined` if all are in cooldown.
4273
- *
4274
- * @task T9265
4275
- */
4276
- _pickLeastUsed(eligible) {
4277
- const healthy = eligible.filter((c) => !isInCooldown(c));
4278
- if (healthy.length === 0) return void 0;
4279
- return healthy.sort((a, b) => {
4280
- const countDiff = (a.requestCount ?? 0) - (b.requestCount ?? 0);
4281
- if (countDiff !== 0) return countDiff;
4282
- return b.priority - a.priority;
4283
- })[0];
4284
- }
4285
- };
4286
- /**
4287
- * Seed-attempt cache TTL — 60 seconds per E2a §5.2 T-E2-5 acceptance criterion.
4288
- *
4289
- * After a successful (non-`force`) seed pass, repeat calls to `seed()` are
4290
- * short-circuited until this TTL elapses. `force: true` always bypasses the
4291
- * cache. `lazy-seed` from `pick()` also honours the cache.
4292
- *
4293
- * @task T9412
4294
- */
4295
- var POOL_SEED_CACHE_TTL_MS = 60 * 1e3;
4296
- /**
4297
- * Process-wide debug log channel — kept minimal so the pool does not pull in
4298
- * the broader `cleo log` plumbing. Mirrors the pattern used by the per-provider
4299
- * pool above.
4300
- *
4301
- * Reads `process.env.CLEO_DEBUG` at call time so tests can toggle without
4302
- * module reload. Output goes to `console.debug` so it shows under `--verbose`
4303
- * but stays out of clean CLI output by default.
4304
- *
4305
- * @internal
4306
- */
4307
- function debugLog(msg, ...rest) {
4308
- if (process.env["CLEO_DEBUG"]) console.debug(`[cleo:credential-pool] ${msg}`, ...rest);
4309
- }
4310
- /**
4311
- * Unified credential pool — drives every registered seeder, upserts the
4312
- * discovered entries, and exposes `pick`/`list` over the populated store.
4313
- *
4314
- * Lifecycle:
4315
- *
4316
- * 1. First `pick()` (or explicit `seed()`) walks `BUILTIN_SEEDERS`.
4317
- * 2. For each seeder: consent gate → suppression gate → `seed()` →
4318
- * upsert each returned entry via `addCredential` (preserves the
4319
- * seeder's `priority` hint when provided, otherwise the store's
4320
- * `max + 10` rule applies).
4321
- * 3. A successful sweep stamps the cache; subsequent calls within
4322
- * {@link POOL_SEED_CACHE_TTL_MS} short-circuit unless `force: true`.
4323
- * 4. `list()` reads the store directly — never triggers seeding so
4324
- * diagnostic surfaces (`cleo auth list`) are pure-read.
4325
- *
4326
- * Error isolation: a single seeder that throws does NOT short-circuit the
4327
- * sweep. The failure is counted, the error stashed in `getSeederStatus`,
4328
- * and the next seeder runs.
4329
- *
4330
- * @example
4331
- * ```ts
4332
- * const pool = getCredentialPool();
4333
- * const entry = await pool.pick('anthropic');
4334
- * if (entry) {
4335
- * // use entry.accessToken
4336
- * }
4337
- * ```
4338
- *
4339
- * @task T9412
4340
- */
4341
- var UnifiedCredentialPool = class {
4342
- registryGetter;
4343
- /** Epoch ms of last successful seed pass (`0` = never seeded). */
4344
- lastSeededAt = 0;
4345
- /** Per-seeder diagnostics keyed on `${sourceId}::${provider}`. */
4346
- seederStatus = /* @__PURE__ */ new Map();
4347
- /**
4348
- * Construct a pool wired to a specific seeder registry.
4349
- *
4350
- * Production code uses the {@link getCredentialPool} singleton which wires
4351
- * to `BUILTIN_SEEDERS`. Tests pass a fresh `SeederRegistry` to isolate
4352
- * registration semantics from the process-wide singleton.
4353
- *
4354
- * @param registryGetter - Getter that returns the active list of seeders.
4355
- * Defaults to `BUILTIN_SEEDERS.getAll()`. A getter (not the array
4356
- * directly) is used so the pool stays in sync with seeders registered
4357
- * after construction.
4358
- */
4359
- constructor(registryGetter = () => BUILTIN_SEEDERS.getAll()) {
4360
- this.registryGetter = registryGetter;
4361
- }
4362
- /**
4363
- * Walk every registered seeder, gate on consent + suppression, and upsert
4364
- * the returned entries into the store.
4365
- *
4366
- * Cache rules:
4367
- * - If `force !== true` and the last successful seed pass was less than
4368
- * {@link POOL_SEED_CACHE_TTL_MS} ago, the sweep is skipped entirely
4369
- * and `{ added: 0, failed: 0, skipped: <total-seeders> }` is returned.
4370
- * - `force: true` always re-runs every seeder.
4371
- *
4372
- * Per-seeder rules:
4373
- * - `isConsentEstablished?` returning `false` → skip (status:
4374
- * `'skipped-consent'`).
4375
- * - `isSuppressed(provider, sourceId)` returning `true` → skip (status:
4376
- * `'skipped-suppressed'`).
4377
- * - `seed()` throwing → counted as `failed`; the error is logged
4378
- * and stashed in `getSeederStatus`; the next seeder still runs.
4379
- * - Each returned entry is upserted via `addCredential`; an upsert
4380
- * failure counts the seeder as `failed`.
4381
- *
4382
- * @param options - `{ force }` — bypass the 60s cache when `true`.
4383
- * @returns Aggregate counts across every seeder.
4384
- * @task T9412
4385
- */
4386
- async seed(options = {}) {
4387
- const seeders = this.registryGetter();
4388
- const now = Date.now();
4389
- if (!options.force && this.lastSeededAt > 0 && now - this.lastSeededAt < 6e4) {
4390
- debugLog("seed: cache hit, skipping sweep", {
4391
- ageMs: now - this.lastSeededAt,
4392
- ttlMs: POOL_SEED_CACHE_TTL_MS
4393
- });
4394
- return {
4395
- added: 0,
4396
- failed: 0,
4397
- skipped: seeders.length
4398
- };
4399
- }
4400
- let added = 0;
4401
- let failed = 0;
4402
- let skipped = 0;
4403
- for (const seeder of seeders) {
4404
- const key = `${seeder.sourceId}::${seeder.provider}`;
4405
- const stampedAt = Date.now();
4406
- if (typeof seeder.isConsentEstablished === "function") {
4407
- let consented;
4408
- try {
4409
- consented = await seeder.isConsentEstablished(seeder.provider);
4410
- } catch (err) {
4411
- failed++;
4412
- this.seederStatus.set(key, {
4413
- sourceId: seeder.sourceId,
4414
- provider: seeder.provider,
4415
- lastSeededAt: stampedAt,
4416
- lastResult: "failed",
4417
- error: `consent gate threw: ${err instanceof Error ? err.message : String(err)}`
4418
- });
4419
- debugLog("seed: consent gate threw", {
4420
- seeder: key,
4421
- err
4422
- });
4423
- continue;
4424
- }
4425
- if (!consented) {
4426
- skipped++;
4427
- this.seederStatus.set(key, {
4428
- sourceId: seeder.sourceId,
4429
- provider: seeder.provider,
4430
- lastSeededAt: stampedAt,
4431
- lastResult: "skipped-consent"
4432
- });
4433
- continue;
4434
- }
4435
- }
4436
- let suppressed;
4437
- try {
4438
- suppressed = isSuppressed(seeder.provider, seeder.sourceId);
4439
- } catch (err) {
4440
- debugLog("seed: isSuppressed threw — treating as un-suppressed", {
4441
- seeder: key,
4442
- err
4443
- });
4444
- suppressed = false;
4445
- }
4446
- if (suppressed) {
4447
- skipped++;
4448
- this.seederStatus.set(key, {
4449
- sourceId: seeder.sourceId,
4450
- provider: seeder.provider,
4451
- lastSeededAt: stampedAt,
4452
- lastResult: "skipped-suppressed"
4453
- });
4454
- continue;
4455
- }
4456
- let entries;
4457
- try {
4458
- entries = (await seeder.seed()).entries;
4459
- } catch (err) {
4460
- failed++;
4461
- this.seederStatus.set(key, {
4462
- sourceId: seeder.sourceId,
4463
- provider: seeder.provider,
4464
- lastSeededAt: stampedAt,
4465
- lastResult: "failed",
4466
- error: err instanceof Error ? err.message : String(err)
4467
- });
4468
- debugLog("seed: seeder threw", {
4469
- seeder: key,
4470
- err
4471
- });
4472
- continue;
4473
- }
4474
- let upsertFailed = false;
4475
- for (const entry of entries) try {
4476
- await addCredential(entry);
4477
- } catch (err) {
4478
- upsertFailed = true;
4479
- debugLog("seed: addCredential threw", {
4480
- seeder: key,
4481
- label: entry.label,
4482
- err
4483
- });
4484
- this.seederStatus.set(key, {
4485
- sourceId: seeder.sourceId,
4486
- provider: seeder.provider,
4487
- lastSeededAt: stampedAt,
4488
- lastResult: "failed",
4489
- entriesProduced: entries.length,
4490
- error: `addCredential threw for '${entry.label}': ${err instanceof Error ? err.message : String(err)}`
4491
- });
4492
- break;
4493
- }
4494
- if (upsertFailed) {
4495
- failed++;
4496
- continue;
4497
- }
4498
- added++;
4499
- this.seederStatus.set(key, {
4500
- sourceId: seeder.sourceId,
4501
- provider: seeder.provider,
4502
- lastSeededAt: stampedAt,
4503
- lastResult: "ok",
4504
- entriesProduced: entries.length
4505
- });
4506
- }
4507
- this.lastSeededAt = Date.now();
4508
- return {
4509
- added,
4510
- failed,
4511
- skipped
4512
- };
4513
- }
4514
- /**
4515
- * Pick a credential for the given provider, lazy-seeding on first call.
4516
- *
4517
- * Behaviour:
4518
- * - First call (or after `resetForTests()`) triggers a `seed()` pass.
4519
- * - Subsequent calls within {@link POOL_SEED_CACHE_TTL_MS} skip seeding.
4520
- * - The actual pick delegates to `pickCredentialForProviderSync` so the
4521
- * strategy + label-preference semantics match the rest of the store.
4522
- *
4523
- * @param provider - LLM transport to pick for.
4524
- * @param options - Optional strategy / label / no-seed flag.
4525
- * @returns The selected `StoredCredential`, or `null` when the pool is
4526
- * empty (or every entry has expired / been disabled).
4527
- * @task T9412
4528
- */
4529
- async pick(provider, options = {}) {
4530
- if (!options.noSeed) await this.seed();
4531
- return pickCredentialForProviderSync(provider, {
4532
- ...options.strategy != null && { strategy: options.strategy },
4533
- ...options.preferLabel != null && { preferLabel: options.preferLabel }
4534
- });
4535
- }
4536
- /**
4537
- * List every entry currently in the credential store.
4538
- *
4539
- * Pure read — never triggers seeding. Intended for diagnostic surfaces
4540
- * (`cleo auth list`, `cleo status`) where calling `seed()` would be
4541
- * surprising side-effect.
4542
- *
4543
- * @returns Read-only snapshot sorted by file order (insertion).
4544
- * @task T9412
4545
- */
4546
- async list() {
4547
- return listCredentials();
4548
- }
4549
- /**
4550
- * Snapshot of the last-known outcome of every registered seeder.
4551
- *
4552
- * Seeders that have never been invoked do not appear in the snapshot.
4553
- *
4554
- * @returns Read-only array of {@link SeederStatus} entries.
4555
- * @task T9412
4556
- */
4557
- getSeederStatus() {
4558
- return Array.from(this.seederStatus.values());
4559
- }
4560
- /**
4561
- * Test-only: invalidate the seed cache and clear status snapshots.
4562
- *
4563
- * Production code MUST NOT call this — it bypasses the 60s rate-limit
4564
- * that exists specifically to prevent runaway seeding under tight retry
4565
- * loops. The export is prefixed `_` to match the convention used by
4566
- * `credentials-store.ts` for analogous helpers (`_resetRoundRobinForTests`).
4567
- *
4568
- * @internal
4569
- */
4570
- _resetForTests() {
4571
- this.lastSeededAt = 0;
4572
- this.seederStatus.clear();
4573
- }
4574
- };
4575
- /**
4576
- * Module-state singleton — `null` until the first {@link getCredentialPool}
4577
- * call constructs the instance. Lazy construction keeps test runners that
4578
- * import the module without ever calling `getCredentialPool` from paying the
4579
- * seeder-registration cost.
4580
- *
4581
- * @internal
4582
- */
4583
- var _singleton = null;
4584
- /**
4585
- * Return the process-wide {@link UnifiedCredentialPool} singleton.
4586
- *
4587
- * Re-imports of this module yield the same instance under Node ESM's module
4588
- * cache. Tests that need an isolated pool MUST construct one directly
4589
- * (`new UnifiedCredentialPool(...)`) rather than mutating this singleton.
4590
- *
4591
- * @returns The shared pool instance.
4592
- * @task T9412
4593
- */
4594
- function getCredentialPool() {
4595
- if (_singleton === null) _singleton = new UnifiedCredentialPool();
4596
- return _singleton;
4597
- }
4598
-
4599
- export { BUILTIN_SEEDERS as B, CredentialPool as C, ENV_VARS as E, PoolExhaustedError as P, REMOVAL_REGISTRY as R, authHeadersFromSealed as a, getCredentialPool as b, addCredential as c, addSuppression as d, removeCredential as e, refreshExpiredOAuthForProvider as f, getProviderProfile as g, pickCredentialForProviderSync as h, getCredentialByLabel as i, recordRateLimit as j, rateLimitRemaining as k, listCredentials as l, getKimiCodeMshHeaders as m, isKimiCodeApiKey as n, pickCredentialForProvider as p, resolveCredentials as r };
4600
- //# sourceMappingURL=credential-pool-vF6NdtT9.js.map