@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.
- package/dist/cli/index.js +3873 -2234
- package/package.json +14 -13
- package/scripts/check-cleo-tarball-size.mjs +105 -0
- package/scripts/copy-studio-dist.mjs +33 -9
- package/studio-dist/client/_app/immutable/chunks/{wI_OOyoh.js → BB7zPpV2.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{D8NsLz6i.js → BOnrCZaR.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{eK8hf4s7.js → BSRBuVG3.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/C6jhAnil.js +1 -0
- package/studio-dist/client/_app/immutable/chunks/C87Q3uxm.js +1 -0
- package/studio-dist/client/_app/immutable/chunks/{DVHIxkgs.js → CLZBEpLY.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{CqF5Lomf.js → CTLGzgdN.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{o1PZ6Kzq.js → CY3heNub.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{RptFUp2P.js → Cge6qiwc.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{BRgWBQlV.js → CnwXLaZJ.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{Bxz2A0ZC.js → DEH43YwF.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/DGftG5oL.js +3 -0
- package/studio-dist/client/_app/immutable/chunks/DX_JOBSO.js +1 -0
- package/studio-dist/client/_app/immutable/chunks/{CEqOYim7.js → DZrdToAH.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/EZwHPWle.js +2 -0
- package/studio-dist/client/_app/immutable/chunks/cWVw9W0E.js +3 -0
- package/studio-dist/client/_app/immutable/chunks/{B9psmq5-.js → rB_71s8h.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{JPJd4fEI.js → rUtW7Hx6.js} +1 -1
- package/studio-dist/client/_app/immutable/entry/app.tvu3-wbp.js +2 -0
- package/studio-dist/client/_app/immutable/entry/start.BS-rpdjH.js +1 -0
- package/studio-dist/client/_app/immutable/nodes/{0.CZzEEyVh.js → 0.DMP7swbx.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{1.Bj3Opbua.js → 1.CYZMFpIp.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{10.xe_Mc2Ko.js → 10.3rxHNnSx.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{11.DuKPA7JD.js → 11.B6jWv3Kz.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{12.BoZ_QrUS.js → 12.BrDHeTEq.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{13.DFJiUiv8.js → 13.CE5bd4JV.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{14.eaAlFU4w.js → 14.DgXD2LRx.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{15.BfXgiwMC.js → 15.Cz1KI9Fv.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{16.mEwy9cqh.js → 16.K9wYQPhb.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{17.DP2QtQGn.js → 17.CtUmdfMg.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{18.Bu0054sX.js → 18.mtosd0sL.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{19.UWt6QbCm.js → 19.su55K8y_.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{2.y5jKpHiR.js → 2.mqgFVWFQ.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{20.B1AIHdkP.js → 20.NJuHLxuS.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{21.CpAN71H8.js → 21.BhwhdQCq.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{22.CrYKrN4c.js → 22.BmJEWwER.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{23.pGRaHQjq.js → 23.CAVIJEH-.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{24.BUoUu37z.js → 24.CMuXs5Sy.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/25.DhMBJYAo.js +39 -0
- package/studio-dist/client/_app/immutable/nodes/26.CJ4FA5FS.js +1 -0
- package/studio-dist/client/_app/immutable/nodes/{28.DcmYM7lb.js → 28.D64RBcfs.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{29.lbHbMqNO.js → 29.yfuBeH1x.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{3.Dyir4xXj.js → 3.y115j1d5.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{30.DXj0Wm2A.js → 30.1WUv5bh9.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/4.D6vXYBoQ.js +1 -0
- package/studio-dist/client/_app/immutable/nodes/{5.VWSdP7oc.js → 5.hjw2O-4e.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{6.Dl-XLwOj.js → 6.CAoDfDt6.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{7.CFuC5L-y.js → 7.CLgPSQ8Z.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{8.BCJ0DhOb.js → 8.zuaO1Hik.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{9.Bq4SUzL9.js → 9.Cdsd85m2.js} +1 -1
- package/studio-dist/client/_app/version.json +1 -1
- package/studio-dist/server/chunks/0-BQvtFOrC.js +41 -0
- package/studio-dist/server/chunks/1-Bx2_yoiK.js +9 -0
- package/studio-dist/server/chunks/{10-DvOD4ksa.js → 10-CYLc3d5x.js} +3 -3
- package/studio-dist/server/chunks/{11-DMnBZ02H.js → 11-DcIYR5vm.js} +3 -3
- package/studio-dist/server/chunks/{12-CCyRyRWX.js → 12-WE1u104t.js} +3 -3
- package/studio-dist/server/chunks/{13-Dx1c3a_X.js → 13-joBDoxhK.js} +3 -3
- package/studio-dist/server/chunks/{14-Bo-dTCaZ.js → 14-Doj5F4uJ.js} +3 -3
- package/studio-dist/server/chunks/{15-BvUNl3KL.js → 15-nQTeHGD1.js} +3 -3
- package/studio-dist/server/chunks/{16-De6EzPSa.js → 16-CcTN0e8k.js} +5 -4
- package/studio-dist/server/chunks/{17-CZNsiZhi.js → 17-DWiFnzES.js} +10 -93
- package/studio-dist/server/chunks/{18-CbxxjZox.js → 18-Dcc9GnPo.js} +5 -4
- package/studio-dist/server/chunks/{19-DqHyladS.js → 19-BAQHPrMB.js} +3 -3
- package/studio-dist/server/chunks/{2-COvmA-ZA.js → 2-DXSVVaej.js} +10 -93
- package/studio-dist/server/chunks/20-DfQlGHPW.js +57 -0
- package/studio-dist/server/chunks/{21-DXK9ptAC.js → 21-qhGjkW41.js} +4 -31
- package/studio-dist/server/chunks/{22-DD5CLWJC.js → 22-Byy16hFA.js} +3 -3
- package/studio-dist/server/chunks/23-CZpFrGp4.js +97 -0
- package/studio-dist/server/chunks/24-EEysaT9T.js +67 -0
- package/studio-dist/server/chunks/{25-CAg6Xcmn.js → 25-CgiYn70L.js} +15 -96
- package/studio-dist/server/chunks/{26-_nfVTvNi.js → 26-Cfd6BaUb.js} +12 -93
- package/studio-dist/server/chunks/{27-C9edghG4.js → 27-Cqzlh9gZ.js} +3 -2
- package/studio-dist/server/chunks/{28-VemTJGyc.js → 28-DeYe2rpm.js} +8 -62
- package/studio-dist/server/chunks/{29-CXP-m29t.js → 29-6pjLM0Z1.js} +10 -93
- package/studio-dist/server/chunks/{3-B8KhfvHp.js → 3-E_fWDAkq.js} +9 -25
- package/studio-dist/server/chunks/{30-QiM1S4cJ.js → 30-Dw317sSW.js} +10 -93
- package/studio-dist/server/chunks/{31-BwdY9NnB.js → 31-Q4kHbp8j.js} +3 -2
- package/studio-dist/server/chunks/{4-CSaG4HjO.js → 4-C9BMSh4K.js} +4 -3
- package/studio-dist/server/chunks/{5-i5ry4y3C.js → 5-C0CF4WKG.js} +3 -3
- package/studio-dist/server/chunks/{6-DR1GWBMT.js → 6-BWyOJust.js} +3 -3
- package/studio-dist/server/chunks/{7-DSNwn3aQ.js → 7-CK4MK5gy.js} +3 -3
- package/studio-dist/server/chunks/{8-Bj0bx07K.js → 8-CiX7Xk9k.js} +3 -3
- package/studio-dist/server/chunks/{9-B3Tz0mZq.js → 9-Be5118w4.js} +3 -3
- package/studio-dist/server/chunks/{HeroHeader-v4Q9vaGr.js → HeroHeader-BrGN5WPw.js} +2 -2
- package/studio-dist/server/chunks/{Sparkline-BJ-GuhRd.js → Sparkline-DU_NVxz9.js} +2 -2
- package/studio-dist/server/chunks/{StatBlock-DHJMmOCx.js → StatBlock-jNsxycS1.js} +2 -2
- package/studio-dist/server/chunks/{Tabs-DzG6Bn9a.js → Tabs-DQKDflnT.js} +2 -2
- package/studio-dist/server/chunks/_dispatch-DTWViLGL.js +145 -0
- package/studio-dist/server/chunks/{_layout.svelte-BEIGQcIv.js → _layout.svelte-CL6GfET2.js} +7 -9
- package/studio-dist/server/chunks/{_page.svelte-al08ovKm.js → _page.svelte-6iMiGR11.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-Byz2djto.js → _page.svelte-B-YXGvAW.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-tYN7_LXI.js → _page.svelte-B5E6RqVb.js} +9 -11
- package/studio-dist/server/chunks/{_page.svelte-UYiF7YBK.js → _page.svelte-BCRyjlk2.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-CCoTp89s.js → _page.svelte-BoYafqft.js} +3 -3
- package/studio-dist/server/chunks/{_page.svelte-nzffIbBP.js → _page.svelte-C7Wm6_HB.js} +3 -3
- package/studio-dist/server/chunks/{_page.svelte-C_VSL8NW.js → _page.svelte-Cgqt7ucn.js} +13 -71
- package/studio-dist/server/chunks/{_page.svelte-CophUar3.js → _page.svelte-CqI6hqMm.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-BgwL2Gyu.js → _page.svelte-CvQ2rZZ1.js} +11 -13
- package/studio-dist/server/chunks/{_page.svelte-l4Z_xjl9.js → _page.svelte-CvTyJEyS.js} +9 -11
- package/studio-dist/server/chunks/{_page.svelte-DOWw416I.js → _page.svelte-CvzAttO9.js} +11 -13
- package/studio-dist/server/chunks/{_page.svelte-DYNorA0S.js → _page.svelte-Cx9Ptecp.js} +10 -67
- package/studio-dist/server/chunks/{_page.svelte-Wfx7ww_h.js → _page.svelte-Cx9dPjsT.js} +11 -13
- package/studio-dist/server/chunks/{_page.svelte-2cgtvHwW.js → _page.svelte-DGN5j-At.js} +13 -71
- package/studio-dist/server/chunks/{_page.svelte-BVVTrzTL.js → _page.svelte-DaheDIRs.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-M5D2QnMS.js → _page.svelte-Dm8y5a8S.js} +10 -67
- package/studio-dist/server/chunks/{_page.svelte-B_pq-Z98.js → _page.svelte-DqXxyNsF.js} +7 -7
- package/studio-dist/server/chunks/{_page.svelte-C3aIui4o.js → _page.svelte-DqsbnAhk.js} +4 -4
- package/studio-dist/server/chunks/{_page.svelte-ksTJUXce.js → _page.svelte-Dt0_c1T-.js} +3 -3
- package/studio-dist/server/chunks/{_page.svelte-bGhsrIjS.js → _page.svelte-Dv6iwhu7.js} +3 -3
- package/studio-dist/server/chunks/{_page.svelte-BXyoMME1.js → _page.svelte-GPiHej3P.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-CYDjW1R0.js → _page.svelte-LuRm8XxE.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-Clbkk1gm.js → _page.svelte-Z7nL9SC-.js} +8 -9
- package/studio-dist/server/chunks/{_page.svelte-DUWbZjC7.js → _page.svelte-_kntHS6v.js} +4 -4
- package/studio-dist/server/chunks/{_page.svelte-BIlhmugm.js → _page.svelte-eiomv6Pa.js} +11 -13
- package/studio-dist/server/chunks/{_page.svelte-9OQphlnm.js → _page.svelte-jXJbd8Mx.js} +14 -69
- package/studio-dist/server/chunks/{_page.svelte-CHzXuw2Q.js → _page.svelte-me5SUosG.js} +3 -3
- package/studio-dist/server/chunks/{_server.ts-DSzRdt1U.js → _server.ts--2m_U4SL.js} +11 -94
- package/studio-dist/server/chunks/{_server.ts-ujoDR2_g.js → _server.ts--sGLmBsz.js} +10 -92
- package/studio-dist/server/chunks/{_server.ts-k2fApwOp.js → _server.ts-0ukyuE0I.js} +5 -60
- package/studio-dist/server/chunks/_server.ts-1eW2mTeh.js +40 -0
- package/studio-dist/server/chunks/{_server.ts-Bfr4u3EP.js → _server.ts-3LzqLzt0.js} +10 -92
- package/studio-dist/server/chunks/_server.ts-40h9Y46Y.js +122 -0
- package/studio-dist/server/chunks/_server.ts-5euivmMj.js +38 -0
- package/studio-dist/server/chunks/{_server.ts-CShsBkeE.js → _server.ts-B0uoay97.js} +7 -23
- package/studio-dist/server/chunks/{_server.ts-hL92cV7Z.js → _server.ts-B5vYKkI6.js} +9 -63
- package/studio-dist/server/chunks/{_server.ts-_RJXtmfi.js → _server.ts-B8dTbPHd.js} +8 -23
- package/studio-dist/server/chunks/{_server.ts-C3Jo-ju8.js → _server.ts-BA8Xdegq.js} +10 -92
- package/studio-dist/server/chunks/_server.ts-BBClE0n9.js +54 -0
- package/studio-dist/server/chunks/{_server.ts-ZS1_yMwR.js → _server.ts-BDwMoyZ_.js} +6 -32
- package/studio-dist/server/chunks/{_server.ts-sv4pA808.js → _server.ts-BE-Uhx4N.js} +5 -46
- package/studio-dist/server/chunks/{_server.ts-DV6pzF-M.js → _server.ts-BSHADOUJ.js} +4 -3
- package/studio-dist/server/chunks/_server.ts-BcsqAqXt.js +67 -0
- package/studio-dist/server/chunks/_server.ts-Bl9tMqwZ.js +159 -0
- package/studio-dist/server/chunks/_server.ts-BlzDnRI0.js +51 -0
- package/studio-dist/server/chunks/{_server.ts-DnE0f-iC.js → _server.ts-BtuemCsf.js} +4 -3
- package/studio-dist/server/chunks/{_server.ts-BPcErnS0.js → _server.ts-C2nYqbd9.js} +10 -92
- package/studio-dist/server/chunks/{_server.ts-4p6cp9ke.js → _server.ts-C6IDM_3I.js} +10 -92
- package/studio-dist/server/chunks/_server.ts-C8Z32vuL.js +45 -0
- package/studio-dist/server/chunks/_server.ts-CBXyadsy.js +26 -0
- package/studio-dist/server/chunks/{_server.ts-Do9RIPRo.js → _server.ts-CE6h2ATE.js} +5 -32
- package/studio-dist/server/chunks/_server.ts-CH06JVqZ.js +65 -0
- package/studio-dist/server/chunks/_server.ts-CNqcws-M.js +29 -0
- package/studio-dist/server/chunks/{_server.ts-CJA1djzJ.js → _server.ts-CPf8necd.js} +8 -92
- package/studio-dist/server/chunks/{_server.ts-uUx7uT2Y.js → _server.ts-CYv5YuHd.js} +6 -43
- package/studio-dist/server/chunks/{_server.ts-B2E7fxo9.js → _server.ts-CZ5GZRWX.js} +6 -91
- package/studio-dist/server/chunks/{_server.ts-DSAv66wD.js → _server.ts-CbThsyhY.js} +4 -3
- package/studio-dist/server/chunks/{_server.ts-DgbY5EB3.js → _server.ts-CeQolQOK.js} +10 -92
- package/studio-dist/server/chunks/_server.ts-ChoGyX5C.js +61 -0
- package/studio-dist/server/chunks/_server.ts-CjJYkLJH.js +61 -0
- package/studio-dist/server/chunks/{_server.ts-lPlBUIJX.js → _server.ts-CtjArzxf.js} +5 -60
- package/studio-dist/server/chunks/{_server.ts-Cq60m8Pw.js → _server.ts-D3QWZwt_.js} +4 -3
- package/studio-dist/server/chunks/{_server.ts-CkoJ2HCW.js → _server.ts-D6wP5bCh.js} +10 -92
- package/studio-dist/server/chunks/{_server.ts-DwTRjS4y.js → _server.ts-D7JwQTun.js} +10 -93
- package/studio-dist/server/chunks/_server.ts-DKFJdXPK.js +97 -0
- package/studio-dist/server/chunks/_server.ts-DNAPGKEF.js +55 -0
- package/studio-dist/server/chunks/_server.ts-DQiTiY3E.js +78 -0
- package/studio-dist/server/chunks/{_server.ts-BT0Uq2yq.js → _server.ts-DUyeSXbV.js} +10 -92
- package/studio-dist/server/chunks/{_server.ts-DRVz9D-J.js → _server.ts-DY7BwZCv.js} +9 -24
- package/studio-dist/server/chunks/{_server.ts-Cz0zgaUV.js → _server.ts-D_E276ms.js} +3 -2
- package/studio-dist/server/chunks/{_server.ts-B5g3eScF.js → _server.ts-DgtmTAp5.js} +5 -60
- package/studio-dist/server/chunks/{_server.ts-CboRJY0Q.js → _server.ts-DhTKaw7h.js} +10 -63
- package/studio-dist/server/chunks/_server.ts-DqKBSCPT.js +31 -0
- package/studio-dist/server/chunks/{_server.ts-CpmMp5D8.js → _server.ts-DyXBNnRD.js} +4 -3
- package/studio-dist/server/chunks/_server.ts-DzjUpduu.js +25 -0
- package/studio-dist/server/chunks/{_server.ts-CGiAXy5i.js → _server.ts-Dznqw6Dg.js} +8 -23
- package/studio-dist/server/chunks/{_server.ts-tKMjUFUP.js → _server.ts-EVwS7UXb.js} +4 -3
- package/studio-dist/server/chunks/_server.ts-J17j25VG.js +62 -0
- package/studio-dist/server/chunks/{_server.ts-BdN4AJ6o.js → _server.ts-VQchZxXq.js} +10 -91
- package/studio-dist/server/chunks/{_server.ts-C4BcDGqS.js → _server.ts-XLV5SvjH.js} +7 -63
- package/studio-dist/server/chunks/_server.ts-jZgqFIJz.js +45 -0
- package/studio-dist/server/chunks/{_server.ts-Bw7A4zgb.js → _server.ts-lyiGGv7w.js} +5 -3
- package/studio-dist/server/chunks/{brain-CXR7ZT7S.js → brain-xt_NeshR.js} +2 -2
- package/studio-dist/server/chunks/cleo-home-_XIqkZaL.js +109 -0
- package/studio-dist/server/chunks/{cli-action-DQcaqcfn.js → cli-action-VJPEn1pY.js} +2 -2
- package/studio-dist/server/chunks/{client-CgX38JBu.js → client-D71MZ7Vi.js} +5 -6
- package/studio-dist/server/chunks/cluster-label-layer-CysRMvhi.js +6721 -0
- package/studio-dist/server/chunks/connections-CWLPjwBP.js +15 -0
- package/studio-dist/server/chunks/{connections2-mi7mZqJM.js → connections2-DHVs5ytW.js} +32 -19
- package/studio-dist/server/chunks/{dev-BNpEUZI3.js → dev-Br5zieNA.js} +2 -2
- package/studio-dist/server/chunks/dist-BzgWhfUR.js +18748 -0
- package/studio-dist/server/chunks/{error.svelte-CplNulnS.js → error.svelte-BIToUowC.js} +6 -8
- package/studio-dist/server/chunks/{explorer-loader-BN_mkt3_.js → explorer-loader-B_uCSs9_.js} +2 -2
- package/studio-dist/server/chunks/exports-Dg-gdPPq.js +124 -0
- package/studio-dist/server/chunks/{hooks.server-DBsyphiX.js → hooks.server-CWBRp8NI.js} +10 -93
- package/studio-dist/server/chunks/{http-CRTZe306.js → http-DPU3HHl2.js} +3 -4
- package/studio-dist/server/chunks/{index-server-DocJvixA.js → index-server-BgfA6C0S.js} +17 -4
- package/studio-dist/server/chunks/{internal-D6eWgsHJ.js → internal-DPnIJYMs.js} +56 -92
- package/studio-dist/server/chunks/{memory2-Cgc_4D5q.js → memory-BZHnDZzu.js} +4 -4
- package/studio-dist/server/chunks/{nexus-adapter-9EL_VlGI.js → nexus-adapter-D1ryYG1Z.js} +5 -5
- package/studio-dist/server/chunks/{no-face-up-DcejJLy-.js → no-face-up-B_3h6Hgs.js} +2 -2
- package/studio-dist/server/chunks/project-context-BAXQmgch.js +188 -0
- package/studio-dist/server/chunks/{src-BUqGet2c.js → src-D2rW9bIF.js} +422 -5
- package/studio-dist/server/chunks/{stores-5DmdjPCH.js → stores-BZdYTyl7.js} +3 -3
- package/studio-dist/server/chunks/{tasks2-Bf9VHrv-.js → tasks-sWuEmiGW.js} +2349 -9
- package/studio-dist/server/chunks/{ui-DR8zOSpC.js → ui-CkQ-pCs3.js} +2 -2
- package/studio-dist/server/chunks/utils-g564cUMV.js +151 -0
- package/studio-dist/server/index.js +5 -16
- package/studio-dist/server/manifest.js +88 -88
- package/dist/cli/index.js.map +0 -6
- package/studio-dist/client/_app/immutable/assets/parse-worker.CSEr67Qi.js +0 -1127
- package/studio-dist/client/_app/immutable/chunks/2mEksghu2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/3KebaI-z.js +0 -53
- package/studio-dist/client/_app/immutable/chunks/3y2yNYEk2.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/7UXYlaI1.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/83VLmCvk.js +0 -4
- package/studio-dist/client/_app/immutable/chunks/84Rhmi6r.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/9CV9TXTg2.js +0 -92
- package/studio-dist/client/_app/immutable/chunks/9LFtcz24.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/9bLMgKwu2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/9kKqohVR2.js +0 -37
- package/studio-dist/client/_app/immutable/chunks/B-Ye28EY.js +0 -7
- package/studio-dist/client/_app/immutable/chunks/B-jnUl7o.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B0ORuy8x2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B0xCM1wN2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B0zCN4sX.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B1wt-iGW.js +0 -7
- package/studio-dist/client/_app/immutable/chunks/B2F3KUIU.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B2_6RrlL2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B2qKEYxA2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B2u2rNyI2.js +0 -8
- package/studio-dist/client/_app/immutable/chunks/B3_X3Lhd2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B3sNtbqf.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B4NQP1Ax2.js +0 -6
- package/studio-dist/client/_app/immutable/chunks/BAtZg5v5.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BB6WKLm82.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BBX8UDtn.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BBs1U1iU.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BEli_4Jp2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BEt2wIUt.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BEyM8qSW2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BFZA3Dsk2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BGdPweG1.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BGiusSqE.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BGmpyfzD.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BH5rhSIR.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BHciFt922.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BHnFwTQY.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BIvye3XP2.js +0 -59
- package/studio-dist/client/_app/immutable/chunks/BJ8mcpQA2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BK17qSHw.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BLkgUtyb.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BMUko3r62.js +0 -5
- package/studio-dist/client/_app/immutable/chunks/BMwzdEaa.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BQZx6brg.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BUPJTTTd.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BVNGAeZe2.js +0 -146
- package/studio-dist/client/_app/immutable/chunks/BWfLryGm.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BXIfeogw.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BXTfFyUU.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BYJC6_MM.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BZV1ZJzH.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BZu9rehd.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Bbzm48C52.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BfGnlwjH.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Bfjb9LPp.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BieFfu-R.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Bmv63uXe2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BtBW1A0u.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BxISGQCl2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/ByNR_KvT.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BzjhD6Hr2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BzpWnq0c2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C-yJVcyI2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C0cQ-8kS2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C1vWmhYe.js +0 -48
- package/studio-dist/client/_app/immutable/chunks/C1w6bBje.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C243sn7r.js +0 -4
- package/studio-dist/client/_app/immutable/chunks/C2oZaDvt.js +0 -67
- package/studio-dist/client/_app/immutable/chunks/C3OziSQx.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C3hFPbMK.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/C4P9oFvU2.js +0 -6
- package/studio-dist/client/_app/immutable/chunks/C5Nlanei.js +0 -8
- package/studio-dist/client/_app/immutable/chunks/C6KBAXPl.js +0 -15
- package/studio-dist/client/_app/immutable/chunks/C8MZnWta.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CAoan3K12.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CBpcINYH.js +0 -29
- package/studio-dist/client/_app/immutable/chunks/CE7phgWq.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CFb4Co_K.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CGWoRK3t2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CH3t4xo1.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CHJgk6g-.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CHgPjuDQ.js +0 -17
- package/studio-dist/client/_app/immutable/chunks/CHkCZrnr.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CI_91WUN.js +0 -4
- package/studio-dist/client/_app/immutable/chunks/CJ7-hP0H.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CKIrNnkY2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CKoQ7Ko5.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CM8O4zqs.js +0 -56
- package/studio-dist/client/_app/immutable/chunks/CMz3AVRO.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CNToUR3P.js +0 -13
- package/studio-dist/client/_app/immutable/chunks/CNuQG1yl2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CQLIZtxj.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CQYVNgM0.js +0 -20
- package/studio-dist/client/_app/immutable/chunks/CQeVOmFO2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CSPHHq09.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CTJEdbIn.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CTTT3wYw.js +0 -35
- package/studio-dist/client/_app/immutable/chunks/CV4ck7fz.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CXQrVnHf.js +0 -11
- package/studio-dist/client/_app/immutable/chunks/CXTCtOUQ.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C_CsU7Qq.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C_IZx3682.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CbTnu2Po2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Cba8RZ1a2.js +0 -8
- package/studio-dist/client/_app/immutable/chunks/Cc8xxDr5.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CdPxoldu.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CdqDTozW.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CdsmcCfC.js +0 -25
- package/studio-dist/client/_app/immutable/chunks/CeYPAoY9.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Cf4kGS9y.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CfWEqXtJ2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Cg7Wj2b9.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CgTpKZfi2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Chpy9ca7.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CiLKuLrQ.js +0 -4
- package/studio-dist/client/_app/immutable/chunks/CjNDJjwu.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Cjde0ZKr2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Cjgnade02.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/Ckp90om52.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/ClAPVhCD.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/ClT89UdO.js +0 -11
- package/studio-dist/client/_app/immutable/chunks/CnJf5tML.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CpF4Rrcc2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CqCka5yX2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CqtqZrOl.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CrJdywQt.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Cs0zC2l2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CsMn5fBs.js +0 -141
- package/studio-dist/client/_app/immutable/chunks/CsvKLHQQ2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/Ctaresvj.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Ctetf8vc2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/Cv7yW6MG.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CxRIz-Zd.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CxbvtjIH.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/CxfeoXkZ.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CySLscL2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/D0PKQAbM.js +0 -87
- package/studio-dist/client/_app/immutable/chunks/D0WQsLTu.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D1LBVx6r2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D1NlFXu9.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D1QfC4hu2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D1yrHAn72.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D2pa4reh.js +0 -7
- package/studio-dist/client/_app/immutable/chunks/D3QsWcIw2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/D4OkQuPl2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D6UDnATk2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D7w7HllH.js +0 -38
- package/studio-dist/client/_app/immutable/chunks/D93KpglN.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D99fQj2G2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DB5ifLM-.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/DBhP8ifO2.js +0 -15
- package/studio-dist/client/_app/immutable/chunks/DF_4OWhj.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DFpNK1vs2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DGRYMk-W.js +0 -142
- package/studio-dist/client/_app/immutable/chunks/DH3CcQwq.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DHQfJeLU.js +0 -4
- package/studio-dist/client/_app/immutable/chunks/DL89yEDy2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DLRku_0p2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DMZkjPYT2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DNP72foU.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DO2GztVm.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DOVNXmCK2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DOqzs8kl2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DQnR63hh2.js +0 -7
- package/studio-dist/client/_app/immutable/chunks/DTxtWPkw.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DVDgCC9A2.js +0 -20
- package/studio-dist/client/_app/immutable/chunks/DVVCJYjz.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DWRhYUMj2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DWavBJBI.js +0 -4
- package/studio-dist/client/_app/immutable/chunks/DXDSWRw-.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DXoHaH4a.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DXpCX2Hi.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DYP3EJMY.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DYQphR_V.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DYUlIivP.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DYo2ye8X2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DYxFu8yV.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DZ66sRb6.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D_XTiylm2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DaRv9jn22.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DaXb9i_E.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Dccf4VP2.js +0 -24
- package/studio-dist/client/_app/immutable/chunks/DdODs-Sb.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DdekYWer.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DeHaovsF.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DeRRkRO52.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DeWI5qCn.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DfivR-Md.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DgmxpSo7.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DhPLWrun2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DhhrVdfi.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/Djr2dbcc2.js +0 -37
- package/studio-dist/client/_app/immutable/chunks/DjuRo5vv.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Djz4EDzO.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DkRfaw-K2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DksB5g4W2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Dmqenb5z2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DnbToVyf.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DpGZwlmj.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Dq0lE5C32.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Dq54XqHT2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DqK84mbb.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DqjFtSJD.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DrKWWDpL.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DsQIgu-4.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DsqdgJHS2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DtNkZUkq.js +0 -17
- package/studio-dist/client/_app/immutable/chunks/DutIls4V2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DvxYFW4C.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Dwp-76GQ.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DxSZd548.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DxiUMhk0.js +0 -5
- package/studio-dist/client/_app/immutable/chunks/DxslLswf.js +0 -23
- package/studio-dist/client/_app/immutable/chunks/Dy7EjGgb2.js +0 -9
- package/studio-dist/client/_app/immutable/chunks/DyGQ_VXC.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/DySe3kTV2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DyzE2Uxw.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DzhuJaxE2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/FmMHyrh6.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Gxs0J274.js +0 -5
- package/studio-dist/client/_app/immutable/chunks/HwtzDYxN.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/ITRz_Q7H2.js +0 -15
- package/studio-dist/client/_app/immutable/chunks/KDNP98XY2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/LmVjFhpk2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/MiAejGV_.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Ml4JR6Jz2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/NU5S3nNs2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/Oh5zPjv3.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/QZkvJ9vi.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Rbn8UpBm.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/SPX3gYwn2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/TX-23QW3.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/UyeQuL132.js +0 -12
- package/studio-dist/client/_app/immutable/chunks/VQszoXcV.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/VtZsEMxD.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/W2IeK61p2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Wadww7ZG.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/WgfA1zWk2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/WxIgAS1f.js +0 -21
- package/studio-dist/client/_app/immutable/chunks/XrsBoOj9.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/Y3PX4poR.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/YxfwqiF32.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Z7uO1YaO.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/_4ykQk_8.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/_kRcZFVn.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/aQNM7eVA2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/bHKz_Fb2.js +0 -45
- package/studio-dist/client/_app/immutable/chunks/bJRyRGSZ.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/dlzF6lnR.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/fjRbpfEr2.js +0 -26
- package/studio-dist/client/_app/immutable/chunks/imLk4if5.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/lgRG0R-32.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/mFhCjxQJ2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/mgnH6qYg.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/n6M_5LoW.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/nLIOiDZp2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/nab7WB3u2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/orUmAKoW2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/p-fLQDfY2.js +0 -63
- package/studio-dist/client/_app/immutable/chunks/s0-WkOGk2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/sPuXfbUF.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/t4h0vrqR2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/uE83FKL-.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/uzhXlm0g2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/vTrEsGlD2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/xJgCk3dx2.js +0 -28
- package/studio-dist/client/_app/immutable/chunks/z0Mt579s2.js +0 -13
- package/studio-dist/client/_app/immutable/entry/app.BoQ7cgT2.js +0 -2
- package/studio-dist/client/_app/immutable/entry/start.Cg1v8DU4.js +0 -1
- package/studio-dist/client/_app/immutable/nodes/25.ddZ1_EFk.js +0 -1
- package/studio-dist/client/_app/immutable/nodes/26.6OcmQbLV.js +0 -1
- package/studio-dist/client/_app/immutable/nodes/4.CCjOEwVU.js +0 -1
- package/studio-dist/server/chunks/0-CvsKDxQ6.js +0 -126
- package/studio-dist/server/chunks/1-ck_x2Hf0.js +0 -9
- package/studio-dist/server/chunks/20-DVU22np7.js +0 -142
- package/studio-dist/server/chunks/23-D_tUnnoc.js +0 -181
- package/studio-dist/server/chunks/24-CVlymvLF.js +0 -151
- package/studio-dist/server/chunks/AwsRestJsonProtocol-BZuoNBmL.js +0 -416
- package/studio-dist/server/chunks/UnionSerde-BflIkhKj.js +0 -2653
- package/studio-dist/server/chunks/_dispatch-ncuRztUQ.js +0 -5597
- package/studio-dist/server/chunks/_server.ts-4NyKAneX.js +0 -116
- package/studio-dist/server/chunks/_server.ts-7hGdxdEI.js +0 -136
- package/studio-dist/server/chunks/_server.ts-B6b3b-yA.js +0 -131
- package/studio-dist/server/chunks/_server.ts-BOgGqaZp.js +0 -144
- package/studio-dist/server/chunks/_server.ts-BV1HiKZ0.js +0 -120
- package/studio-dist/server/chunks/_server.ts-BWp3g6Cr.js +0 -110
- package/studio-dist/server/chunks/_server.ts-C5NF-UgC.js +0 -122
- package/studio-dist/server/chunks/_server.ts-CDMurH5m.js +0 -2301
- package/studio-dist/server/chunks/_server.ts-COJHCgbq.js +0 -127
- package/studio-dist/server/chunks/_server.ts-CUl-Utkt.js +0 -148
- package/studio-dist/server/chunks/_server.ts-D0GAFJRb.js +0 -156
- package/studio-dist/server/chunks/_server.ts-DA6p3pmi.js +0 -117
- package/studio-dist/server/chunks/_server.ts-DThYFaZj.js +0 -160
- package/studio-dist/server/chunks/_server.ts-D_3Pm0gr.js +0 -179
- package/studio-dist/server/chunks/_server.ts-DcHxdMsm.js +0 -144
- package/studio-dist/server/chunks/_server.ts-Dlfadh2I.js +0 -144
- package/studio-dist/server/chunks/_server.ts-JallJnps.js +0 -240
- package/studio-dist/server/chunks/_server.ts-NEFTbJpM.js +0 -142
- package/studio-dist/server/chunks/_server.ts-d-2NvRwX.js +0 -122
- package/studio-dist/server/chunks/_server.ts-rUWMLkIw.js +0 -146
- package/studio-dist/server/chunks/abort-events-CDRAJSqb.js +0 -30
- package/studio-dist/server/chunks/adapters-BkMgPNUM.js +0 -359
- package/studio-dist/server/chunks/adapters2-CQ3PzoaK.js +0 -28
- package/studio-dist/server/chunks/api-mode2-3W-I1-XO.js +0 -36
- package/studio-dist/server/chunks/api2-CmNQzb0b.js +0 -96
- package/studio-dist/server/chunks/attention-consolidate-CidG1jfH.js +0 -485
- package/studio-dist/server/chunks/audit3-DMO_fUGp.js +0 -52
- package/studio-dist/server/chunks/auto-extract-B84La1Vs.js +0 -34
- package/studio-dist/server/chunks/brain-consolidator-JGm6mzFf.js +0 -373
- package/studio-dist/server/chunks/brain-doctor-Dc2WqVbs.js +0 -311
- package/studio-dist/server/chunks/brain-embedding-Djw9ekXu.js +0 -42
- package/studio-dist/server/chunks/brain-lifecycle-BOOa876K.js +0 -969
- package/studio-dist/server/chunks/brain-maintenance-U6VvsvmK.js +0 -133
- package/studio-dist/server/chunks/brain-maintenance2-CwVSmfDT.js +0 -43
- package/studio-dist/server/chunks/brain-retrieval2-exR8mdgh.js +0 -751
- package/studio-dist/server/chunks/brain-search-CpSh-SNi.js +0 -48
- package/studio-dist/server/chunks/brain-search2-CwI2cXgg.js +0 -716
- package/studio-dist/server/chunks/brain-similarity-DI4yzm9e.js +0 -113
- package/studio-dist/server/chunks/brain-stdp2-BxSBIlSr.js +0 -960
- package/studio-dist/server/chunks/brain-writer-handlers-Dwu0QCAA.js +0 -188
- package/studio-dist/server/chunks/brain-writer-thread-6n7wI7Gy.js +0 -360
- package/studio-dist/server/chunks/branch-lock-Cq7zzmFK.js +0 -1118
- package/studio-dist/server/chunks/branch-lock2-CmDc4CZW.js +0 -45
- package/studio-dist/server/chunks/cancelled-child-waiver-audit-DkesrN83.js +0 -54
- package/studio-dist/server/chunks/catalog-model-resolver-Bas9qnoP.js +0 -9
- package/studio-dist/server/chunks/catalog-model-resolver2-Cexk2TN0.js +0 -237
- package/studio-dist/server/chunks/catalog-resolver-CLot3XJO.js +0 -260
- package/studio-dist/server/chunks/catalog-resolver2-JmKb91kY.js +0 -33
- package/studio-dist/server/chunks/chunk-DDP5xfzh.js +0 -39
- package/studio-dist/server/chunks/cleo-home-CCfe6kAQ.js +0 -64
- package/studio-dist/server/chunks/concrete-session2-bb_vxJ2N.js +0 -43
- package/studio-dist/server/chunks/config-DHj9CcFs.js +0 -386
- package/studio-dist/server/chunks/config2-F6PPIn7D.js +0 -35
- package/studio-dist/server/chunks/connections-BMGJVYcS.js +0 -98
- package/studio-dist/server/chunks/credential-pool-vF6NdtT9.js +0 -4600
- package/studio-dist/server/chunks/credentials-C7EJFf5O.js +0 -35
- package/studio-dist/server/chunks/credentials-store-CFygIuB2.js +0 -35
- package/studio-dist/server/chunks/cross-db-cleanup-1LWTUKPi.js +0 -147
- package/studio-dist/server/chunks/data-accessor-CWgVndbR.js +0 -72
- package/studio-dist/server/chunks/data-safety-central-DaFxK27G.js +0 -683
- package/studio-dist/server/chunks/decisions-CmVXV2-H.js +0 -574
- package/studio-dist/server/chunks/decisions2-XQsSYdxa.js +0 -48
- package/studio-dist/server/chunks/depends-waiver-audit-CUmlBYHx.js +0 -56
- package/studio-dist/server/chunks/dialectic-evaluator2-BOyyXhdI.js +0 -96
- package/studio-dist/server/chunks/dist-es2-DQy2Mjn_.js +0 -43
- package/studio-dist/server/chunks/dist-es3-CBygU4n5.js +0 -365
- package/studio-dist/server/chunks/dist-es4-CavbNu4B.js +0 -162
- package/studio-dist/server/chunks/dist-es5-Crpm8Wsl.js +0 -465
- package/studio-dist/server/chunks/dist-es6-CgePHzh4.js +0 -99
- package/studio-dist/server/chunks/dist-es7-k6c5GEd7.js +0 -311
- package/studio-dist/server/chunks/dist-es8-Bd9v0_pE.js +0 -91
- package/studio-dist/server/chunks/dist3-23bsJhRw.js +0 -27
- package/studio-dist/server/chunks/dist4-DmC1zSXi.js +0 -35618
- package/studio-dist/server/chunks/dual-scope-db-DKnWu1KM.js +0 -5672
- package/studio-dist/server/chunks/dual-scope-db2-Dht3luRw.js +0 -31
- package/studio-dist/server/chunks/duplicate-bypass-audit-BN5w1H2y.js +0 -36
- package/studio-dist/server/chunks/duplicate-detector-CaVFMKdE.js +0 -692
- package/studio-dist/server/chunks/edge-types-CgBvb0w1.js +0 -28
- package/studio-dist/server/chunks/embedding-local-xEnym8Ho.js +0 -92
- package/studio-dist/server/chunks/event-streams-Bi2p_fVo.js +0 -232
- package/studio-dist/server/chunks/execution-learning-BVCw7Plq.js +0 -278
- package/studio-dist/server/chunks/exodus-BBuuN6aA.js +0 -2594
- package/studio-dist/server/chunks/externalDataInterceptor-C3C4cObe.js +0 -21
- package/studio-dist/server/chunks/extraction-gate-DDNBq7gu.js +0 -48
- package/studio-dist/server/chunks/extraction-gate2-D77EZBQ6.js +0 -375
- package/studio-dist/server/chunks/getSSOTokenFromFile-yZAJxDci.js +0 -21
- package/studio-dist/server/chunks/governor2-CtfV3wfX.js +0 -923
- package/studio-dist/server/chunks/graph-auto-populate-Dmsr9ZJM.js +0 -42
- package/studio-dist/server/chunks/graph-auto-populate2-Cz0qeebM.js +0 -156
- package/studio-dist/server/chunks/graph-memory-bridge-CY8ft_d7.js +0 -43
- package/studio-dist/server/chunks/graph-memory-bridge2-Cd0XZwqc.js +0 -429
- package/studio-dist/server/chunks/handler-helpers-AANBGA_p.js +0 -49
- package/studio-dist/server/chunks/hash-Cm6yMET4.js +0 -14
- package/studio-dist/server/chunks/health-monitor-FepUZN1m.js +0 -106
- package/studio-dist/server/chunks/identity-DRpurt56.js +0 -30
- package/studio-dist/server/chunks/identity2-DeAB5RXE.js +0 -114
- package/studio-dist/server/chunks/index-61916rmV.js +0 -632
- package/studio-dist/server/chunks/index-DAgVDouG.js +0 -268
- package/studio-dist/server/chunks/index-Dtt_h58T.js +0 -2116
- package/studio-dist/server/chunks/indexes-DZ52grq6.js +0 -615
- package/studio-dist/server/chunks/json-Bfixam3g.js +0 -126
- package/studio-dist/server/chunks/learnings-D6Y3YGMU.js +0 -47
- package/studio-dist/server/chunks/learnings2-MwBdR9vb.js +0 -171
- package/studio-dist/server/chunks/lifecycle-ops2-V2CR5-7X.js +0 -121
- package/studio-dist/server/chunks/list-BhObczvn.js +0 -229
- package/studio-dist/server/chunks/living-brain2-j0B_Ir8j.js +0 -285
- package/studio-dist/server/chunks/llm-backend-resolver-D4ANvUPL.js +0 -295
- package/studio-dist/server/chunks/llm-extraction-DVCu4heR.js +0 -408
- package/studio-dist/server/chunks/loadSharedConfigFiles-umvW7kuz.js +0 -73
- package/studio-dist/server/chunks/loadSso-iidd5iKx.js +0 -599
- package/studio-dist/server/chunks/local-model-fit-B0TxifnX.js +0 -53
- package/studio-dist/server/chunks/local-model-fit2-BMqo6GJm.js +0 -2664
- package/studio-dist/server/chunks/lock-CA7d6lj7.js +0 -57
- package/studio-dist/server/chunks/logger-DQIoyZbq.js +0 -34
- package/studio-dist/server/chunks/memory-BUbicBbE.js +0 -618
- package/studio-dist/server/chunks/memory-accessor-i3P-RHSg.js +0 -42
- package/studio-dist/server/chunks/memory-accessor2-B981tHDR.js +0 -467
- package/studio-dist/server/chunks/memory-bridge-BOlEWg9w.js +0 -424
- package/studio-dist/server/chunks/memory-sqlite2-B7dq8doO.js +0 -42
- package/studio-dist/server/chunks/model-runner2-B71xjp4y.js +0 -43
- package/studio-dist/server/chunks/mvi-helpers-BNsJ8ZpQ.js +0 -59
- package/studio-dist/server/chunks/nexus-plasticity-BTfU8cW_.js +0 -269
- package/studio-dist/server/chunks/nexus-risk-audit-p379-TU5.js +0 -54
- package/studio-dist/server/chunks/nexus-schema-zeygY8OD.js +0 -11
- package/studio-dist/server/chunks/nexus-schema2-DP0Qd7CA.js +0 -444
- package/studio-dist/server/chunks/nexus-sqlite-BVNIpwc7.js +0 -36
- package/studio-dist/server/chunks/nexus-sqlite2-BBfY2xnY.js +0 -1103
- package/studio-dist/server/chunks/node-http-handler-CJI8kn7X.js +0 -386
- package/studio-dist/server/chunks/observe-Bo9veHGe.js +0 -47
- package/studio-dist/server/chunks/observe2-wzO0MaMY.js +0 -245
- package/studio-dist/server/chunks/observer-reflector-BwNOuq1d.js +0 -62
- package/studio-dist/server/chunks/observer-reflector2-ggtC1hmk.js +0 -478
- package/studio-dist/server/chunks/on-open-Wpcw7DV4.js +0 -740
- package/studio-dist/server/chunks/open-cleo-db--8sYNaYY.js +0 -70
- package/studio-dist/server/chunks/parseKnownFiles-Dg0pMFcT.js +0 -18
- package/studio-dist/server/chunks/parseUrl-qgGfl-Vh.js +0 -120
- package/studio-dist/server/chunks/paths-CV7S5-Zm.js +0 -30012
- package/studio-dist/server/chunks/patterns-lh5kn13I.js +0 -48
- package/studio-dist/server/chunks/patterns2-CzIcT-ai.js +0 -177
- package/studio-dist/server/chunks/pipeline-stage-IB_pOft-.js +0 -63
- package/studio-dist/server/chunks/prediction-D1lSGJ4c.js +0 -217
- package/studio-dist/server/chunks/premature-close-audit-VG09uAco.js +0 -53
- package/studio-dist/server/chunks/project-context-BjiqfEBi.js +0 -166
- package/studio-dist/server/chunks/provider-registry2-Cu7rAF0R.js +0 -35
- package/studio-dist/server/chunks/quality-feedback-CuCGEI6S.js +0 -371
- package/studio-dist/server/chunks/quality-scoring-CCPuRQmJ.js +0 -160
- package/studio-dist/server/chunks/query-ops-DCrjbZb9.js +0 -78
- package/studio-dist/server/chunks/query-promise-BkPmGPJy.js +0 -1257
- package/studio-dist/server/chunks/readFile-CxwXhymG.js +0 -31
- package/studio-dist/server/chunks/registry-CL2gN1zd.js +0 -27
- package/studio-dist/server/chunks/registry2-DoPYJnUW.js +0 -233
- package/studio-dist/server/chunks/registry3-BajEb-ll.js +0 -42
- package/studio-dist/server/chunks/registry4-DCqffZkT.js +0 -142
- package/studio-dist/server/chunks/retry-B3oEKYpp.js +0 -105
- package/studio-dist/server/chunks/role-executor-CC8BU54L.js +0 -25379
- package/studio-dist/server/chunks/role-resolver-D9QaDT0e.js +0 -6544
- package/studio-dist/server/chunks/role-resolver2-nkhzS1oy.js +0 -37
- package/studio-dist/server/chunks/rollup-CiefjN_Y.js +0 -194
- package/studio-dist/server/chunks/safety-data-accessor-CniDmGJO.js +0 -276
- package/studio-dist/server/chunks/service-connections-accessor-yBCdcgU-.js +0 -282
- package/studio-dist/server/chunks/session-factory-vbengc42.js +0 -96
- package/studio-dist/server/chunks/session-grade-BF4ZZtN_.js +0 -244
- package/studio-dist/server/chunks/session-journal-Bvey70Ur.js +0 -235
- package/studio-dist/server/chunks/session-narrative-BirZINSU.js +0 -304
- package/studio-dist/server/chunks/session-store--N_xHxUH.js +0 -557
- package/studio-dist/server/chunks/session-store2-DAM-eVTH.js +0 -43
- package/studio-dist/server/chunks/sessions-33DD4hkt.js +0 -2273
- package/studio-dist/server/chunks/sessions2-BHswKRDP.js +0 -50
- package/studio-dist/server/chunks/sigil-B-RznFXz.js +0 -33
- package/studio-dist/server/chunks/sigil2-jRMG5DuR.js +0 -47
- package/studio-dist/server/chunks/signin-BAsW3mM8.js +0 -644
- package/studio-dist/server/chunks/sleep-consolidation-BnSwXrvq.js +0 -823
- package/studio-dist/server/chunks/specialists-B_ZbpQlK.js +0 -441
- package/studio-dist/server/chunks/sqlite-Bm5LzbPs.js +0 -5977
- package/studio-dist/server/chunks/sqlite-backup-CFG1-b2e.js +0 -42
- package/studio-dist/server/chunks/sqlite-data-accessor-DD_RFX6r.js +0 -987
- package/studio-dist/server/chunks/sqlite-data-accessor2-CRmgNBnq.js +0 -44
- package/studio-dist/server/chunks/sqlite-native-BwlIqtbs.js +0 -42
- package/studio-dist/server/chunks/sqlite2-CUPut2fh.js +0 -42
- package/studio-dist/server/chunks/src2-DK8DKufK.js +0 -290
- package/studio-dist/server/chunks/src3-BJjf7XRG.js +0 -798
- package/studio-dist/server/chunks/src4-DNleK_t_.js +0 -28
- package/studio-dist/server/chunks/sso-oidc-eUvwseuL.js +0 -861
- package/studio-dist/server/chunks/storage-B9UA3BHR.js +0 -198
- package/studio-dist/server/chunks/sts-g7LDMLEd.js +0 -4655
- package/studio-dist/server/chunks/surprisal-CwTfWQi8.js +0 -224
- package/studio-dist/server/chunks/surprisal-tree-7dfHsuw1.js +0 -230
- package/studio-dist/server/chunks/system-resolver2-Bzhue_zQ.js +0 -53
- package/studio-dist/server/chunks/tasks-SHX7UPGY.js +0 -19694
- package/studio-dist/server/chunks/tasks-schema2-CTMoQKQy.js +0 -42
- package/studio-dist/server/chunks/temporal-supersession-DP4GPG_B.js +0 -298
- package/studio-dist/server/chunks/token-cAhAD9f9.js +0 -139
- package/studio-dist/server/chunks/token-util-D6drEz8_.js +0 -353
- package/studio-dist/server/chunks/token-util2-8UPSGvbb.js +0 -97
- package/studio-dist/server/chunks/transcript-scanner-kJWhWmlg.js +0 -53
- package/studio-dist/server/chunks/transformers.node-_xbxmK5p.js +0 -38475
- package/studio-dist/server/chunks/typed-query-l1D_QElt.js +0 -18
- package/studio-dist/server/chunks/unique-constraint-BZrrwgwc.js +0 -88
- package/studio-dist/server/chunks/update-B2YVBZG0.js +0 -1594
- package/studio-dist/server/chunks/user-profile-B61ZhVmC.js +0 -89
- package/studio-dist/server/chunks/utils-BPSZyajG.js +0 -1176
- package/studio-dist/server/chunks/validation-rules-CE27rtoF.js +0 -44
- package/studio-dist/server/chunks/zod-DsAeKNLm.js +0 -141
- /package/studio-dist/client/_app/immutable/chunks/{CQCxd1p9.js → B8WnZMYa.js} +0 -0
- /package/studio-dist/client/_app/immutable/chunks/{D13Quv9e2.js → B_0_YCYG.js} +0 -0
- /package/studio-dist/client/_app/immutable/chunks/{djL6V-tr.js → CqgtdAx_.js} +0 -0
- /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
|