very-happy-cli 0.2.191 → 0.2.193
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/{AcpBackend-CCy_9AfR.mjs → AcpBackend-B4PwjqHt.mjs} +3 -3
- package/dist/{AcpBackend-DgrIc1vH.cjs → AcpBackend-DtJCgftG.cjs} +3 -3
- package/dist/{AcpSessionManager-CQnxx7gD.mjs → AcpSessionManager-BzKWgkdP.mjs} +2 -2
- package/dist/{AcpSessionManager-DVOzCD3P.cjs → AcpSessionManager-D4qshX_H.cjs} +2 -2
- package/dist/{BasePermissionHandler-C9Ag7yvs.cjs → BasePermissionHandler-5iG-cvA2.cjs} +1 -1
- package/dist/{BasePermissionHandler-DYLN6Ec9.mjs → BasePermissionHandler-DV6UM6e9.mjs} +1 -1
- package/dist/account-Dco_nGT4.mjs +297 -0
- package/dist/account-DvpA60oU.cjs +302 -0
- package/dist/accountService-Cd7bnkfd.mjs +916 -0
- package/dist/accountService-CuUSJDhP.cjs +921 -0
- package/dist/{acpSessionResume-TvP_4Zq2.cjs → acpSessionResume-Bk9mjXZ9.cjs} +1 -1
- package/dist/{acpSessionResume-Bi7pA1n_.mjs → acpSessionResume-TQwN9ZZE.mjs} +1 -1
- package/dist/{agentHome-DGv105SR.mjs → agentHome-Bep8semY.mjs} +4 -4
- package/dist/{agentHome-DNUKJWOY.cjs → agentHome-c2dlgcw1.cjs} +4 -4
- package/dist/{api-BBxZ7y2z.cjs → api-CBE_a88I.cjs} +10 -10
- package/dist/{api-jfeMRg5R.mjs → api-DUfunY3x.mjs} +10 -10
- package/dist/{apiMachine-DqyjKBZG.cjs → apiMachine-BqtG0IWm.cjs} +83 -27
- package/dist/{apiMachine-DRJ7y-BB.mjs → apiMachine-CU_bw5nV.mjs} +78 -22
- package/dist/{attachmentContent-BO2njyza.cjs → attachmentContent-B3AzuYqk.cjs} +2 -2
- package/dist/{attachmentContent-Dm_RIs5L.mjs → attachmentContent-KNIZxwlu.mjs} +2 -2
- package/dist/{auth-BYDtjVNb.mjs → auth-CZ7pKS-j.mjs} +8 -8
- package/dist/{auth-DnxoVkkf.cjs → auth-DCjxpI6Z.cjs} +5 -5
- package/dist/{auth-DLR3ksCF.cjs → auth-SJ_dPw7H.cjs} +8 -8
- package/dist/{auth-DC6rg8lJ.mjs → auth-jN5dNqTO.mjs} +5 -5
- package/dist/{auto-CwO0u8hB.cjs → auto-B_59RYH1.cjs} +23 -21
- package/dist/{auto-D5Jhukq1.mjs → auto-DC0Sq-qB.mjs} +23 -21
- package/dist/{bootstrap-rLaL3kTa.mjs → bootstrap-B3gMpU9e.mjs} +2 -2
- package/dist/{bootstrap-sly4qPPY.cjs → bootstrap-CoJu9VnB.cjs} +2 -2
- package/dist/{browser-CHpVgyni.mjs → browser-LHW2hBH8.mjs} +1 -1
- package/dist/{browser-BTgZ9d4i.cjs → browser-v1pdPP1E.cjs} +1 -1
- package/dist/claudeAccountEnv-BL1pR_37.mjs +133 -0
- package/dist/claudeAccountEnv-CEX_EMZr.cjs +138 -0
- package/dist/{claudeLocal-Bgb5GGsq.mjs → claudeLocal-BZUgA5G2.mjs} +5 -5
- package/dist/{claudeLocal-1vuTFcL8.cjs → claudeLocal-BrzM2RBI.cjs} +5 -5
- package/dist/{claudeLocalLauncher-T7EfT68h.mjs → claudeLocalLauncher-A0AQyHFi.mjs} +10 -10
- package/dist/{claudeLocalLauncher-DOsL6HFY.cjs → claudeLocalLauncher-XAMpXUM7.cjs} +10 -10
- package/dist/{claudeSessionFork-Cko4SaVi.cjs → claudeSessionFork-Mhn2fYe_.cjs} +1 -1
- package/dist/{claudeSessionFork-CPvi6yDX.mjs → claudeSessionFork-_GwIlmMB.mjs} +1 -1
- package/dist/{client-BXD66ZF5.mjs → client-7RnI9J0W.mjs} +2 -2
- package/dist/{client-Bi20Ka0h.mjs → client-Bde4QVIn.mjs} +4 -2
- package/dist/{client-C5-C0t2a.cjs → client-CJr8Pjwm.cjs} +4 -2
- package/dist/{client-BscskEBk.cjs → client-D2GWkd-T.cjs} +2 -2
- package/dist/{client-C-OdTbrU.cjs → client-DLlnmAPL.cjs} +4 -4
- package/dist/{client-Bh1aUPh-.mjs → client-DwbhrX6D.mjs} +4 -4
- package/dist/codex/happyMcpStdioBridge.cjs +16 -16
- package/dist/codex/happyMcpStdioBridge.mjs +16 -16
- package/dist/codexAccount-BDQjRbtI.cjs +1028 -0
- package/dist/codexAccount-NOX5pmQf.mjs +1015 -0
- package/dist/{codexAppServerClient-DXhAf6i-.mjs → codexAppServerClient-B3QJV6Ey.mjs} +18 -7
- package/dist/{codexAppServerClient-DKXeikEe.cjs → codexAppServerClient-DWEsZUC4.cjs} +18 -7
- package/dist/{codexCommand-DvbbpYBS.cjs → codexCommand-Bg8AmxVm.cjs} +195 -63
- package/dist/{codexCommand-CIaOiiOj.mjs → codexCommand-D1xlj4sT.mjs} +196 -64
- package/dist/{codexTermination-BszWfyk5.cjs → codexTermination-ByX9GcEJ.cjs} +1 -1
- package/dist/{codexTermination-BcQdEDaw.mjs → codexTermination-CaWAVQjZ.mjs} +1 -1
- package/dist/{config-Di3Cf6js.cjs → config-B2GGbiTv.cjs} +3 -3
- package/dist/{config-OCEcVVK1.mjs → config-C2XYEwXz.mjs} +3 -3
- package/dist/{configuration-DfPBa-9d.cjs → configuration-BBK0rmmj.cjs} +1 -1
- package/dist/{configuration-ECqXu34R.mjs → configuration-ThKBP_zY.mjs} +1 -1
- package/dist/{connect-BTfAUhk8.mjs → connect-BirnCl9q.mjs} +13 -13
- package/dist/{connect-BPQRuOw5.cjs → connect-C3tZs_NC.cjs} +13 -13
- package/dist/{context-CfgTFpCB.mjs → context-C0EFLnOg.mjs} +1 -1
- package/dist/{context-Co7hUaW-.cjs → context-wDp0aZBK.cjs} +1 -1
- package/dist/{controlClient-ClI7A0qE.mjs → controlClient-Ckl8N1MS.mjs} +14 -5
- package/dist/{controlClient-C13_pSKG.cjs → controlClient-DwWTns3A.cjs} +15 -4
- package/dist/{createSessionMetadata-Cf78KHtt.cjs → createSessionMetadata-DqZ6h02H.cjs} +2 -2
- package/dist/{createSessionMetadata-B0eT_Kfj.mjs → createSessionMetadata-vug4Cv8z.mjs} +2 -2
- package/dist/daemonAccounts-CpGDY_4k.cjs +54 -0
- package/dist/daemonAccounts-DwPwKxvv.mjs +30 -0
- package/dist/{defaultPermissionMode-oqTupStc.cjs → defaultPermissionMode-BakXiqRr.cjs} +1 -1
- package/dist/{defaultPermissionMode-m6z2QVyP.mjs → defaultPermissionMode-DF5AfeBb.mjs} +1 -1
- package/dist/{deterministicJson-BtSwbsok.mjs → deterministicJson-BzX94gi3.mjs} +13 -13
- package/dist/{deterministicJson-CpFFgUL_.cjs → deterministicJson-t33SsuRC.cjs} +13 -13
- package/dist/{doctor-BEU0_1Xv.cjs → doctor-B9ug1LZb.cjs} +7 -7
- package/dist/{doctor-lsxmAK4o.mjs → doctor-C1zyOSXw.mjs} +7 -7
- package/dist/{downloadTarget-BXqXhNF7.mjs → downloadTarget-Baslc6jW.mjs} +6 -6
- package/dist/{downloadTarget-CikzJv1h.cjs → downloadTarget-T1id2Gh6.cjs} +6 -6
- package/dist/{ensureDaemonRunning-B_9WAlbU.mjs → ensureDaemonRunning-C6ERYK-O.mjs} +5 -5
- package/dist/{ensureDaemonRunning-BUuH-pbE.cjs → ensureDaemonRunning-CoAg11Zi.cjs} +5 -5
- package/dist/{handleResumeCommand-BK9XKH_l.mjs → handleResumeCommand-CrlqDuz7.mjs} +5 -5
- package/dist/{handleResumeCommand-Dj7oi2KO.cjs → handleResumeCommand-DPi-sPm7.cjs} +5 -5
- package/dist/{index-D3c7CLot.cjs → index-B1swQJUS.cjs} +4 -4
- package/dist/{index-CmZusg8Q.cjs → index-B2dJA8VG.cjs} +3 -3
- package/dist/{index-CLUHaPU8.cjs → index-BUFrKZeQ.cjs} +205 -5
- package/dist/{index-BjZnpGWX.mjs → index-BbdV2mzy.mjs} +3 -3
- package/dist/{index-BdOJOZ6J.mjs → index-BcSPg3c5.mjs} +4 -4
- package/dist/{index-CauRYQKr.cjs → index-Bkslzin5.cjs} +44 -40
- package/dist/{index-DuK9XYbZ.mjs → index-Cn73rdtQ.mjs} +4 -4
- package/dist/{index-Bq8NKSCF.mjs → index-D1YnIih6.mjs} +195 -6
- package/dist/{index-Cxso1rg4.mjs → index-fZO4nF6e.mjs} +45 -41
- package/dist/{index-Dg2NAF_-.cjs → index-rocrvOYG.cjs} +4 -4
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{install-DXllkuy0.cjs → install-CArXjPfW.cjs} +3 -3
- package/dist/{install-DTIveLOS.mjs → install-CChhyV2G.mjs} +3 -3
- package/dist/lib.cjs +12 -12
- package/dist/lib.d.cts +354 -0
- package/dist/lib.d.mts +354 -0
- package/dist/lib.mjs +12 -12
- package/dist/{lidState-CD81ryTy.mjs → lidState-B6yYKjNK.mjs} +1 -1
- package/dist/{lidState-C9_DoF9i.cjs → lidState-B_v6qgfE.cjs} +1 -1
- package/dist/{localHappyAgentAuth-Cg-m1lwm.mjs → localHappyAgentAuth-KRlg_aI0.mjs} +1 -1
- package/dist/{localHappyAgentAuth-CCjUvOfi.cjs → localHappyAgentAuth-jJH0n67S.cjs} +1 -1
- package/dist/{logger-Ct2zvaq0.cjs → logger-CX3x2eI4.cjs} +2 -2
- package/dist/{logger-GcbI0eg2.mjs → logger-JV7-4Q_6.mjs} +2 -2
- package/dist/{machineMetadata-CXRmFNOV.mjs → machineMetadata-D5-SW9kv.mjs} +3 -3
- package/dist/{machineMetadata-DDCVWb_x.cjs → machineMetadata-DhZuM0iB.cjs} +3 -3
- package/dist/{main-BHC-WIcM.cjs → main-1x1HGQEy.cjs} +92 -48
- package/dist/{main-omGr_ONk.mjs → main-CFOdXbPD.mjs} +92 -48
- package/dist/{mcp-xUzii0m8.cjs → mcp-BnKBSEh7.cjs} +18 -15
- package/dist/{mcp-D5hxmRAq.mjs → mcp-R6lMcFLO.mjs} +18 -15
- package/dist/{mirrorProtocol-Lcx20Oby.mjs → mirrorProtocol-CD4ntuZJ.mjs} +3 -3
- package/dist/{mirrorProtocol-Dfl8Ahyf.cjs → mirrorProtocol-DO-KtvAL.cjs} +3 -3
- package/dist/{package-BsUHyqWe.mjs → package-Bw3H6VLn.mjs} +7 -7
- package/dist/{package-9n7wd2dT.cjs → package-uzlvxa5S.cjs} +7 -7
- package/dist/{panels-CpEKWWbI.mjs → panels-BamhZCoZ.mjs} +5 -5
- package/dist/{panels-BfknEmt7.cjs → panels-C46FxjmH.cjs} +5 -5
- package/dist/{peerTools-CElYh-5u.cjs → peerTools-D-VNNFw-.cjs} +5 -5
- package/dist/{peerTools-CwCe6tmJ.mjs → peerTools-WtRgTSqG.mjs} +5 -5
- package/dist/{persistence-Cpw4yJs8.mjs → persistence-Cg4sfjQV.mjs} +2 -2
- package/dist/{persistence-A2NeERZJ.cjs → persistence-CsmAgN-C.cjs} +2 -2
- package/dist/{piRunnerArgs-Bqmu16He.mjs → piRunnerArgs-BbaAQvb4.mjs} +2 -2
- package/dist/{piRunnerArgs-BnFU0K73.cjs → piRunnerArgs-vnFygygF.cjs} +1 -1
- package/dist/{piRuntime-Cgn1b0OX.mjs → piRuntime-Bid7TBf5.mjs} +1 -1
- package/dist/{piRuntime-DpS9TAIs.cjs → piRuntime-C2uj5Ucx.cjs} +1 -1
- package/dist/{piTerminal-rmve-VlI.cjs → piTerminal-Ckpjfoqa.cjs} +20 -17
- package/dist/{piTerminal-BGdps8f3.mjs → piTerminal-CqxgrI13.mjs} +20 -17
- package/dist/{pushNotifications-23YSd-0l.cjs → pushNotifications-BCg7YAiu.cjs} +5 -3
- package/dist/{pushNotifications-XAM8__Pg.mjs → pushNotifications-BbE-qV9I.mjs} +5 -3
- package/dist/{query-SZcYnJoW.mjs → query-Bfr9O_62.mjs} +1 -1
- package/dist/{query-u2GX-I0C.cjs → query-vQ-HG394.cjs} +1 -1
- package/dist/{rateLimitReporter-CaTzBe3x.cjs → rateLimitReporter-BZ8x0-6-.cjs} +3 -3
- package/dist/{rateLimitReporter-COUkf446.mjs → rateLimitReporter-CAz7VMw4.mjs} +3 -3
- package/dist/{rateLimitWindows-B2Kx6L0S.cjs → rateLimitWindows-BZl6GMwQ.cjs} +1 -1
- package/dist/{rateLimitWindows-j_qQk0KY.mjs → rateLimitWindows-CRARnKaI.mjs} +1 -1
- package/dist/reconnectEnv-B31lY4nR.cjs +27 -0
- package/dist/reconnectEnv-DeSvHl3q.mjs +24 -0
- package/dist/{registerCommonHandlers-D1MiSfcB.cjs → registerCommonHandlers-CZGmrPbT.cjs} +2 -2
- package/dist/{registerCommonHandlers-DRRfinzK.mjs → registerCommonHandlers-Cj3Nl9y-.mjs} +2 -2
- package/dist/{registerKillSessionHandler-Cpug_r5C.mjs → registerKillSessionHandler-Bg9t45Ub.mjs} +6 -6
- package/dist/{registerKillSessionHandler-hvCsEn7b.cjs → registerKillSessionHandler-C3EDnW4F.cjs} +6 -6
- package/dist/{remoteSessionClient-BdnJ_n8C.mjs → remoteSessionClient--0vfA7I5.mjs} +5 -5
- package/dist/{remoteSessionClient-6ymeMVa4.cjs → remoteSessionClient-CcSP24Zq.cjs} +5 -5
- package/dist/{reportProgressTool-XgQBzxoD.mjs → reportProgressTool-DVru7jc_.mjs} +3 -3
- package/dist/{reportProgressTool-tzQLCLIw.cjs → reportProgressTool-Ddg1yVq2.cjs} +3 -3
- package/dist/{run-DYL_wuj7.cjs → run-BTrIT_jU.cjs} +347 -856
- package/dist/{run-Bt0AklUU.mjs → run-HtvH6BLi.mjs} +346 -855
- package/dist/{runClaude-C3rii_S6.mjs → runClaude-DsHTk5Bu.mjs} +62 -44
- package/dist/{runClaude-CBEpYO4S.cjs → runClaude-ipjwAHPI.cjs} +62 -44
- package/dist/{runClaudeDeps-CYuozSC6.cjs → runClaudeDeps-BKpwL6ln.cjs} +51 -42
- package/dist/{runClaudeDeps-DlVfMG4F.mjs → runClaudeDeps-BV3vpeyY.mjs} +56 -47
- package/dist/{runGemini-CEI3b90u.cjs → runGemini-B-G0_7Ab.cjs} +33 -33
- package/dist/{runGemini-DkYhRk9S.mjs → runGemini-DhjK6_Fp.mjs} +33 -33
- package/dist/{runOpenClaw-DpprWCoz.mjs → runOpenClaw-B6lawk8n.mjs} +18 -18
- package/dist/{runOpenClaw-BgjD6E4d.cjs → runOpenClaw-BHIo8tkn.cjs} +18 -18
- package/dist/{sandbox-XCDUbH72.cjs → sandbox-BAsO0ILe.cjs} +4 -4
- package/dist/{sandbox-DJ7A7lsy.mjs → sandbox-CKb7pCXg.mjs} +4 -4
- package/dist/selfCheck-CZ-t1Xzq.mjs +88 -0
- package/dist/{selfCheck-BYUDB1Uq.cjs → selfCheck-D4VvVZNi.cjs} +39 -37
- package/dist/{send-CASDBCyJ.cjs → send-Cz5m39QR.cjs} +8 -8
- package/dist/{send-Ba63huxv.mjs → send-D8ipLKMY.mjs} +8 -8
- package/dist/{server-w3W0toNd.mjs → server-CjvgT3N9.mjs} +4 -4
- package/dist/{server-cZoV9XRt.cjs → server-CySyJKPH.cjs} +6 -6
- package/dist/{sessionLock-G2PMqvsh.mjs → sessionLock-B73fhgQN.mjs} +10 -4
- package/dist/{sessionLock-BmLA2S8K.cjs → sessionLock-DdDCuNUs.cjs} +10 -3
- package/dist/{sessionOps-DPw5k8vV.mjs → sessionOps-C65Iej_C.mjs} +50 -8
- package/dist/{sessionOps-DXlTmS7J.cjs → sessionOps-XSEjIqNq.cjs} +50 -8
- package/dist/sessionProvider-CLVNkfji.mjs +110 -0
- package/dist/sessionProvider-Dgsty8z_.cjs +115 -0
- package/dist/{sessionScanner-AHjnicxi.cjs → sessionScanner-D8IVXMMH.cjs} +3 -3
- package/dist/{sessionScanner-BCXpdZJN.mjs → sessionScanner-MW1P9Pm9.mjs} +3 -3
- package/dist/{sessions-BiBXOBx0.cjs → sessions-BKz7gZqg.cjs} +10 -10
- package/dist/{sessions-Clk8MR5Y.mjs → sessions-Dtk_agUL.mjs} +10 -10
- package/dist/{setupOfflineReconnection-DI-QZGYz.mjs → setupOfflineReconnection-Bvdy-2xd.mjs} +2 -2
- package/dist/{setupOfflineReconnection-BKLfOd-i.cjs → setupOfflineReconnection-DxMiW7OV.cjs} +2 -2
- package/dist/{spawn-D5omsx1Q.cjs → spawn-DiPkJcrQ.cjs} +36 -17
- package/dist/{spawn-4tsp1uhh.mjs → spawn-Dm4xsZBX.mjs} +36 -17
- package/dist/{spawnAgents-CtkkIFHd.cjs → spawnAgents-B9olJ8xn.cjs} +1 -1
- package/dist/{spawnAgents-CjoU6ZOS.mjs → spawnAgents-I29H9nO8.mjs} +1 -1
- package/dist/{spawnHappyCLI-CF_gLi92.cjs → spawnHappyCLI-BYGOUda1.cjs} +1 -1
- package/dist/{spawnHappyCLI-CIA9Sc_5.mjs → spawnHappyCLI-CnMwzjWX.mjs} +1 -1
- package/dist/{startFileWatcher-9Stpxe7p.cjs → startFileWatcher-D14W6gn-.cjs} +1 -1
- package/dist/{startFileWatcher-CDO6fb2m.mjs → startFileWatcher-DePAY1T5.mjs} +1 -1
- package/dist/{startupSessionCreate-BtR4iYEC.mjs → startupSessionCreate-DoK0IvJG.mjs} +3 -3
- package/dist/{startupSessionCreate-CcF7rGGB.cjs → startupSessionCreate-rBHd4tc1.cjs} +3 -3
- package/dist/store-CkNa6oj4.cjs +326 -0
- package/dist/store-Vej_738f.mjs +305 -0
- package/dist/{streamRelay-BhI6jqF-.cjs → streamRelay-C0G1HOOd.cjs} +1 -1
- package/dist/{streamRelay-DMwOMTKf.mjs → streamRelay-C5U-C4GS.mjs} +1 -1
- package/dist/{sync-C0PRuTMj.mjs → sync-BP6vnxVP.mjs} +1 -1
- package/dist/{sync-Ba8N6xba.cjs → sync-WYBwkJvG.cjs} +1 -1
- package/dist/{systemPrompt-DV5IReD4.cjs → systemPrompt-BkQlCk5_.cjs} +2 -2
- package/dist/{systemPrompt-D0q8eyjy.mjs → systemPrompt-aRqBmkpw.mjs} +2 -2
- package/dist/{teams-BO3_arwM.mjs → teams-CGmC4ygu.mjs} +7 -7
- package/dist/{teams-z3Gp-KlV.cjs → teams-phbpwAGD.cjs} +7 -7
- package/dist/{template-B_5l6pkc.cjs → template-B9pLEP8v.cjs} +2 -2
- package/dist/{template-DLR_0hfj.mjs → template-CL9EvQLQ.mjs} +2 -2
- package/dist/{time-Cy0a-7YX.cjs → time-BTLcO8jp.cjs} +1 -1
- package/dist/{time-D0jvDRhn.mjs → time-Cw0liIqB.mjs} +1 -1
- package/dist/{titleGenerator-RGHWP2YL.cjs → titleGenerator-Y1_7ceJC.cjs} +17 -13
- package/dist/{titleGenerator-AGQsXskb.mjs → titleGenerator-h6TmFe-c.mjs} +16 -12
- package/dist/{todo-CzIDPLFy.cjs → todo-BxiPiCvB.cjs} +6 -6
- package/dist/{todo-BZ954vsz.mjs → todo-OKcTC_VC.mjs} +6 -6
- package/dist/{tools-Bei5i03L.mjs → tools-BR-drXZ8.mjs} +3 -2
- package/dist/{tools-B1x1j6tz.cjs → tools-CcebYtLj.cjs} +3 -2
- package/dist/{uninstall-C3cnIvz1.cjs → uninstall-CnDeCC-M.cjs} +3 -3
- package/dist/{uninstall-B4T_e3Mr.mjs → uninstall-D3stWKD2.mjs} +3 -3
- package/dist/wrapperAccount-BkufO_9g.mjs +107 -0
- package/dist/wrapperAccount-D2A4DuZy.cjs +113 -0
- package/package.json +7 -7
- package/dist/providerBinding-B-Qbx9W2.cjs +0 -140
- package/dist/providerBinding-CYubGg-Z.mjs +0 -132
- package/dist/selfCheck-C-rNljKV.mjs +0 -86
- package/dist/sessionProvider-DUn91LhJ.mjs +0 -109
- package/dist/sessionProvider-Dg691XQ4.cjs +0 -114
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { G as AGENT_PROFILE_FEATURE_LAUNCH, H as AgentProfileAddRequestSchema, I as AgentProfileLoginStartRequestSchema, J as AgentProfileLoginSubmitRequestSchema, K as AgentProfileBindRequestSchema } from './index-D1YnIih6.mjs';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { A as ACCOUNT_STRIPPED_ENV_KEYS } from './claudeAccountEnv-BL1pR_37.mjs';
|
|
7
|
+
import { C as CODEX_ALWAYS_STRIPPED_ENV_KEYS, r as readCodexProviders, s as syncCodexOverlay, b as baseCodexHome } from './codexAccount-NOX5pmQf.mjs';
|
|
8
|
+
import { createInterface } from 'node:readline';
|
|
9
|
+
import { h as buildAgentProfilesState, d as defaultSecretProbe, a as readAccounts, s as summarizeAccount, c as readAccountsFileV2, n as newAccountId, i as writeAccountSecret, f as writeAccountsFile, j as sessionsBoundTo, r as readSessionBindings, k as removeAccountSecret, b as accountSecretPath, e as expandHome, l as accountCodexHomePath, m as SUBSCRIPTION_TOKEN_TTL_MS, w as writeSessionBinding, o as readSessionBindingV2 } from './store-Vej_738f.mjs';
|
|
10
|
+
|
|
11
|
+
const CHECK_TIMEOUT_MS = 2e4;
|
|
12
|
+
const DEFAULT_CHECK_MODEL = "claude-haiku-4-5";
|
|
13
|
+
function classifyHttpCheck(kind, status, bodySnippet, now) {
|
|
14
|
+
if (status >= 200 && status < 300) return { status: "ok", at: now };
|
|
15
|
+
if (status === 401 || status === 403) {
|
|
16
|
+
return { status: kind === "claude-subscription" ? "expired" : "invalid", at: now, detail: `HTTP ${status}` };
|
|
17
|
+
}
|
|
18
|
+
return { status: "unverified", at: now, detail: `HTTP ${status}${bodySnippet ? `: ${sanitizeDetail(bodySnippet)}` : ""}` };
|
|
19
|
+
}
|
|
20
|
+
function classifyClaudeCheck(exitCode, output, now) {
|
|
21
|
+
if (exitCode === 0) return { status: "ok", at: now };
|
|
22
|
+
if (/\b401\b|OAuth access token is invalid|invalid.*token|token.*(expired|revoked)/i.test(output)) {
|
|
23
|
+
return { status: "expired", at: now, detail: sanitizeDetail(lastLine(output)) };
|
|
24
|
+
}
|
|
25
|
+
return { status: "unverified", at: now, detail: sanitizeDetail(lastLine(output) || `exit ${exitCode}`) };
|
|
26
|
+
}
|
|
27
|
+
function lastLine(text) {
|
|
28
|
+
return text.trim().split("\n").filter(Boolean).pop() ?? "";
|
|
29
|
+
}
|
|
30
|
+
function sanitizeDetail(text) {
|
|
31
|
+
return text.replace(/sk-ant-[A-Za-z0-9_-]+/g, "sk-ant-\u2026").replace(/\b(sk|rk)-[A-Za-z0-9_-]{16,}/g, "$1-\u2026").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
32
|
+
}
|
|
33
|
+
async function checkApiAccount(record, key, now = Date.now) {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timer = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`${record.baseUrl}/v1/messages`, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
headers: {
|
|
41
|
+
"content-type": "application/json",
|
|
42
|
+
"anthropic-version": "2023-06-01",
|
|
43
|
+
"x-api-key": key,
|
|
44
|
+
authorization: `Bearer ${key}`
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
model: record.models?.[0] ?? DEFAULT_CHECK_MODEL,
|
|
48
|
+
max_tokens: 1,
|
|
49
|
+
messages: [{ role: "user", content: "ping" }]
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
const text = res.ok ? "" : (await res.text().catch(() => "")).slice(0, 400);
|
|
53
|
+
return classifyHttpCheck(record.kind, res.status, text, now());
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return { status: "unverified", at: now(), detail: sanitizeDetail(error instanceof Error ? error.message : String(error)) };
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function checkSubscriptionAccount(token, scratchDir, now = Date.now) {
|
|
61
|
+
mkdirSync(scratchDir, { recursive: true });
|
|
62
|
+
const settingsPath = join(scratchDir, "check-settings.json");
|
|
63
|
+
writeFileSync(settingsPath, JSON.stringify({ disableAllHooks: true }));
|
|
64
|
+
const env = { ...process.env };
|
|
65
|
+
for (const key of ACCOUNT_STRIPPED_ENV_KEYS) delete env[key];
|
|
66
|
+
env.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
let output = "";
|
|
69
|
+
const child = spawn("claude", ["-p", "ping", "--model", "haiku", "--no-session-persistence", "--strict-mcp-config", "--settings", settingsPath], {
|
|
70
|
+
cwd: scratchDir,
|
|
71
|
+
env,
|
|
72
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
73
|
+
});
|
|
74
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), CHECK_TIMEOUT_MS);
|
|
75
|
+
child.stdout.on("data", (d) => {
|
|
76
|
+
output += d;
|
|
77
|
+
});
|
|
78
|
+
child.stderr.on("data", (d) => {
|
|
79
|
+
output += d;
|
|
80
|
+
});
|
|
81
|
+
child.on("error", (error) => {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
resolve({ status: "unverified", at: now(), detail: sanitizeDetail(error.message) });
|
|
84
|
+
});
|
|
85
|
+
child.on("close", (code) => {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
resolve(classifyClaudeCheck(code, output.slice(-2e3), now()));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const LOGIN_CODE_TIMEOUT_MS = 10 * 6e4;
|
|
93
|
+
const LOGIN_URL_TIMEOUT_MS = 2e4;
|
|
94
|
+
const LOGIN_EXCHANGE_TIMEOUT_MS = 6e4;
|
|
95
|
+
const LOGIN_RETAIN_MS$1 = 10 * 6e4;
|
|
96
|
+
function stripAnsi(text) {
|
|
97
|
+
return text.replace(/\x1b\][^\x07\x1b]*(\x07|\x1b\\)/g, "").replace(/\x1b\[[0-9;?<>=]*[ -/]*[@-~]/g, " ").replace(/\x1b[()][A-Za-z0-9]/g, "").replace(/\x1b[=>78]/g, "");
|
|
98
|
+
}
|
|
99
|
+
function findAuthorizeUrl(clean) {
|
|
100
|
+
const m = clean.match(/https:\/\/[^\s]*\/oauth\/authorize\?[^\s]+/);
|
|
101
|
+
return m ? m[0] : null;
|
|
102
|
+
}
|
|
103
|
+
function findSetupToken(clean) {
|
|
104
|
+
const m = clean.match(/sk-ant-oat01-[A-Za-z0-9_-]{20,}/);
|
|
105
|
+
return m ? m[0] : null;
|
|
106
|
+
}
|
|
107
|
+
function findLoginError(clean) {
|
|
108
|
+
const m = clean.match(/(OAuth error:[^\n]*?)(?:\s{2,}|Press|$)/) ?? clean.match(/(Login failed[^\n]*)/);
|
|
109
|
+
return m ? m[1].replace(/\s+/g, " ").trim().slice(0, 200) : null;
|
|
110
|
+
}
|
|
111
|
+
function setupTokenEnv(base) {
|
|
112
|
+
const env = {};
|
|
113
|
+
for (const [k, v] of Object.entries(base)) if (typeof v === "string") env[k] = v;
|
|
114
|
+
for (const key of ACCOUNT_STRIPPED_ENV_KEYS) delete env[key];
|
|
115
|
+
env.BROWSER = "true";
|
|
116
|
+
env.TERM = "xterm-256color";
|
|
117
|
+
return env;
|
|
118
|
+
}
|
|
119
|
+
class SetupTokenLoginManager {
|
|
120
|
+
constructor(deps) {
|
|
121
|
+
this.deps = deps;
|
|
122
|
+
this.now = deps.now ?? Date.now;
|
|
123
|
+
}
|
|
124
|
+
logins = /* @__PURE__ */ new Map();
|
|
125
|
+
now;
|
|
126
|
+
/** One login at a time per machine: a second start cancels nothing and is refused. */
|
|
127
|
+
activeLoginId() {
|
|
128
|
+
for (const [id, e] of this.logins) if (e.status.state === "waiting-code" || e.status.state === "exchanging") return id;
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
async start() {
|
|
132
|
+
const busy = this.activeLoginId();
|
|
133
|
+
if (busy) throw Object.assign(new Error("another login is in progress"), { code: "login-busy", loginId: busy });
|
|
134
|
+
const loginId = this.deps.newId();
|
|
135
|
+
const entry = {
|
|
136
|
+
status: { loginId, state: "starting" },
|
|
137
|
+
pty: null,
|
|
138
|
+
output: "",
|
|
139
|
+
timers: []
|
|
140
|
+
};
|
|
141
|
+
this.logins.set(loginId, entry);
|
|
142
|
+
const urlReady = new Promise((resolve) => {
|
|
143
|
+
entry.resolveUrl = resolve;
|
|
144
|
+
});
|
|
145
|
+
try {
|
|
146
|
+
entry.pty = this.deps.spawnPty(setupTokenEnv(this.deps.env));
|
|
147
|
+
} catch (error) {
|
|
148
|
+
this.finish(entry, "failed", `could not start claude setup-token: ${error instanceof Error ? error.message : String(error)}`);
|
|
149
|
+
return { ...entry.status };
|
|
150
|
+
}
|
|
151
|
+
entry.pty.onData((data) => this.onData(entry, data));
|
|
152
|
+
entry.pty.onExit(({ exitCode }) => {
|
|
153
|
+
entry.pty = null;
|
|
154
|
+
if (entry.tokenSeen) return;
|
|
155
|
+
if (entry.status.state === "starting" || entry.status.state === "waiting-code" || entry.status.state === "exchanging") {
|
|
156
|
+
this.finish(entry, "failed", findLoginError(stripAnsi(entry.output)) ?? `claude setup-token exited (${exitCode})`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
entry.timers.push(setTimeout(() => {
|
|
160
|
+
if (entry.status.state === "starting") this.finish(entry, "failed", "claude setup-token printed no sign-in URL");
|
|
161
|
+
}, LOGIN_URL_TIMEOUT_MS));
|
|
162
|
+
await Promise.race([urlReady, new Promise((r) => entry.timers.push(setTimeout(r, LOGIN_URL_TIMEOUT_MS + 50)))]);
|
|
163
|
+
return { ...entry.status };
|
|
164
|
+
}
|
|
165
|
+
submit(loginId, code) {
|
|
166
|
+
const entry = this.get(loginId);
|
|
167
|
+
if (entry.status.state !== "waiting-code" || !entry.pty) return { ...entry.status };
|
|
168
|
+
entry.status = { ...entry.status, state: "exchanging" };
|
|
169
|
+
entry.output = "";
|
|
170
|
+
entry.pty.write(code.trim().replace(/[\r\n]/g, "") + "\r");
|
|
171
|
+
entry.timers.push(setTimeout(() => {
|
|
172
|
+
if (entry.status.state === "exchanging" && !entry.tokenSeen) this.finish(entry, "failed", "timed out waiting for Claude to issue the token");
|
|
173
|
+
}, LOGIN_EXCHANGE_TIMEOUT_MS));
|
|
174
|
+
return { ...entry.status };
|
|
175
|
+
}
|
|
176
|
+
status(loginId) {
|
|
177
|
+
return { ...this.get(loginId).status };
|
|
178
|
+
}
|
|
179
|
+
cancel(loginId) {
|
|
180
|
+
const entry = this.get(loginId);
|
|
181
|
+
if (entry.status.state === "starting" || entry.status.state === "waiting-code" || entry.status.state === "exchanging") {
|
|
182
|
+
this.finish(entry, "canceled");
|
|
183
|
+
}
|
|
184
|
+
return { ...entry.status };
|
|
185
|
+
}
|
|
186
|
+
dispose() {
|
|
187
|
+
for (const entry of this.logins.values()) this.finish(entry, entry.status.state === "succeeded" ? "succeeded" : "canceled");
|
|
188
|
+
this.logins.clear();
|
|
189
|
+
}
|
|
190
|
+
get(loginId) {
|
|
191
|
+
const entry = this.logins.get(loginId);
|
|
192
|
+
if (!entry) throw Object.assign(new Error("unknown or expired login"), { code: "login-not-found" });
|
|
193
|
+
return entry;
|
|
194
|
+
}
|
|
195
|
+
onData(entry, data) {
|
|
196
|
+
entry.output = (entry.output + data).slice(-64e3);
|
|
197
|
+
const clean = stripAnsi(entry.output);
|
|
198
|
+
if (entry.status.state === "starting") {
|
|
199
|
+
const url = findAuthorizeUrl(clean);
|
|
200
|
+
if (url) {
|
|
201
|
+
const expiresAt = this.now() + LOGIN_CODE_TIMEOUT_MS;
|
|
202
|
+
entry.status = { ...entry.status, state: "waiting-code", url, expiresAt };
|
|
203
|
+
entry.timers.push(setTimeout(() => {
|
|
204
|
+
if (entry.status.state === "waiting-code") this.finish(entry, "expired");
|
|
205
|
+
}, LOGIN_CODE_TIMEOUT_MS));
|
|
206
|
+
entry.resolveUrl?.();
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (entry.status.state !== "exchanging" || entry.tokenSeen) return;
|
|
211
|
+
const token = findSetupToken(clean);
|
|
212
|
+
if (token) {
|
|
213
|
+
entry.tokenSeen = true;
|
|
214
|
+
entry.output = "";
|
|
215
|
+
this.deps.onToken(entry.status.loginId, token).then(
|
|
216
|
+
(account) => this.finish(entry, "succeeded", void 0, account),
|
|
217
|
+
(error2) => this.finish(entry, "failed", error2 instanceof Error ? error2.message : String(error2))
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const error = findLoginError(clean);
|
|
222
|
+
if (error) this.finish(entry, "failed", error);
|
|
223
|
+
}
|
|
224
|
+
finish(entry, state, error, account) {
|
|
225
|
+
for (const t2 of entry.timers) clearTimeout(t2);
|
|
226
|
+
entry.timers = [];
|
|
227
|
+
const { loginId, url, expiresAt } = entry.status;
|
|
228
|
+
entry.status = { loginId, state, ...url && state !== "succeeded" ? { url } : {}, ...expiresAt ? { expiresAt } : {}, ...account ? { account } : {}, ...error ? { error } : {} };
|
|
229
|
+
entry.resolveUrl?.();
|
|
230
|
+
const pty = entry.pty;
|
|
231
|
+
entry.pty = null;
|
|
232
|
+
try {
|
|
233
|
+
pty?.kill();
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
entry.output = "";
|
|
237
|
+
const loginId_ = loginId;
|
|
238
|
+
const t = setTimeout(() => this.logins.delete(loginId_), LOGIN_RETAIN_MS$1);
|
|
239
|
+
t.unref?.();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const DEVICE_LOGIN_TIMEOUT_MS = 15 * 6e4;
|
|
244
|
+
const DEVICE_LOGIN_START_TIMEOUT_MS = 2e4;
|
|
245
|
+
const LOGIN_RETAIN_MS = 10 * 6e4;
|
|
246
|
+
const END_AFTER_CLOSE_MS = 5e3;
|
|
247
|
+
function codexLoginEnv(base, codexHome) {
|
|
248
|
+
const env = {};
|
|
249
|
+
for (const [k, v] of Object.entries(base)) if (typeof v === "string") env[k] = v;
|
|
250
|
+
for (const key of CODEX_ALWAYS_STRIPPED_ENV_KEYS) delete env[key];
|
|
251
|
+
env.CODEX_HOME = codexHome;
|
|
252
|
+
env.RUST_LOG = "warn";
|
|
253
|
+
return env;
|
|
254
|
+
}
|
|
255
|
+
const spawnCodexRpc = (codexHome, env) => {
|
|
256
|
+
const child = spawn("codex", ["app-server", "--listen", "stdio://"], { env, cwd: codexHome, stdio: ["pipe", "pipe", "ignore"], windowsHide: true });
|
|
257
|
+
const pending = /* @__PURE__ */ new Map();
|
|
258
|
+
const listeners = [];
|
|
259
|
+
let nextId = 1;
|
|
260
|
+
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
261
|
+
let msg;
|
|
262
|
+
try {
|
|
263
|
+
msg = JSON.parse(line);
|
|
264
|
+
} catch {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (msg.id != null && pending.has(msg.id)) {
|
|
268
|
+
const p = pending.get(msg.id);
|
|
269
|
+
pending.delete(msg.id);
|
|
270
|
+
if (msg.error) p.reject(new Error(msg.error.message ?? "codex app-server error"));
|
|
271
|
+
else p.resolve(msg.result);
|
|
272
|
+
} else if (msg.method && msg.id == null) {
|
|
273
|
+
for (const cb of listeners) cb(msg.method, msg.params);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
const send = (obj) => child.stdin?.writable && child.stdin.write(JSON.stringify(obj) + "\n");
|
|
277
|
+
const request = (method, params, timeoutMs = 15e3) => new Promise((resolve, reject) => {
|
|
278
|
+
const id = nextId++;
|
|
279
|
+
const timer = setTimeout(() => {
|
|
280
|
+
pending.delete(id);
|
|
281
|
+
reject(new Error(`${method} timed out`));
|
|
282
|
+
}, timeoutMs);
|
|
283
|
+
pending.set(id, { resolve: (v) => {
|
|
284
|
+
clearTimeout(timer);
|
|
285
|
+
resolve(v);
|
|
286
|
+
}, reject: (e) => {
|
|
287
|
+
clearTimeout(timer);
|
|
288
|
+
reject(e);
|
|
289
|
+
} });
|
|
290
|
+
send({ jsonrpc: "2.0", id, method, params });
|
|
291
|
+
});
|
|
292
|
+
child.on("error", () => {
|
|
293
|
+
});
|
|
294
|
+
const initialized = request("initialize", { clientInfo: { name: "very-happy-accounts", version: "1" } }).then(() => send({ jsonrpc: "2.0", method: "initialized" }));
|
|
295
|
+
return {
|
|
296
|
+
request: async (method, params, timeoutMs) => {
|
|
297
|
+
await initialized;
|
|
298
|
+
return request(method, params, timeoutMs);
|
|
299
|
+
},
|
|
300
|
+
onNotification: (cb) => {
|
|
301
|
+
listeners.push(cb);
|
|
302
|
+
},
|
|
303
|
+
onExit: (cb) => {
|
|
304
|
+
child.on("exit", (code) => cb(code));
|
|
305
|
+
},
|
|
306
|
+
close: () => {
|
|
307
|
+
child.kill();
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
};
|
|
311
|
+
class CodexDeviceLoginManager {
|
|
312
|
+
constructor(deps) {
|
|
313
|
+
this.deps = deps;
|
|
314
|
+
this.now = deps.now ?? Date.now;
|
|
315
|
+
}
|
|
316
|
+
logins = /* @__PURE__ */ new Map();
|
|
317
|
+
now;
|
|
318
|
+
activeLoginId() {
|
|
319
|
+
for (const [id, e] of this.logins) if (e.status.state === "starting" || e.status.state === "waiting-code") return id;
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
has(loginId) {
|
|
323
|
+
return this.logins.has(loginId);
|
|
324
|
+
}
|
|
325
|
+
async start(codexHome) {
|
|
326
|
+
const busy = this.activeLoginId();
|
|
327
|
+
if (busy) throw Object.assign(new Error("another Codex sign-in is in progress"), { code: "login-busy", loginId: busy });
|
|
328
|
+
const loginId = this.deps.newId();
|
|
329
|
+
const entry = { status: { loginId, state: "starting", flow: "device-code" }, rpc: null, timers: [] };
|
|
330
|
+
this.logins.set(loginId, entry);
|
|
331
|
+
let rpc;
|
|
332
|
+
try {
|
|
333
|
+
rpc = this.deps.spawnRpc(codexHome, codexLoginEnv(this.deps.env, codexHome));
|
|
334
|
+
} catch (error) {
|
|
335
|
+
this.finish(entry, "failed", `could not start codex: ${error instanceof Error ? error.message : String(error)}`);
|
|
336
|
+
return { ...entry.status };
|
|
337
|
+
}
|
|
338
|
+
entry.rpc = rpc;
|
|
339
|
+
rpc.onExit(() => {
|
|
340
|
+
entry.rpc = null;
|
|
341
|
+
if (entry.status.state === "starting" || entry.status.state === "waiting-code") this.finish(entry, "failed", "codex app-server exited");
|
|
342
|
+
});
|
|
343
|
+
rpc.onNotification((method, params) => {
|
|
344
|
+
if (method !== "account/login/completed") return;
|
|
345
|
+
if (entry.status.state !== "starting" && entry.status.state !== "waiting-code") return;
|
|
346
|
+
if (params?.loginId && entry.codexLoginId && params.loginId !== entry.codexLoginId) return;
|
|
347
|
+
if (params?.success) void this.complete(entry);
|
|
348
|
+
else this.finish(entry, "failed", sanitizeDetail(String(params?.error ?? "sign-in failed")));
|
|
349
|
+
});
|
|
350
|
+
try {
|
|
351
|
+
const result = await rpc.request("account/login/start", { type: "chatgptDeviceCode" }, DEVICE_LOGIN_START_TIMEOUT_MS);
|
|
352
|
+
if (typeof result?.verificationUrl !== "string" || typeof result?.userCode !== "string") throw new Error("codex returned no device code");
|
|
353
|
+
entry.codexLoginId = typeof result.loginId === "string" ? result.loginId : void 0;
|
|
354
|
+
if (entry.status.state === "starting") {
|
|
355
|
+
entry.status = { ...entry.status, state: "waiting-code", url: result.verificationUrl, userCode: result.userCode, expiresAt: this.now() + DEVICE_LOGIN_TIMEOUT_MS };
|
|
356
|
+
entry.timers.push(setTimeout(() => {
|
|
357
|
+
if (entry.status.state === "waiting-code") this.finish(entry, "expired");
|
|
358
|
+
}, DEVICE_LOGIN_TIMEOUT_MS));
|
|
359
|
+
}
|
|
360
|
+
} catch (error) {
|
|
361
|
+
this.finish(entry, "failed", sanitizeDetail(error instanceof Error ? error.message : String(error)));
|
|
362
|
+
}
|
|
363
|
+
return { ...entry.status };
|
|
364
|
+
}
|
|
365
|
+
async complete(entry) {
|
|
366
|
+
if (entry.status.state !== "waiting-code" && entry.status.state !== "starting") return;
|
|
367
|
+
let account = {};
|
|
368
|
+
try {
|
|
369
|
+
const read = await entry.rpc?.request("account/read", { refreshToken: false });
|
|
370
|
+
const a = read?.account;
|
|
371
|
+
if (a?.type === "chatgpt") account = { ...a.email ? { email: String(a.email) } : {}, ...a.planType ? { planType: String(a.planType) } : {} };
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const summary = await this.deps.onSuccess(entry.status.loginId, account);
|
|
376
|
+
entry.status = { ...entry.status, account: summary };
|
|
377
|
+
this.finish(entry, "succeeded");
|
|
378
|
+
} catch (error) {
|
|
379
|
+
this.finish(entry, "failed", error instanceof Error ? error.message : String(error));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
status(loginId) {
|
|
383
|
+
return { ...this.get(loginId).status };
|
|
384
|
+
}
|
|
385
|
+
cancel(loginId) {
|
|
386
|
+
const entry = this.get(loginId);
|
|
387
|
+
if (entry.status.state === "starting" || entry.status.state === "waiting-code") {
|
|
388
|
+
if (entry.codexLoginId) void entry.rpc?.request("account/login/cancel", { loginId: entry.codexLoginId }, 5e3).catch(() => {
|
|
389
|
+
});
|
|
390
|
+
this.finish(entry, "canceled");
|
|
391
|
+
}
|
|
392
|
+
return { ...entry.status };
|
|
393
|
+
}
|
|
394
|
+
dispose() {
|
|
395
|
+
for (const entry of this.logins.values()) {
|
|
396
|
+
if (entry.status.state === "starting" || entry.status.state === "waiting-code") this.finish(entry, "canceled");
|
|
397
|
+
for (const t of entry.timers) clearTimeout(t);
|
|
398
|
+
}
|
|
399
|
+
this.logins.clear();
|
|
400
|
+
for (const end of [...this.pendingEnds]) end(true);
|
|
401
|
+
}
|
|
402
|
+
/** finish() → onEnd still waiting for its app-server to exit; dispose() runs them at once. */
|
|
403
|
+
pendingEnds = /* @__PURE__ */ new Set();
|
|
404
|
+
settleWaiters = [];
|
|
405
|
+
/** Resolves once every finished login's onEnd has run (a CLI exiting right after Ctrl-C waits for the cleanup). */
|
|
406
|
+
settle() {
|
|
407
|
+
if (this.pendingEnds.size === 0) return Promise.resolve();
|
|
408
|
+
return new Promise((resolve) => this.settleWaiters.push(resolve));
|
|
409
|
+
}
|
|
410
|
+
get(loginId) {
|
|
411
|
+
const entry = this.logins.get(loginId);
|
|
412
|
+
if (!entry) throw Object.assign(new Error(`no login "${loginId}"`), { code: "login-not-found" });
|
|
413
|
+
return entry;
|
|
414
|
+
}
|
|
415
|
+
finish(entry, state, error) {
|
|
416
|
+
entry.status = { ...entry.status, state, ...error ? { error } : {} };
|
|
417
|
+
for (const t2 of entry.timers) clearTimeout(t2);
|
|
418
|
+
entry.timers = [];
|
|
419
|
+
const rpc = entry.rpc;
|
|
420
|
+
entry.rpc = null;
|
|
421
|
+
const loginId = entry.status.loginId;
|
|
422
|
+
if (!rpc) this.deps.onEnd?.(loginId, state);
|
|
423
|
+
else {
|
|
424
|
+
let ended = false;
|
|
425
|
+
const end = (closeNow) => {
|
|
426
|
+
if (ended) return;
|
|
427
|
+
ended = true;
|
|
428
|
+
this.pendingEnds.delete(end);
|
|
429
|
+
if (closeNow) rpc.close();
|
|
430
|
+
this.deps.onEnd?.(loginId, state);
|
|
431
|
+
if (this.pendingEnds.size === 0) for (const resolve of this.settleWaiters.splice(0)) resolve();
|
|
432
|
+
};
|
|
433
|
+
this.pendingEnds.add(end);
|
|
434
|
+
rpc.onExit(() => end());
|
|
435
|
+
setTimeout(() => rpc.close(), state === "canceled" ? 500 : 0).unref?.();
|
|
436
|
+
setTimeout(() => end(), END_AFTER_CLOSE_MS).unref?.();
|
|
437
|
+
}
|
|
438
|
+
const t = setTimeout(() => this.logins.delete(entry.status.loginId), LOGIN_RETAIN_MS);
|
|
439
|
+
t.unref?.();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function classifyChatgptRead(read, now) {
|
|
443
|
+
const account = read?.account;
|
|
444
|
+
if (account && account.type === "chatgpt") return { status: "ok", at: now };
|
|
445
|
+
return { status: "expired", at: now, detail: "not signed in" };
|
|
446
|
+
}
|
|
447
|
+
async function checkCodexChatgptAccount(codexHome, env, spawnRpc = spawnCodexRpc, now = Date.now) {
|
|
448
|
+
let rpc = null;
|
|
449
|
+
try {
|
|
450
|
+
rpc = spawnRpc(codexHome, codexLoginEnv(env, codexHome));
|
|
451
|
+
const read = await rpc.request("account/read", { refreshToken: true }, CHECK_TIMEOUT_MS);
|
|
452
|
+
return classifyChatgptRead(read, now());
|
|
453
|
+
} catch (error) {
|
|
454
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
455
|
+
if (/refresh token|401|unauthori[sz]ed|sign in again|log in again/i.test(message)) return { status: "expired", at: now(), detail: sanitizeDetail(message) };
|
|
456
|
+
return { status: "unverified", at: now(), detail: sanitizeDetail(message) };
|
|
457
|
+
} finally {
|
|
458
|
+
rpc?.close();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function checkCodexProviderAccount(baseUrl, key, headers = {}, now = Date.now) {
|
|
462
|
+
const controller = new AbortController();
|
|
463
|
+
const timer = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
|
|
464
|
+
try {
|
|
465
|
+
const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/models`, { signal: controller.signal, headers: { ...headers, authorization: `Bearer ${key}` } });
|
|
466
|
+
const body = res.ok ? "" : (await res.text().catch(() => "")).slice(0, 300);
|
|
467
|
+
return classifyHttpCheck("codex-provider", res.status, body, now());
|
|
468
|
+
} catch (error) {
|
|
469
|
+
return { status: "unverified", at: now(), detail: sanitizeDetail(error instanceof Error ? error.message : String(error)) };
|
|
470
|
+
} finally {
|
|
471
|
+
clearTimeout(timer);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const ACCOUNT_AGENTS = ["claude", "codex"];
|
|
476
|
+
class AccountError extends Error {
|
|
477
|
+
constructor(code, message, sessions) {
|
|
478
|
+
super(message);
|
|
479
|
+
this.code = code;
|
|
480
|
+
this.sessions = sessions;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const DAEMON_DOWN = /^(No daemon running|Daemon is not running)/;
|
|
484
|
+
function localRestartFailure(error, stillRunning) {
|
|
485
|
+
return { ok: false, stillRunning, notRunning: !stillRunning && (error.startsWith("restart-precheck:not-tracked") || DAEMON_DOWN.test(error)), error };
|
|
486
|
+
}
|
|
487
|
+
function bindResultMessages(result, sessionId, target, noun) {
|
|
488
|
+
const bound = `Bound ${sessionId} \u2192 ${target}.`;
|
|
489
|
+
if (result.restart === "done") return { out: [bound, "Restarted in place (history kept)."], err: [], exitCode: 0 };
|
|
490
|
+
if (result.restart === "pending") return { out: [bound, "Restart in progress."], err: [], exitCode: 0 };
|
|
491
|
+
if (result.restart === "skipped") return { out: [bound, `Not restarted${result.error ? ` (${result.error})` : ""}; applies when the session next starts.`], err: [], exitCode: 0 };
|
|
492
|
+
if (result.rolledBack) return { out: [], err: [`Not switched: the restart failed (${result.error}). The session keeps running on its previous ${noun}; the binding was not changed.`], exitCode: 1 };
|
|
493
|
+
return { out: [], err: [`Binding to ${target} saved, but the restart failed (${result.error}); the session is not running and starts on ${target} next time.`], exitCode: 1 };
|
|
494
|
+
}
|
|
495
|
+
const BIND_RESTART_BUDGET_MS = 22e3;
|
|
496
|
+
class AccountService {
|
|
497
|
+
constructor(deps) {
|
|
498
|
+
this.deps = deps;
|
|
499
|
+
this.home = deps.home;
|
|
500
|
+
this.env = deps.env ?? process.env;
|
|
501
|
+
this.now = deps.now ?? Date.now;
|
|
502
|
+
}
|
|
503
|
+
home;
|
|
504
|
+
env;
|
|
505
|
+
now;
|
|
506
|
+
logins = null;
|
|
507
|
+
codexLogins = null;
|
|
508
|
+
/** loginId → account to (re)write when its token arrives. */
|
|
509
|
+
loginTargets = /* @__PURE__ */ new Map();
|
|
510
|
+
state() {
|
|
511
|
+
const state = buildAgentProfilesState(this.home, readAccounts(this.home), defaultSecretProbe(this.env), this.now(), this.deps.daemonPid ?? process.pid);
|
|
512
|
+
const codexProviders = readCodexProviders(this.env).map((p) => ({ id: p.id, ...p.name ? { name: p.name } : {}, ...p.baseUrl ? { baseUrl: p.baseUrl } : {}, ...p.envKey ? { envKey: p.envKey } : {} }));
|
|
513
|
+
const withFeatures = { ...state, features: [AGENT_PROFILE_FEATURE_LAUNCH] };
|
|
514
|
+
return codexProviders.length > 0 ? { ...withFeatures, codexProviders } : withFeatures;
|
|
515
|
+
}
|
|
516
|
+
summary(id) {
|
|
517
|
+
const record = readAccounts(this.home).accounts[id];
|
|
518
|
+
if (!record) throw new AccountError("not-found", `no account "${id}"`);
|
|
519
|
+
return summarizeAccount(this.home, id, record, defaultSecretProbe(this.env), this.now());
|
|
520
|
+
}
|
|
521
|
+
changed() {
|
|
522
|
+
try {
|
|
523
|
+
this.deps.onChanged?.();
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
editable(id) {
|
|
528
|
+
const file = readAccountsFileV2(this.home);
|
|
529
|
+
const record = file.accounts[id];
|
|
530
|
+
if (!record) {
|
|
531
|
+
if (readAccounts(this.home).accounts[id]?.legacy) throw new AccountError("legacy-readonly", `"${id}" comes from providers.json; edit or remove it there`);
|
|
532
|
+
throw new AccountError("not-found", `no account "${id}"`);
|
|
533
|
+
}
|
|
534
|
+
return { file, record };
|
|
535
|
+
}
|
|
536
|
+
// ---- add / edit -----------------------------------------------------------
|
|
537
|
+
async add(params) {
|
|
538
|
+
const parsed = AgentProfileAddRequestSchema.safeParse(params);
|
|
539
|
+
if (!parsed.success) throw new AccountError("invalid-params", parsed.error.issues[0]?.message ?? "invalid params");
|
|
540
|
+
const req = parsed.data;
|
|
541
|
+
if (req.agent === "codex" && req.provider && !readCodexProviders(this.env).some((p) => p.id === req.provider)) {
|
|
542
|
+
throw new AccountError("not-found", `no [model_providers.${req.provider}] in the machine's Codex config.toml`);
|
|
543
|
+
}
|
|
544
|
+
const file = readAccountsFileV2(this.home);
|
|
545
|
+
const id = newAccountId(req.label, req.kind, Object.keys(readAccounts(this.home).accounts));
|
|
546
|
+
const models = [];
|
|
547
|
+
const contextWindows = {};
|
|
548
|
+
for (const m of req.models ?? []) {
|
|
549
|
+
if (typeof m === "string") models.push(m);
|
|
550
|
+
else {
|
|
551
|
+
models.push(m.id);
|
|
552
|
+
if (m.contextWindow) contextWindows[m.id] = m.contextWindow;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const record = {
|
|
556
|
+
label: req.label,
|
|
557
|
+
agent: req.agent,
|
|
558
|
+
kind: req.kind,
|
|
559
|
+
createdAt: this.now(),
|
|
560
|
+
...req.baseUrl ? { baseUrl: req.baseUrl.replace(/\/+$/, "") } : {},
|
|
561
|
+
...req.agent === "codex" && req.provider ? { provider: req.provider } : {},
|
|
562
|
+
...req.agent === "codex" && req.wireApi ? { wireApi: req.wireApi } : {},
|
|
563
|
+
key: req.key.source === "paste" ? { source: "stored" } : req.key.source === "file" ? { source: "file", path: req.key.path } : { source: "env", name: req.key.name },
|
|
564
|
+
...models.length > 0 ? { models } : {},
|
|
565
|
+
...Object.keys(contextWindows).length > 0 ? { contextWindows } : {}
|
|
566
|
+
};
|
|
567
|
+
if (req.key.source === "paste") writeAccountSecret(this.home, id, req.key.value);
|
|
568
|
+
file.accounts[id] = record;
|
|
569
|
+
writeAccountsFile(this.home, file);
|
|
570
|
+
this.changed();
|
|
571
|
+
return this.test(id);
|
|
572
|
+
}
|
|
573
|
+
rename(accountId, label) {
|
|
574
|
+
if (typeof accountId !== "string" || typeof label !== "string" || !label.trim() || label.trim().length > 60) {
|
|
575
|
+
throw new AccountError("invalid-params", "accountId and a 1\u201360 character label are required");
|
|
576
|
+
}
|
|
577
|
+
const { file, record } = this.editable(accountId);
|
|
578
|
+
record.label = label.trim();
|
|
579
|
+
writeAccountsFile(this.home, file);
|
|
580
|
+
this.changed();
|
|
581
|
+
return this.summary(accountId);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* `force` with bound sessions: the credential is gone, so each of those
|
|
585
|
+
* sessions restarts in place right away and comes back `unavailable`
|
|
586
|
+
* (refusing turns, Q3) instead of running on the deleted credential until
|
|
587
|
+
* its next start. Restarts run in parallel within the bind budget; one
|
|
588
|
+
* still going reports 'pending'.
|
|
589
|
+
*/
|
|
590
|
+
async remove(accountId, force) {
|
|
591
|
+
if (typeof accountId !== "string") throw new AccountError("invalid-params", "accountId is required");
|
|
592
|
+
const { file } = this.editable(accountId);
|
|
593
|
+
const sessions = sessionsBoundTo(readSessionBindings(this.home), accountId);
|
|
594
|
+
if (sessions.length > 0 && force !== true) {
|
|
595
|
+
throw new AccountError("in-use", `${sessions.length} session(s) use this account; switch them first or remove with force`, sessions);
|
|
596
|
+
}
|
|
597
|
+
delete file.accounts[accountId];
|
|
598
|
+
for (const [agent, id] of Object.entries(file.defaults)) if (id === accountId) delete file.defaults[agent];
|
|
599
|
+
writeAccountsFile(this.home, file);
|
|
600
|
+
removeAccountSecret(this.home, accountId);
|
|
601
|
+
this.changed();
|
|
602
|
+
if (sessions.length === 0 || !this.deps.restartSession) return { removed: true, sessions };
|
|
603
|
+
const restarts = {};
|
|
604
|
+
await Promise.all(sessions.map(async (sessionId) => {
|
|
605
|
+
restarts[sessionId] = await this.restartWithinBudget(sessionId);
|
|
606
|
+
}));
|
|
607
|
+
return { removed: true, sessions, restarts };
|
|
608
|
+
}
|
|
609
|
+
/** 'done' | 'skipped' (not running here) | 'pending' (still going after the budget) | 'failed' */
|
|
610
|
+
async restartWithinBudget(sessionId) {
|
|
611
|
+
const outcome = await Promise.race([
|
|
612
|
+
this.deps.restartSession(sessionId),
|
|
613
|
+
new Promise((r) => setTimeout(() => r("pending"), BIND_RESTART_BUDGET_MS).unref?.())
|
|
614
|
+
]);
|
|
615
|
+
if (outcome === "pending") return "pending";
|
|
616
|
+
if (outcome.ok) return "done";
|
|
617
|
+
return outcome.notRunning ? "skipped" : "failed";
|
|
618
|
+
}
|
|
619
|
+
setDefault(agent, accountId) {
|
|
620
|
+
if (!ACCOUNT_AGENTS.includes(agent)) throw new AccountError("invalid-params", 'agent must be "claude" or "codex"');
|
|
621
|
+
if (accountId !== null && typeof accountId !== "string") throw new AccountError("invalid-params", "accountId must be a string or null");
|
|
622
|
+
if (typeof accountId === "string") {
|
|
623
|
+
const record = readAccounts(this.home).accounts[accountId];
|
|
624
|
+
if (!record) throw new AccountError("not-found", `no account "${accountId}"`);
|
|
625
|
+
if (record.agent !== agent) throw new AccountError("invalid-params", `"${accountId}" is not a ${agent} account`);
|
|
626
|
+
}
|
|
627
|
+
const file = readAccountsFileV2(this.home);
|
|
628
|
+
if (accountId === null) delete file.defaults[agent];
|
|
629
|
+
else file.defaults[agent] = accountId;
|
|
630
|
+
writeAccountsFile(this.home, file);
|
|
631
|
+
this.changed();
|
|
632
|
+
return this.state();
|
|
633
|
+
}
|
|
634
|
+
// ---- check ---------------------------------------------------------------
|
|
635
|
+
readSecretFor(id, record) {
|
|
636
|
+
try {
|
|
637
|
+
if (record.kind === "claude-subscription" || record.key?.source === "stored") return readFileSync(accountSecretPath(this.home, id), "utf8").trim() || null;
|
|
638
|
+
if (record.key?.source === "file") return readFileSync(expandHome(record.key.path), "utf8").trim() || null;
|
|
639
|
+
if (record.key?.source === "env") return this.env[record.key.name]?.trim() || null;
|
|
640
|
+
} catch {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
async test(accountId) {
|
|
646
|
+
if (typeof accountId !== "string") throw new AccountError("invalid-params", "accountId is required");
|
|
647
|
+
const record = readAccounts(this.home).accounts[accountId];
|
|
648
|
+
if (!record) throw new AccountError("not-found", `no account "${accountId}"`);
|
|
649
|
+
let check;
|
|
650
|
+
if (record.kind === "codex-chatgpt") {
|
|
651
|
+
if (this.summary(accountId).status === "missing-secret") return this.summary(accountId);
|
|
652
|
+
const codexHome = accountCodexHomePath(this.home, accountId);
|
|
653
|
+
syncCodexOverlay(codexHome, baseCodexHome(this.env), this.now());
|
|
654
|
+
check = this.deps.check ? await this.deps.check(record, "") : await checkCodexChatgptAccount(codexHome, this.env, this.deps.spawnCodexRpc ?? spawnCodexRpc, this.now);
|
|
655
|
+
} else {
|
|
656
|
+
const secret = this.readSecretFor(accountId, record);
|
|
657
|
+
if (!secret) return this.summary(accountId);
|
|
658
|
+
check = this.deps.check ? await this.deps.check(record, secret) : record.kind === "claude-subscription" ? await checkSubscriptionAccount(secret, join(this.home, "tmp", "account-check"), this.now) : record.kind === "codex-provider" ? await this.checkCodexProvider(record, secret) : await checkApiAccount(record, secret, this.now);
|
|
659
|
+
}
|
|
660
|
+
if (!record.legacy) {
|
|
661
|
+
const file = readAccountsFileV2(this.home);
|
|
662
|
+
if (file.accounts[accountId]) {
|
|
663
|
+
file.accounts[accountId].check = check;
|
|
664
|
+
writeAccountsFile(this.home, file);
|
|
665
|
+
this.changed();
|
|
666
|
+
}
|
|
667
|
+
return this.summary(accountId);
|
|
668
|
+
}
|
|
669
|
+
return { ...this.summary(accountId), status: check.status, checkedAt: check.at, ...check.detail ? { statusDetail: check.detail } : {} };
|
|
670
|
+
}
|
|
671
|
+
async checkCodexProvider(record, secret) {
|
|
672
|
+
const base = record.provider ? readCodexProviders(this.env).find((p) => p.id === record.provider) : void 0;
|
|
673
|
+
const baseUrl = record.baseUrl ?? base?.baseUrl;
|
|
674
|
+
if (!baseUrl) return { status: "unverified", at: this.now(), detail: `provider "${record.provider}" is not in the machine's Codex config.toml` };
|
|
675
|
+
const headers = {};
|
|
676
|
+
const raw = base?.table.http_headers;
|
|
677
|
+
if (raw && typeof raw === "object") {
|
|
678
|
+
for (const [k, v] of Object.entries(raw)) if (typeof v === "string") headers[k] = v;
|
|
679
|
+
}
|
|
680
|
+
return checkCodexProviderAccount(baseUrl, secret, headers, this.now);
|
|
681
|
+
}
|
|
682
|
+
// ---- remote login (claude setup-token) -------------------------------------
|
|
683
|
+
loginManager() {
|
|
684
|
+
if (this.logins) return this.logins;
|
|
685
|
+
const spawnPty = this.deps.spawnPty;
|
|
686
|
+
if (!spawnPty) throw new AccountError("internal", "remote login is not available in this process");
|
|
687
|
+
this.logins = new SetupTokenLoginManager({
|
|
688
|
+
spawnPty,
|
|
689
|
+
env: this.env,
|
|
690
|
+
newId: () => randomUUID(),
|
|
691
|
+
now: this.now,
|
|
692
|
+
onToken: async (loginId, token) => {
|
|
693
|
+
const target = this.loginTargets.get(loginId) ?? {};
|
|
694
|
+
this.loginTargets.delete(loginId);
|
|
695
|
+
return this.storeSubscriptionToken(token, target);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
return this.logins;
|
|
699
|
+
}
|
|
700
|
+
/** Also the terminal path (`very-happy account login`). */
|
|
701
|
+
storeSubscriptionToken(token, target) {
|
|
702
|
+
const file = readAccountsFileV2(this.home);
|
|
703
|
+
const now = this.now();
|
|
704
|
+
let id = target.accountId;
|
|
705
|
+
if (id) {
|
|
706
|
+
const record = file.accounts[id];
|
|
707
|
+
if (!record || record.kind !== "claude-subscription") throw new AccountError("not-found", `no subscription account "${id}"`);
|
|
708
|
+
record.expiresAt = now + SUBSCRIPTION_TOKEN_TTL_MS;
|
|
709
|
+
record.check = { status: "ok", at: now };
|
|
710
|
+
} else {
|
|
711
|
+
const label = target.label?.trim() || "Claude";
|
|
712
|
+
id = newAccountId(label, "claude-subscription", Object.keys(readAccounts(this.home).accounts));
|
|
713
|
+
file.accounts[id] = { label, agent: "claude", kind: "claude-subscription", createdAt: now, expiresAt: now + SUBSCRIPTION_TOKEN_TTL_MS, check: { status: "ok", at: now } };
|
|
714
|
+
}
|
|
715
|
+
writeAccountSecret(this.home, id, token);
|
|
716
|
+
writeAccountsFile(this.home, file);
|
|
717
|
+
this.changed();
|
|
718
|
+
return this.summary(id);
|
|
719
|
+
}
|
|
720
|
+
async loginStart(params) {
|
|
721
|
+
const parsed = AgentProfileLoginStartRequestSchema.safeParse(params);
|
|
722
|
+
if (!parsed.success) throw new AccountError("invalid-params", parsed.error.issues[0]?.message ?? "invalid params");
|
|
723
|
+
if (parsed.data.agent === "codex") return this.codexLoginStart(parsed.data.accountId, parsed.data.label);
|
|
724
|
+
const { accountId, label } = parsed.data;
|
|
725
|
+
if (accountId) {
|
|
726
|
+
const record = readAccounts(this.home).accounts[accountId];
|
|
727
|
+
if (!record || record.kind !== "claude-subscription") throw new AccountError("not-found", `no subscription account "${accountId}"`);
|
|
728
|
+
} else if (!label) {
|
|
729
|
+
throw new AccountError("invalid-params", "label is required for a new account");
|
|
730
|
+
}
|
|
731
|
+
let status;
|
|
732
|
+
try {
|
|
733
|
+
status = await this.loginManager().start();
|
|
734
|
+
} catch (error) {
|
|
735
|
+
const code = error.code ?? "setup-token-failed";
|
|
736
|
+
throw new AccountError(code, error instanceof Error ? error.message : String(error));
|
|
737
|
+
}
|
|
738
|
+
this.loginTargets.set(status.loginId, { accountId, label });
|
|
739
|
+
return { ...status, flow: "paste-code" };
|
|
740
|
+
}
|
|
741
|
+
// ---- remote login (Codex ChatGPT device code) — B-850b ----------------------
|
|
742
|
+
codexLoginManager() {
|
|
743
|
+
if (this.codexLogins) return this.codexLogins;
|
|
744
|
+
this.codexLogins = new CodexDeviceLoginManager({
|
|
745
|
+
spawnRpc: this.deps.spawnCodexRpc ?? spawnCodexRpc,
|
|
746
|
+
env: this.env,
|
|
747
|
+
newId: () => randomUUID(),
|
|
748
|
+
now: this.now,
|
|
749
|
+
onSuccess: async (loginId, info) => {
|
|
750
|
+
const target = this.codexLoginTargets.get(loginId);
|
|
751
|
+
this.codexLoginTargets.delete(loginId);
|
|
752
|
+
if (!target) throw new AccountError("internal", "sign-in target lost");
|
|
753
|
+
return this.storeChatgptAccount(target, info);
|
|
754
|
+
},
|
|
755
|
+
onEnd: (loginId, state) => {
|
|
756
|
+
const target = this.codexLoginTargets.get(loginId);
|
|
757
|
+
if (state === "succeeded" || !target) return;
|
|
758
|
+
this.codexLoginTargets.delete(loginId);
|
|
759
|
+
if (target.isNew) removeAccountSecret(this.home, target.accountId);
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
return this.codexLogins;
|
|
763
|
+
}
|
|
764
|
+
/** loginId → the account a Codex sign-in writes. */
|
|
765
|
+
codexLoginTargets = /* @__PURE__ */ new Map();
|
|
766
|
+
async codexLoginStart(accountId, label) {
|
|
767
|
+
const accounts = readAccounts(this.home).accounts;
|
|
768
|
+
let target;
|
|
769
|
+
if (accountId) {
|
|
770
|
+
const record = accounts[accountId];
|
|
771
|
+
if (!record || record.kind !== "codex-chatgpt") throw new AccountError("not-found", `no ChatGPT account "${accountId}"`);
|
|
772
|
+
target = { accountId, isNew: false };
|
|
773
|
+
} else {
|
|
774
|
+
const taken = [...Object.keys(accounts), ...[...this.codexLoginTargets.values()].map((t) => t.accountId)];
|
|
775
|
+
target = { accountId: newAccountId(label ?? "", "codex-chatgpt", taken), ...label ? { label } : {}, isNew: true };
|
|
776
|
+
}
|
|
777
|
+
const manager = this.codexLoginManager();
|
|
778
|
+
const busy = manager.activeLoginId();
|
|
779
|
+
if (busy) throw new AccountError("login-busy", "another Codex sign-in is in progress");
|
|
780
|
+
const codexHome = accountCodexHomePath(this.home, target.accountId);
|
|
781
|
+
syncCodexOverlay(codexHome, baseCodexHome(this.env), this.now());
|
|
782
|
+
const status = await manager.start(codexHome);
|
|
783
|
+
if (status.state === "failed" || status.state === "canceled" || status.state === "expired") {
|
|
784
|
+
if (target.isNew) removeAccountSecret(this.home, target.accountId);
|
|
785
|
+
return status;
|
|
786
|
+
}
|
|
787
|
+
this.codexLoginTargets.set(status.loginId, target);
|
|
788
|
+
return status;
|
|
789
|
+
}
|
|
790
|
+
/** A finished ChatGPT sign-in → the account record (new) or its refreshed status (re-login). */
|
|
791
|
+
storeChatgptAccount(target, info) {
|
|
792
|
+
const file = readAccountsFileV2(this.home);
|
|
793
|
+
const now = this.now();
|
|
794
|
+
const existing = file.accounts[target.accountId];
|
|
795
|
+
if (existing && existing.kind === "codex-chatgpt") {
|
|
796
|
+
existing.check = { status: "ok", at: now };
|
|
797
|
+
if (info.email) existing.email = info.email;
|
|
798
|
+
if (info.planType) existing.planType = info.planType;
|
|
799
|
+
} else {
|
|
800
|
+
file.accounts[target.accountId] = {
|
|
801
|
+
label: target.label?.trim() || info.email || "ChatGPT",
|
|
802
|
+
agent: "codex",
|
|
803
|
+
kind: "codex-chatgpt",
|
|
804
|
+
createdAt: now,
|
|
805
|
+
check: { status: "ok", at: now },
|
|
806
|
+
...info.email ? { email: info.email } : {},
|
|
807
|
+
...info.planType ? { planType: info.planType } : {}
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
writeAccountsFile(this.home, file);
|
|
811
|
+
this.changed();
|
|
812
|
+
return this.summary(target.accountId);
|
|
813
|
+
}
|
|
814
|
+
loginOp(fn, loginId, codex) {
|
|
815
|
+
try {
|
|
816
|
+
if (codex && typeof loginId === "string" && this.codexLogins?.has(loginId)) return codex(this.codexLogins);
|
|
817
|
+
return fn(this.loginManager());
|
|
818
|
+
} catch (error) {
|
|
819
|
+
const code = error.code ?? "internal";
|
|
820
|
+
throw new AccountError(code, error instanceof Error ? error.message : String(error));
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
loginSubmit(params) {
|
|
824
|
+
const parsed = AgentProfileLoginSubmitRequestSchema.safeParse(params);
|
|
825
|
+
if (!parsed.success) throw new AccountError("invalid-params", "loginId and code are required");
|
|
826
|
+
if (this.codexLogins?.has(parsed.data.loginId)) throw new AccountError("invalid-params", "a Codex device-code sign-in has nothing to submit: enter the code on the sign-in page");
|
|
827
|
+
return this.loginOp((m) => m.submit(parsed.data.loginId, parsed.data.code));
|
|
828
|
+
}
|
|
829
|
+
loginStatus(loginId) {
|
|
830
|
+
if (typeof loginId !== "string") throw new AccountError("invalid-params", "loginId is required");
|
|
831
|
+
return this.loginOp((m) => m.status(loginId), loginId, (m) => m.status(loginId));
|
|
832
|
+
}
|
|
833
|
+
loginCancel(loginId) {
|
|
834
|
+
if (typeof loginId !== "string") throw new AccountError("invalid-params", "loginId is required");
|
|
835
|
+
const status = this.loginOp((m) => m.cancel(loginId), loginId, (m) => m.cancel(loginId));
|
|
836
|
+
this.loginTargets.delete(loginId);
|
|
837
|
+
return status;
|
|
838
|
+
}
|
|
839
|
+
dispose() {
|
|
840
|
+
this.logins?.dispose();
|
|
841
|
+
this.codexLogins?.dispose();
|
|
842
|
+
}
|
|
843
|
+
/** B-850d2: waits until finished Codex sign-ins are cleaned up (their app-server gone, a canceled new account's overlay removed). */
|
|
844
|
+
settleLogins() {
|
|
845
|
+
return this.codexLogins?.settle() ?? Promise.resolve();
|
|
846
|
+
}
|
|
847
|
+
// ---- sessions --------------------------------------------------------------
|
|
848
|
+
/** Validates and writes a session binding (no restart). */
|
|
849
|
+
writeBinding(sessionId, accountId, model) {
|
|
850
|
+
if (accountId !== null) {
|
|
851
|
+
const record = readAccounts(this.home).accounts[accountId];
|
|
852
|
+
if (!record) throw new AccountError("not-found", `no account "${accountId}"`);
|
|
853
|
+
const agent = this.deps.sessionAgent?.(sessionId);
|
|
854
|
+
if (agent && agent !== record.agent) throw new AccountError("invalid-params", `"${accountId}" is a ${record.agent} account; this session runs ${agent}`);
|
|
855
|
+
if (model !== void 0) {
|
|
856
|
+
if (record.kind === "claude-subscription") throw new AccountError("invalid-params", "model applies to API and Codex accounts only");
|
|
857
|
+
if (record.models && record.models.length > 0 && !record.models.includes(model)) {
|
|
858
|
+
throw new AccountError("invalid-params", `model "${model}" is not in ${accountId}'s models (${record.models.join(", ")})`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
} else if (model !== void 0) {
|
|
862
|
+
throw new AccountError("invalid-params", "model cannot be set for the machine login");
|
|
863
|
+
}
|
|
864
|
+
writeSessionBinding(this.home, sessionId, { account: accountId, ...model ? { model } : {} });
|
|
865
|
+
}
|
|
866
|
+
async bind(params) {
|
|
867
|
+
const parsed = AgentProfileBindRequestSchema.safeParse(params);
|
|
868
|
+
if (!parsed.success) throw new AccountError("invalid-params", parsed.error.issues[0]?.message ?? "invalid params");
|
|
869
|
+
const { sessionId, accountId, model, restart } = parsed.data;
|
|
870
|
+
const previous = readSessionBindingV2(this.home, sessionId);
|
|
871
|
+
this.writeBinding(sessionId, accountId, model);
|
|
872
|
+
this.changed();
|
|
873
|
+
if (restart === false || !this.deps.restartSession) return { bound: true, restart: "skipped" };
|
|
874
|
+
const outcome = await Promise.race([
|
|
875
|
+
this.deps.restartSession(sessionId),
|
|
876
|
+
new Promise((r) => setTimeout(() => r("pending"), BIND_RESTART_BUDGET_MS).unref?.())
|
|
877
|
+
]);
|
|
878
|
+
if (outcome === "pending") return { bound: true, restart: "pending" };
|
|
879
|
+
if (outcome.ok) return { bound: true, restart: "done" };
|
|
880
|
+
if (outcome.stillRunning) {
|
|
881
|
+
writeSessionBinding(this.home, sessionId, previous);
|
|
882
|
+
this.changed();
|
|
883
|
+
return { bound: false, rolledBack: true, restart: "failed", error: outcome.error };
|
|
884
|
+
}
|
|
885
|
+
if (outcome.notRunning) return { bound: true, restart: "skipped", error: outcome.error };
|
|
886
|
+
return { bound: true, restart: "failed", error: outcome.error };
|
|
887
|
+
}
|
|
888
|
+
/** B-850d: the account a session is bound to (null = machine login, also when unbound). */
|
|
889
|
+
sessionAccount(sessionId) {
|
|
890
|
+
return readSessionBindings(this.home)[sessionId]?.account ?? null;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Spawn-time choice → the account a NEW session starts on.
|
|
894
|
+
* undefined → the machine default for this agent (none = machine login)
|
|
895
|
+
* null → machine login
|
|
896
|
+
* id → that account; must exist and be usable (Q3: never fall back)
|
|
897
|
+
*/
|
|
898
|
+
resolveSpawnAccount(agent, requested) {
|
|
899
|
+
if (requested !== void 0 && requested !== null && typeof requested !== "string") {
|
|
900
|
+
throw new AccountError("invalid-params", "agentProfileId must be a string or null");
|
|
901
|
+
}
|
|
902
|
+
const file = readAccounts(this.home);
|
|
903
|
+
const id = requested === void 0 ? file.defaults[agent] ?? null : requested;
|
|
904
|
+
if (id === null || id === "") return null;
|
|
905
|
+
const record = file.accounts[id];
|
|
906
|
+
if (!record) throw new AccountError("agent-profile-unavailable", `agent-profile-unavailable:missing: no account "${id}" on this machine`);
|
|
907
|
+
if (record.agent !== agent) throw new AccountError("agent-profile-unavailable", `agent-profile-unavailable:agent-mismatch: "${id}" is a ${record.agent} account`);
|
|
908
|
+
const { status } = summarizeAccount(this.home, id, record, defaultSecretProbe(this.env), this.now());
|
|
909
|
+
if (status === "missing-secret" || status === "expired" || status === "invalid") {
|
|
910
|
+
throw new AccountError("agent-profile-unavailable", `agent-profile-unavailable:${status}: account "${record.label}"`);
|
|
911
|
+
}
|
|
912
|
+
return id;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
export { AccountService as A, AccountError as a, bindResultMessages as b, localRestartFailure as l };
|